using System.Diagnostics.CodeAnalysis;
using PkmnLib.Dynamic.BattleFlow;
using PkmnLib.Dynamic.Events;
using PkmnLib.Dynamic.Libraries;
using PkmnLib.Dynamic.Models.Choices;
using PkmnLib.Dynamic.ScriptHandling;
using PkmnLib.Static;
using PkmnLib.Static.Utils;

namespace PkmnLib.Dynamic.Models;

/// <summary>
/// A battle is a representation of a battle in the Pokemon games. It contains all the information needed
/// to simulate a battle, and can be used to simulate a battle between two parties.
/// </summary>
public interface IBattle : IScriptSource, IDeepCloneable, IDisposable
{
    /// <summary>
    /// The library the battle uses for handling.
    /// </summary>
    IDynamicLibrary Library { get; }

    /// <summary>
    /// A list of all different parties in the battle.
    /// </summary>
    IReadOnlyList<IBattleParty> Parties { get; }

    /// <summary>
    /// Whether or not Pokemon can flee from the battle.
    /// </summary>
    bool CanFlee { get; }

    /// <summary>
    /// The number of sides in the battle. Typically 2.
    /// </summary>
    byte NumberOfSides { get; }

    /// <summary>
    /// The number of Pokemon that can be on each side.
    /// </summary>
    byte PositionsPerSide { get; }

    /// <summary>
    /// The name of the environment the battle is taking place in, such as "grass", "cave", etc.
    /// This is sometimes referred to as the "terrain" in the games, but is not the same as the
    /// <see cref="TerrainName"/> which is a battle condition that can be set by scripts.
    /// </summary>
    StringKey EnvironmentName { get; }

    /// <summary>
    /// Whether this battle is a wild battle. In a wild battle, the player can catch the opposing Pokemon,
    /// and moves like roar will end the battle instead of switching out the Pokemon.
    /// </summary>
    bool IsWildBattle { get; }

    /// <summary>
    /// A list of all sides in the battle.
    /// </summary>
    IReadOnlyList<IBattleSide> Sides { get; }

    /// <summary>
    /// The RNG used for the battle.
    /// </summary>
    IBattleRandom Random { get; }

    /// <summary>
    /// Whether the battle has ended.
    /// </summary>
    [MemberNotNull(nameof(Result))]
    bool HasEnded { get; }

    /// <summary>
    /// The result of the battle. If the battle has not ended, this is null.
    /// </summary>
    BattleResult? Result { get; }

    /// <summary>
    /// The handler to send all events to.
    /// </summary>
    EventHook EventHook { get; }

    /// <summary>
    /// The index of the current turn. Initially 0, until the first turn starts when all choices are made.
    /// </summary>
    uint CurrentTurnNumber { get; }

    /// <summary>
    /// A queue of the yet to be executed choices in a turn.
    /// </summary>
    BattleChoiceQueue? ChoiceQueue { get; }

    /// <summary>
    /// Get a Pokemon on the battlefield, on a specific side and an index on that side.
    /// </summary>
    IPokemon? GetPokemon(byte side, byte position);

    /// <summary>
    /// Returns whether a slot on the battlefield can still be filled. If no party is responsible
    /// for that slot, or a party is responsible, but has no remaining Pokemon to throw out anymore,
    /// this returns false.
    /// </summary>
    bool CanSlotBeFilled(byte side, byte position);

    /// <summary>
    /// Validates whether the battle is still in a non-ended state. If the battle has ended, this
    /// properly sets who has won etc.
    /// </summary>
    void ValidateBattleState();

    /// <summary>
    /// Forcefully ends the battle. This will set the result to inconclusive and set HasEnded to true.
    /// </summary>
    void ForceEndBattle();

    /// <summary>
    /// Checks whether a Pokemon has a forced turn choice. If it does, this returns true and the choice
    /// is set in the out parameter. If it does not, this returns false and the out parameter is null.
    /// </summary>
    bool HasForcedTurn(IPokemon pokemon, [NotNullWhen(true)] out ITurnChoice? choice);

    /// <summary>
    /// Checks whether a choice is actually possible.
    /// </summary>
    bool CanUse(ITurnChoice choice);

