#region

using System.Collections.ObjectModel;
using System.Diagnostics;
using JetBrains.Annotations;

#endregion

namespace Prexonite.Types;

/// <summary>
///     Implements the interfaces <see cref = "IObject" /> and <see cref = "IIndirectCall" /> in
///     such a way that the object acts like a Prexonite structure (e.g., members can be added at runtime).
/// </summary>
[DebuggerNonUserCode]
public class ExtendableObject : IObject, IIndirectCall
{
    ExtensionTable? _et;

    protected void InitializeExtensionTable()
    {
        if (_et != null)
            throw new InvalidOperationException(
                "The extension table for this object has already been created."
            );
        _et = [];
    }

    /// <summary>
    ///     Creates a new instance of ExtendableObject.
    /// </summary>
    /// <param name = "tableIsInitialized">Indicates whether the initialization of the
    ///     extension table (<see cref = "InitializeExtensionTable" />) should be performed by this
    ///     constructor overload.</param>
    protected ExtendableObject(bool tableIsInitialized)
    {
        if (!tableIsInitialized)
            InitializeExtensionTable();
    }

    protected ExtendableObject()
        : this(false) { }

    #region IObject Members

    /// <summary>
    ///     Tries to call an instance member of the object (CLR).
    /// </summary>
    /// <param name = "sctx">The context in which to perform the call.</param>
    /// <param name = "subject">The subject to substitute for this</param>
    /// <param name = "args">The arguments for the call.</param>
    /// <param name = "call">The type of call.</param>
    /// <param name = "id">The id of the member. The empty string represents the default member.</param>
    /// <param name = "result">The result of the call.</param>
    /// <returns>True if the call was successful, false otherwise.</returns>
    /// <remarks>
    ///     <para>
    ///         <paramref name = "result" /> is only defined if the method returns true.
    ///     </para>
    ///     <para>
    ///         If you want to intercept object member calls yourself, overwrite <see
    ///      cref = "TryDynamicCall(Prexonite.StackContext,System.ReadOnlySpan{Prexonite.PValue},Prexonite.Types.PCall,string,out Prexonite.PValue?)" /> instead.
    ///     </para>
    /// </remarks>
    [PublicAPI]
    protected virtual bool TryDynamicClrCall(
        StackContext sctx,
        PValue subject,
        ReadOnlySpan<PValue> args,
        PCall call,
        string id,
        [NotNullWhen(true)] out PValue? result
    )
    {
        var objT = subject.Type as ObjectPType;
        if ((object?)objT != null)
            return objT.TryDynamicCall(sctx, subject, args, call, id, out result, out _, true);
        else
            return subject.TryDynamicCall(sctx, args, call, id, out result);
    }

    /// <summary>
    ///     Tries to call an instance member of the object (CLR). Overwrite this method to intercept object member calls.
    /// </summary>
    /// <param name = "sctx">The context in which to perform the call.</param>
    /// <param name = "args">The arguments for the call.</param>
    /// <param name = "call">The type of call.</param>
    /// <param name = "id">The id of the member. The empty string represents the default member.</param>
    /// <param name = "result">The result of the call.</param>
    /// <returns>True if the call was successful, false otherwise.</returns>
    /// <remarks>
    ///     <para>
    ///         <paramref name = "result" /> is only defined if the method returns true.
    ///     </para>
    ///     <para>
    ///         If you want to intercept object member calls yourself, overwrite <see
    ///      cref = "TryDynamicClrCall(Prexonite.StackContext,Prexonite.PValue,System.ReadOnlySpan{Prexonite.PValue},Prexonite.Types.PCall,string,out Prexonite.PValue?)" /> instead.
    ///     </para>
    /// </remarks>
    [PublicAPI]
    protected bool TryDynamicClrCall(
        StackContext sctx,
        PValue[] args,
        PCall call,
        string id,
        [NotNullWhen(true)] out PValue? result
    )
    {
        return TryDynamicClrCall(sctx, sctx.CreateNativePValue(this), args, call, id, out result);
    }

