using PkmnLib.Dynamic.Events;
using PkmnLib.Dynamic.Libraries;
using PkmnLib.Dynamic.Models.BattleFlow;
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
{
    /// <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>
    /// 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>
    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>
    /// 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>
    /// Sets the current weather for the battle. If null is passed, this clears the weather.
    /// </summary>
    void SetWeather(StringKey? weatherName);

    /// <summary>
    /// Gets the current weather of the battle. If no weather is present, this returns null.
    /// </summary>
    StringKey? WeatherName { 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; }
    
    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="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, int? randomSeed = null)
    {
        Library = library;
        Parties = parties;
        CanFlee = canFlee;
        NumberOfSides = numberOfSides;
        PositionsPerSide = positionsPerSide;
        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 IReadOnlyList<IBattleSide> Sides { get; }

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

    /// <inheritdoc />
    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.HasPokemonNotInField());

    /// <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 bool CanUse(ITurnChoice choice)
    {
        if (!choice.User.IsUsable)
            return false;
        if (choice is IMoveChoice moveChoice)
        {
            // TODO: Hook to change number of PP needed.
            if (moveChoice.ChosenMove.CurrentPp < 1)
                return false;
            if (!TargetResolver.IsValidTarget(moveChoice.TargetSide, moveChoice.TargetPosition,
                    moveChoice.ChosenMove.MoveData.Target, moveChoice.User))
                return false;
        }

        return true;
    }

    /// <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(script => script.ChangePriority(moveChoice, ref priority));
                    moveChoice.Priority = priority;
                }

                var speed = choice.User.BoostedStats.Speed;
                choice.RunScriptHook(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();

    /// <inheritdoc />
    public void SetWeather(StringKey? weatherName)
    {
        if (weatherName.HasValue)
        {
            if (!Library.ScriptResolver.TryResolve(ScriptCategory.Weather, weatherName.Value, null, out var script))
                throw new InvalidOperationException($"Weather script {weatherName} not found.");
            _weatherScript.Set(script);
        }
        else
        {
            _weatherScript.Clear();
        }
        // TODO: Trigger weather change script hooks
    }

    private IScriptSet Volatile { get; } = new ScriptSet();

    /// <inheritdoc />
    public StringKey? WeatherName => _weatherScript.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));
        return attemptCapture;
    }

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

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

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