    /// <summary>
    /// Try and set the choice for the battle. If the choice is not valid, this returns false.
    /// </summary>
    bool TrySetChoice(ITurnChoice choice);

    /// <summary>
    /// The script that handles the current weather of the battle.
    /// </summary>
    IReadOnlyScriptContainer WeatherScript { get; }

    /// <summary>
    /// Sets the current weather for the battle. If null is passed, this clears the weather.
    /// A duration can be passed to set the duration of the weather in turns. This duration can be modified by
    /// other scripts before the weather is set through the <see cref="IScriptChangeWeatherDuration"/> script hook.
    /// </summary>
    bool SetWeather(StringKey? weatherName, int duration, EventBatchId batchId = default);

    /// <summary>
    /// Volatile scripts are scripts that are not permanent and can be removed by other scripts.
    /// </summary>
    IScriptSet Volatile { get; }

    /// <summary>
    /// Gets the current weather of the battle. If no weather is present, this returns null.
    /// </summary>
    StringKey? WeatherName { get; }

    /// <summary>
    /// Sets the current terrain for the battle. If null is passed, this clears the terrain.
    /// </summary>
    void SetTerrain(StringKey? terrainName, EventBatchId batchId = default);

    /// <summary>
    /// Gets the current terrain of the battle. If no terrain is present, this returns null.
    /// </summary>
    StringKey? TerrainName { get; }

    /// <summary>
    /// Gets the turn choices of the previous turn. This is a list of lists, where each list represents the choices
    /// for a single turn. The outer list is ordered from oldest to newest turn.
    /// </summary>
    IReadOnlyList<IReadOnlyList<ITurnChoice>> PreviousTurnChoices { get; }

    /// <summary>
    /// Attempts to capture a Pokemon. This will use the current RNG to determine whether the capture is successful.
    /// </summary>
    CaptureResult AttempCapture(byte sideIndex, byte position, IItem item);
}

/// <inheritdoc cref="IBattle"/>
public class BattleImpl : ScriptSource, IBattle
{
    /// <inheritdoc cref="BattleImpl"/>
    /// <param name="library">The library the battle uses for data and dynamic handling.</param>
    /// <param name="parties">The parties that will be in the battle.</param>
    /// <param name="canFlee">Whether Pokémon are allowed to flee from the battle.</param>
    /// <param name="numberOfSides">The number of sides in the battle. Generally 2.</param>
    /// <param name="positionsPerSide">The number of spots there are on each side for Pokémon. 1 for singles, 2 for doubles, etc.</param>
    /// <param name="isWildBattle">Whether this battle is a wild battle. In a wild battle, the player can catch the opposing Pokémon,
    /// and moves like roar will end the battle instead of switching out the Pokémon.</param>
    /// <param name="environmentName">The name of the environment the battle is taking place in, such as "grass", "cave", etc.</param>
    /// <param name="randomSeed">The seed for the RNG. If null, this uses a time-dependent seed.</param>
    public BattleImpl(IDynamicLibrary library, IReadOnlyList<IBattleParty> parties, bool canFlee, byte numberOfSides,
        byte positionsPerSide, bool isWildBattle, StringKey environmentName, int? randomSeed = null)
    {
        Library = library;
        Parties = parties;
        CanFlee = canFlee;
        NumberOfSides = numberOfSides;
        PositionsPerSide = positionsPerSide;
        IsWildBattle = isWildBattle;
        EnvironmentName = environmentName;
        Volatile = new ScriptSet(this);
        var sides = new IBattleSide[numberOfSides];
        for (byte i = 0; i < numberOfSides; i++)
            sides[i] = new BattleSideImpl(i, positionsPerSide, this);
        Sides = sides;
        Random = randomSeed.HasValue ? new BattleRandomImpl(randomSeed.Value) : new BattleRandomImpl();
        EventHook = new EventHook();
    }

    /// <inheritdoc />
    public IDynamicLibrary Library { get; }

    /// <inheritdoc />
    public IReadOnlyList<IBattleParty> Parties { get; }

    /// <inheritdoc />
    public bool CanFlee { get; }

    /// <inheritdoc />
    public byte NumberOfSides { get; }

    /// <inheritdoc />
    public byte PositionsPerSide { get; }