    /// <summary>
    ///     Tries to call instance members of the object or members of the extended part.
    /// </summary>
    /// <param name = "sctx">The context in which to perform the call.</param>
    /// <param name = "args">The arguments for the call.</param>
    /// <param name = "call">The type of call.</param>
    /// <param name = "id">The id of the member. The empty string represents the default member.</param>
    /// <param name = "result">The result of the call.</param>
    /// <returns>True if the call was successful, false otherwise.</returns>
    /// <remarks>
    ///     <para>
    ///         <paramref name = "result" /> is only defined if the method returns true.
    ///     </para>
    ///     <para>
    ///         If you want to intercept object member calls yourself, overwrite <see
    ///      cref = "TryDynamicClrCall(Prexonite.StackContext,Prexonite.PValue,System.ReadOnlySpan{Prexonite.PValue},Prexonite.Types.PCall,string,out Prexonite.PValue?)" /> instead.
    ///     </para>
    /// </remarks>
    [PublicAPI]
    public bool TryDynamicCall(
        StackContext sctx,
        ReadOnlySpan<PValue> args,
        PCall call,
        string id,
        [NotNullWhen(true)] out PValue? result
    )
    {
        return TryDynamicCall(sctx, sctx.CreateNativePValue(this), args, call, id, out result);
    }

    /// <summary>
    ///     Tries to call instance members of the object or members of the extended part.
    /// </summary>
    /// <param name = "sctx">The context in which to perform the call.</param>
    /// <param name = "subject">The subject to substitute for <value>this</value>.</param>
    /// <param name = "args">The arguments for the call.</param>
    /// <param name = "call">The type of call.</param>
    /// <param name = "id">The id of the member. The empty string represents the default member.</param>
    /// <param name = "result">The result of the call.</param>
    /// <returns>True if the call was successful, false otherwise.</returns>
    /// <remarks>
    ///     <para>
    ///         <paramref name = "result" /> is only defined if the method returns true.
    ///     </para>
    ///     <para>
    ///         If you want to intercept object member calls yourself, overwrite <see
    ///      cref = "TryDynamicClrCall(Prexonite.StackContext,Prexonite.PValue,System.ReadOnlySpan{Prexonite.PValue},Prexonite.Types.PCall,string,out Prexonite.PValue?)" /> instead.
    ///     </para>
    /// </remarks>
    [PublicAPI]
    public bool TryDynamicCall(
        StackContext sctx,
        PValue subject,
        ReadOnlySpan<PValue> args,
        PCall? call,
        string? id,
        out PValue? result
    )
    {
        if (sctx == null)
            throw new ArgumentNullException(nameof(sctx));
        id ??= "";
        call ??= PCall.Get;

        _et ??= [];

        if (
            TryDynamicClrCall(sctx, subject, args, call.Value, id, out result)
            ||
            //Try conventional call
            _tryDynamicExtensionCall(sctx, subject, args, call.Value, id, out result)
        )
            //Try extension call
            return true;
        else if (call == PCall.Set && args.Length > 0) //Add field if it does not exist
            result = _dynamicCall(sctx, [id, args[0]], PCall.Set, StructurePType.SetId);
        else
            result = null; //Make sure result is really null.

        return result != null;
    }

    [Obsolete(
        "Use TryDynamicCall(StackContext, PValue, ReadOnlySpan<PValue>, PCall?, string?, out PValue?) instead."
    )]
    public bool TryDynamicCall(
        StackContext sctx,
        PValue subject,
        PValue[] args,
        PCall? call,
        string? id,
        out PValue? result
    ) => TryDynamicCall(sctx, subject, args.AsSpan(), call, id, out result);

    #endregion

    [MemberNotNull(nameof(_et))]
    protected void EnsureExtensionTableInitialized()
    {
        if (_et == null)
        {
            throw new PrexoniteException(
                $"Extension table of expando object is not initialized. The constructor of the derived class must call {nameof(InitializeExtensionTable)}."
            );
        }
    }

    [PublicAPI]
    protected void AddRefMember(string id, PValue value)
    {
        if (id == null)
            throw new ArgumentNullException(nameof(id));
        if (value == null)
            throw new ArgumentNullException(nameof(value));
        EnsureExtensionTableInitialized();
        _et.Add(new(id, true, value));
    }

    [PublicAPI]
    protected void AddMember(string id, PValue value)
    {
        if (id == null)
            throw new ArgumentNullException(nameof(id));
        if (value == null)
            throw new ArgumentNullException(nameof(value));
        EnsureExtensionTableInitialized();
        _et.Add(new(id, value));
    }

    [PublicAPI]
    protected void RemoveMember(string id)
    {
        EnsureExtensionTableInitialized();
        _et.Remove(id);
    }

