using System.Collections;
using System.Diagnostics.CodeAnalysis;
using PkmnLib.Dynamic.ScriptHandling.Registry;
using PkmnLib.Static.Utils;

namespace PkmnLib.Dynamic.ScriptHandling;

/// <summary>
/// A script set is a collection of scripts that can be accessed by a key.
/// We can add, remove, and clear scripts from the set.
/// This is generally used for volatile scripts.
/// </summary>
public interface IScriptSet : IEnumerable<ScriptContainer>, IDeepCloneable
{
    /// <summary>
    /// Adds a script to the set. If the script with that name already exists in this set, this
    /// makes that script stack instead. The return value here is that script.
    /// If the script was blocked from being added, this will return null.
    /// </summary>
    /// <param name="script">The script to add.</param>
    /// <param name="forceAdd">If true, the script cannot be blocked, and will always be added</param>
    ScriptContainer? Add(Script script, bool forceAdd = false);

    /// <summary>
    /// Adds a script with a name to the set. If the script with that name already exists in this
    /// set, this makes that script stack instead. The return value here is that script.
    /// </summary>
    ScriptContainer? StackOrAdd(StringKey scriptKey, Func<Script?> instantiation);

    /// <summary>
    /// Gets a script from the set using its unique name.
    /// </summary>
    ScriptContainer? Get(StringKey scriptKey);

    /// <summary>
    /// Tries to get a script from the set using its type.
    /// </summary>
    bool TryGet<T>([NotNullWhen(true)] out T? script) where T : Script;

    /// <summary>
    /// Gets a script from the set using its type.
    /// </summary>
    T? Get<T>() where T : Script;

    /// <summary>
    /// Removes a script from the set using its unique name.
    /// </summary>
    void Remove(StringKey scriptKey);

    /// <summary>
    /// Removes a script from the set using its type.
    /// </summary>
    void Remove<T>() where T : Script => Remove(ScriptUtils.ResolveName<T>());

    /// <summary>
    /// Clears all scripts from the set.
    /// </summary>
    void Clear();

    /// <summary>
    /// Checks if the set has a script with the given type.
    /// </summary>
    bool Contains<T>() where T : Script => Contains(ScriptUtils.ResolveName<T>());

    /// <summary>
    /// Checks if the set has a script with the given name.
    /// </summary>
    bool Contains(StringKey scriptKey);

    /// <summary>
    /// Gets a script from the set at a specific index.
    /// </summary>
    ScriptContainer At(int index);

    /// <summary>
    /// Gets the number of scripts in the set.
    /// </summary>
    int Count { get; }

    /// <summary>
    /// Gets the names of all scripts in the set.
    /// </summary>
    IEnumerable<StringKey> GetScriptNames();
}

/// <inheritdoc cref="IScriptSet"/>
public class ScriptSet : IScriptSet
{
    private readonly IScriptSource _source;

    private readonly List<ScriptContainer> _scripts = [];

    /// <inheritdoc cref="IScriptSet"/>
    public ScriptSet(IScriptSource source)
    {
        _source = source;
    }

    /// <inheritdoc />
    public IEnumerator<ScriptContainer> GetEnumerator() => _scripts.GetEnumerator();

    /// <inheritdoc />
    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();

    /// <inheritdoc />
    public ScriptContainer? Add(Script script, bool forceAdd = false)
    {
        if (!forceAdd)
        {
            var preventVolatileAdd = false;
            _source.RunScriptHook<IScriptPreventVolatileAdd>(x =>
                x.PreventVolatileAdd(_source, script, ref preventVolatileAdd));
            if (preventVolatileAdd)
                return null;
        }

        var existing = _scripts.FirstOrDefault(s => s.Script?.Name == script.Name);
        if (existing != null)
        {
            if (existing.Script is IScriptStack stackable)
                stackable.Stack();
            return existing;
        }

        script.OnRemoveEvent += s => Remove(s.Name);
        var container = new ScriptContainer(script);
        _scripts.Add(container);
        script.OnAddedToParent(_source);
        return container;
    }

    /// <inheritdoc />
    public ScriptContainer? StackOrAdd(StringKey scriptKey, Func<Script?> instantiation)
    {
        var existing = _scripts.FirstOrDefault(s => s.Script?.Name == scriptKey);
        if (existing != null)
        {
            if (existing.Script is IScriptStack stackable)
                stackable.Stack();
            return existing;
        }

        var script = instantiation();
        if (script is null)
            return null;
        script.OnRemoveEvent += s => Remove(s.Name);
        var container = new ScriptContainer(script);
        _scripts.Add(container);
        script.OnAddedToParent(_source);
        return container;
    }

    /// <inheritdoc />
    public ScriptContainer? Get(StringKey scriptKey) => _scripts.FirstOrDefault(s => s.Script?.Name == scriptKey);

    /// <inheritdoc />
    public bool TryGet<T>([NotNullWhen(true)] out T? script) where T : Script
    {
        var scriptName = ScriptUtils.ResolveName<T>();
        var container = _scripts.FirstOrDefault(sc => sc.Script?.Name == scriptName);
        if (container?.Script is not T s)
        {
            script = null;
            return false;
        }
        script = s;
        return true;
    }

    /// <inheritdoc />
    public T? Get<T>() where T : Script => Get(ScriptUtils.ResolveName<T>())?.Script as T;

    /// <inheritdoc />
    public void Remove(StringKey scriptKey)
    {
        var script = _scripts.FirstOrDefault(s => s.Script?.Name == scriptKey);
        if (script is null)
            return;
        script.Script?.OnRemove();
        script.Clear();
    }

    /// <inheritdoc />
    public void Clear()
    {
        foreach (var script in _scripts)
        {
            if (!script.IsEmpty)
            {
                script.Script.OnRemove();
            }
        }
        _scripts.Clear();
    }

    /// <inheritdoc />
    public bool Contains(StringKey scriptKey) => _scripts.Any(s => s.Script?.Name == scriptKey);

    /// <inheritdoc />
    public ScriptContainer At(int index) => _scripts[index];

    /// <inheritdoc />
    public int Count => _scripts.Count;

    /// <inheritdoc />
    public IEnumerable<StringKey> GetScriptNames() =>
        _scripts.Select(x => x.Script).WhereNotNull().Select(s => s.Name);
}