    /// <inheritdoc />
    public StringKey EnvironmentName { get; }

    /// <inheritdoc />
    public bool IsWildBattle { get; }

    /// <inheritdoc />
    public IReadOnlyList<IBattleSide> Sides { get; }

    /// <inheritdoc />
    public IBattleRandom Random { get; }

    /// <inheritdoc />
    [MemberNotNull(nameof(Result))]
    public bool HasEnded { get; private set; }

    /// <inheritdoc />
    public BattleResult? Result { get; private set; }

    /// <inheritdoc />
    public EventHook EventHook { get; }

    /// <inheritdoc />
    public uint CurrentTurnNumber { get; private set; }

    /// <inheritdoc />
    public BattleChoiceQueue? ChoiceQueue { get; private set; }

    /// <inheritdoc />
    public IPokemon? GetPokemon(byte side, byte position) => Sides[side].Pokemon[position];

    /// <inheritdoc />
    public bool CanSlotBeFilled(byte side, byte position) => Parties.Any(x =>
        x.IsResponsibleForIndex(new ResponsibleIndex(side, position)) && x.HasUsablePokemonNotInField());

    /// <inheritdoc />
    public void ValidateBattleState()
    {
        if (HasEnded)
            return;
        var survivingSideExists = false;
        IBattleSide? survivingSide = null;
        foreach (var side in Sides)
        {
            if (side.HasFledBattle)
            {
                Result = BattleResult.Inconclusive;
                HasEnded = true;
                return;
            }

            if (!side.IsDefeated())
            {
                // If we already found a surviving side, the battle is not over yet
                if (survivingSideExists)
                    return;
                survivingSideExists = true;
                survivingSide = side;
            }
        }

        // If every side is defeated, the battle is a draw
        if (!survivingSideExists)
        {
            Result = BattleResult.Inconclusive;
            HasEnded = true;
            return;
        }

        // If only one side is left, that side has won
        Result = BattleResult.Conclusive(survivingSide!.Index);
        HasEnded = true;
    }

    /// <inheritdoc />
    public void ForceEndBattle()
    {
        HasEnded = true;
        Result = BattleResult.Inconclusive;
    }

    /// <inheritdoc />
    public bool HasForcedTurn(IPokemon pokemon, [NotNullWhen(true)] out ITurnChoice? choice)
    {
        var battleData = pokemon.BattleData;
        if (battleData == null)
        {
            choice = null;
            return false;
        }

        ITurnChoice? forcedChoice = null;
        pokemon.RunScriptHook<IScriptForceTurnSelection>(script =>
            script.ForceTurnSelection(this, battleData.SideIndex, battleData.Position, ref forcedChoice));
        choice = forcedChoice;
        return choice != null;
    }

    /// <inheritdoc />
    public bool CanUse(ITurnChoice choice)
    {
        if (!choice.User.IsUsable)
            return false;
        // Always allow moves such as Struggle. If we block this, we can run into an infinite loop
        if (Library.MiscLibrary.IsReplacementChoice(choice))
            return true;
        if (HasForcedTurn(choice.User, out var forcedChoice) && !IsValidForForcedTurn(forcedChoice, choice))
            return false;

        if (choice is IMoveChoice moveChoice)
        {
            if (moveChoice.ChosenMove.CurrentPp < 1)
                return false;
            if (!TargetResolver.IsValidTarget(moveChoice.TargetSide, moveChoice.TargetPosition,
                    moveChoice.ChosenMove.MoveData.Target, moveChoice.User))
                return false;
            var preventMove = false;
            choice.RunScriptHook<IScriptPreventMoveSelection>(script =>
                script.PreventMoveSelection(moveChoice, ref preventMove));
            if (preventMove)
                return false;
        }
        else if (choice is ISwitchChoice switchChoice)
        {
            if (!switchChoice.SwitchTo.IsUsable)
                return false;
            // Can't switch to a Pokémon already on the field
            if (switchChoice.SwitchTo.BattleData is { IsOnBattlefield: true })
                return false;
            if (switchChoice.SwitchTo == switchChoice.User)
                return false;
        }
        else if (choice is IItemChoice itemChoice)
        {
            if (!Library.ScriptResolver.TryResolveBattleItemScript(itemChoice.Item, out var itemScript))
                return false;
            if (!itemScript.IsItemUsable)
                return false;
            if (itemScript.TargetType != ItemTargetType.None)
            {
                var target = itemChoice.GetTargetPokemon(this);
                if (target is null || !itemScript.IsTargetValid(target))
                    return false;
                if (!itemScript.TargetType.IsValidTarget(this, choice.User, target))
                    return false;
            }
        }

        return true;

        bool IsValidForForcedTurn(ITurnChoice forcedChoiceInner, ITurnChoice choiceToCheck)
        {
            // If the forced choice is a move choice, we can only use it if the move is the same
            if (forcedChoiceInner is IMoveChoice forcedMove && choiceToCheck is IMoveChoice moveChoiceInner)
            {
                return forcedMove.ChosenMove.MoveData.Name == moveChoiceInner.ChosenMove.MoveData.Name;
            }
            if (forcedChoiceInner is IPassChoice && choiceToCheck is IPassChoice)
            {
                return true; // Both are pass choices, so they are valid
            }
            return forcedChoiceInner.Equals(choiceToCheck);
        }
    }