    bool _tryDynamicExtensionCall(
        StackContext sctx,
        PValue subject,
        ReadOnlySpan<PValue> args,
        PCall call,
        string id,
        [NotNullWhen(true)] out PValue? result
    )
    {
        EnsureExtensionTableInitialized();
        result = null;

        var argst = StructurePType._AddThis(subject, args);

        var reference = false;

        //Try to call the member
        if (_et.TryGetValue(id, out var m))
            result = m.DynamicCall(sctx, argst, call);
        else
            switch (id.ToLowerInvariant())
            {
                case StructurePType.SetRefId:
                    reference = true;
                    goto case StructurePType.SetId;
                case StructurePType.SetId:
                case StructurePType.SetIdAlternative:
                    if (args.Length < 2)
                        goto default;

                    var mid = (string)args[0].ConvertTo(sctx, PType.String).Value!;

                    if (reference || args.Length > 2)
                        reference = (bool)args[1].ConvertTo(sctx, PType.Bool).Value!;

                    if (_et.Contains(mid))
                        m = _et[mid];
                    else
                    {
                        m = new(mid);
                        _et.Add(m);
                    }

                    m.Value = args[^1];
                    m.Indirect = reference;

                    result = m.Value;

                    break;
                default:
                    //Try to call the generic "call" member
                    if (_et.TryGetValue(StructurePType.CallId, out m))
                        result = m.DynamicCall(sctx, StructurePType._AddThis(subject, argst), call);
                    else
                        return false;
                    break;
            }

        return (PValue?)result != null;
    }

    PValue _dynamicCall(StackContext sctx, PValue[] args, PCall call, string id)
    {
        return _dynamicCall(sctx, sctx.CreateNativePValue(this), args, call, id);
    }

    PValue _dynamicCall(StackContext sctx, PValue subject, PValue[] args, PCall call, string id)
    {
        if (!_tryDynamicExtensionCall(sctx, subject, args, call, id, out var result))
            throw new InvalidCallException(
                "Cannot call " + id + " on extension of type " + GetType().Name
            );
        return (PValue?)result ?? PType.Null;
    }

    #region IIndirectCall Members

    /// <summary>
    ///     Indirectly calls the extended part of the object.
    /// </summary>
    /// <param name = "sctx">The stack context in which to perform the call.</param>
    /// <param name = "args">The arguments to pass to the handling function.</param>
    /// <returns>The value returned by the extended part of the object.</returns>
    public virtual PValue IndirectCall(StackContext sctx, params ReadOnlySpan<PValue> args)
    {
        var subject = sctx.CreateNativePValue(this);
        if (sctx == null)
            throw new ArgumentNullException(nameof(sctx));

        _et ??= [];

        if (!_et.TryGetValue(StructurePType.IndirectCallId, out var m))
            throw new PrexoniteException(this + " does not support indirect calls.");
        return m.DynamicCall(sctx, StructurePType._AddThis(subject, args), PCall.Get);
    }

    #endregion
}

[DebuggerNonUserCode]
class ExtensionMember
{
    internal ExtensionMember(string id)
        : this(id, false, PType.Null) { }

    internal ExtensionMember(string id, bool indirect)
        : this(id, indirect, PType.Null) { }

    internal ExtensionMember(string id, PValue value)
        : this(id, false, value) { }

    internal ExtensionMember(string id, bool indirect, PValue value)
    {
        Id = id ?? throw new ArgumentNullException(nameof(id));
        Indirect = indirect;
        Value = value;
    }

    public bool Indirect { get; set; }

    public string Id { get; set; }

    public PValue Value { get; set; }

    public bool TryDynamicCall(StackContext sctx, PValue[] args, PCall call, out PValue? result)
    {
        result = null;
        if (Indirect)
        {
            result = Value.IndirectCall(sctx, args);
        }
        else // direct
        {
            if (args == null)
                throw new ArgumentNullException(nameof(args));
            if (sctx == null)
                throw new ArgumentNullException(nameof(sctx));

            result = Value;

            if (call == PCall.Set && args.Length > 0)
                Value = args[^1];
        }

        return true;
    }

    public PValue DynamicCall(StackContext sctx, PValue[] args, PCall call)
    {
        if (!TryDynamicCall(sctx, args, call, out var result))
            throw new InvalidCallException(
                "Cannot call extension member " + Id + " with " + args.Length + " arguments."
            );
        return result ?? PType.Null;
    }
}

[DebuggerNonUserCode]
class ExtensionTable : KeyedCollection<string, ExtensionMember>
{
    internal ExtensionTable()
        : base(StringComparer.CurrentCultureIgnoreCase) { }

    ///<summary>
    ///    When implemented in a derived class, extracts the key from the specified element.
    ///</summary>
    ///<returns>
    ///    The key for the specified element.
    ///</returns>
    ///<param name = "item">The element from which to extract the key.</param>
    protected override string GetKeyForItem(ExtensionMember item)
    {
        if (item == null)
            throw new ArgumentNullException(nameof(item));

        return item.Id;
    }
}