    /// <inheritdoc />
    public bool TrySetChoice(ITurnChoice choice)
    {
        if (!CanUse(choice))
            return false;
        if (choice.User.BattleData?.IsOnBattlefield != true)
            return false;
        var side = Sides[choice.User.BattleData!.SideIndex];
        side.SetChoice(choice.User.BattleData!.Position, choice);
        CheckChoicesSetAndRun();
        return true;
    }

    private void CheckChoicesSetAndRun()
    {
        foreach (var side in Sides)
        {
            if (!side.AllChoicesSet)
                return;
            if (!side.AllPositionsFilled())
                return;
        }

        var choices = new ITurnChoice[NumberOfSides * PositionsPerSide];
        for (var index = 0; index < Sides.Count; index++)
        {
            var side = Sides[index];
            for (byte i = 0; i < PositionsPerSide; i++)
            {
                var choice = side.SetChoices[i];
                if (choice is null)
                    throw new InvalidOperationException("Choice is null.");
                if (choice is IMoveChoice moveChoice)
                {
                    var priority = moveChoice.ChosenMove.MoveData.Priority;
                    choice.RunScriptHook<IScriptChangePriority>(script =>
                        script.ChangePriority(moveChoice, ref priority));
                    moveChoice.Priority = priority;
                }

                var speed = choice.User.BoostedStats.Speed;
                choice.RunScriptHook<IScriptChangeSpeed>(script => script.ChangeSpeed(choice, ref speed));
                choice.Speed = speed;

                choice.RandomValue = (uint)Random.GetInt();
                choices[index * PositionsPerSide + i] = choice;

                choices[index * PositionsPerSide + i] = choice;
            }

            side.ResetChoices();
        }

        _previousTurnChoices.Add(choices.ToList());

        CurrentTurnNumber += 1;
        ChoiceQueue = new BattleChoiceQueue(choices);

        this.RunTurn();

        ChoiceQueue = null;
        EventHook.Invoke(new EndTurnEvent());
    }

    private readonly ScriptContainer _weatherScript = new();

    /// <summary>
    /// The script that handles the current weather of the battle.
    /// </summary>
    public IReadOnlyScriptContainer WeatherScript => _weatherScript;

    /// <inheritdoc />
    public bool SetWeather(StringKey? weatherName, int duration, EventBatchId batchId = default)
    {
        var preventWeatherChange = false;
        this.RunScriptHook<IScriptPreventWeatherChange>(x =>
            x.PreventWeatherChange(weatherName, ref preventWeatherChange));
        if (preventWeatherChange)
            return false;

        var oldWeatherName = WeatherScript.Script?.Name;
        if (weatherName.HasValue)
        {
            if (weatherName == oldWeatherName)
            {
                // Extend duration of existing weather
                if (_weatherScript.Script is ILimitedTurnsScript existingWeatherScript)
                {
                    this.RunScriptHook<IScriptChangeWeatherDuration>(x =>
                        x.ChangeWeatherDuration(weatherName.Value, ref duration));
                    if (duration < existingWeatherScript.TurnsRemaining)
                        return true;
                    existingWeatherScript.SetTurns(duration);
                }
                return true;
            }
            if (!Library.ScriptResolver.TryResolve(ScriptCategory.Weather, weatherName.Value, null, out var script))
                throw new InvalidOperationException($"Weather script {weatherName} not found.");

            if (script is ILimitedTurnsScript weatherScript)
            {
                this.RunScriptHook<IScriptChangeWeatherDuration>(x =>
                    x.ChangeWeatherDuration(weatherName.Value, ref duration));
                weatherScript.SetTurns(duration);
            }

            _weatherScript.Set(script);
            script.OnAddedToParent(this);
        }
        else
        {
            _weatherScript.Clear();
        }
        EventHook.Invoke(new WeatherChangeEvent(oldWeatherName, weatherName)
        {
            BatchId = batchId,
        });
        Sides.SelectMany(x => x.Pokemon).WhereNotNull()
            .RunScriptHook<IScriptOnWeatherChange>(x => x.OnWeatherChange(this, weatherName, oldWeatherName));
        return true;
    }

    /// <inheritdoc />
    public IScriptSet Volatile { get; }

    /// <inheritdoc />
    public StringKey? WeatherName => WeatherScript.Script?.Name;

    private readonly ScriptContainer _terrainScript = new();

    /// <inheritdoc />
    public void SetTerrain(StringKey? terrainName, EventBatchId batchId = default)
    {
        var oldTerrainName = TerrainName;
        if (terrainName.HasValue)
        {
            if (!Library.ScriptResolver.TryResolve(ScriptCategory.Terrain, terrainName.Value, null, out var script))
                throw new InvalidOperationException($"Terrain script {terrainName} not found.");

            _terrainScript.Set(script);
            script.OnAddedToParent(this);
        }
        else
        {
            _terrainScript.Clear();
        }
        EventHook.Invoke(new TerrainChangeEvent(oldTerrainName, terrainName)
        {
            BatchId = batchId,
        });
    }

    /// <inheritdoc />
    public StringKey? TerrainName => _terrainScript.Script?.Name;

    private readonly List<IReadOnlyList<ITurnChoice>> _previousTurnChoices = new();

    /// <inheritdoc />
    public IReadOnlyList<IReadOnlyList<ITurnChoice>> PreviousTurnChoices => _previousTurnChoices;

    /// <inheritdoc />
    public CaptureResult AttempCapture(byte sideIndex, byte position, IItem item)
    {
        var target = GetPokemon(sideIndex, position);
        if (target is not { IsUsable: true })
            return CaptureResult.Failed;

        var attemptCapture = Library.CaptureLibrary.TryCapture(target, item, Random);
        if (attemptCapture.IsCaught)
        {
            target.MarkAsCaught();
            var side = Sides[target.BattleData!.SideIndex];
            side.ForceClearPokemonFromField(target.BattleData.Position);
        }
        EventHook.Invoke(new CaptureAttemptEvent(target, attemptCapture, item));

        if (attemptCapture.IsCaught)
        {
            if (!CanSlotBeFilled(sideIndex, position))
            {
                Sides[sideIndex].MarkPositionAsUnfillable(position);
            }
            Sides[sideIndex].ForceClearPokemonFromField(position);

            ValidateBattleState();
        }
        return attemptCapture;
    }

    /// <inheritdoc />
    public override int ScriptCount => 3;

    /// <inheritdoc />
    public override void GetOwnScripts(List<IEnumerable<ScriptContainer>> scripts)
    {
        scripts.Add(WeatherScript);
        scripts.Add(_terrainScript);
        scripts.Add(Volatile);
    }

    /// <inheritdoc />
    public override void CollectScripts(List<IEnumerable<ScriptContainer>> scripts) => GetOwnScripts(scripts);

    /// <inheritdoc />
    public void Dispose()
    {
        foreach (var party in Parties)
        {
            foreach (var pokemon in party.Party.WhereNotNull())
            {
                pokemon.ClearBattleData();
            }
        }
        _weatherScript.Clear();
        _terrainScript.Clear();
        Volatile.Clear();
    }
}