using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.ComTypes;
using System.Runtime.Serialization;
using ELEMDESC = System.Runtime.InteropServices.ComTypes.ELEMDESC;
using FUNCDESC = System.Runtime.InteropServices.ComTypes.FUNCDESC;
using INVOKEKIND = System.Runtime.InteropServices.ComTypes.INVOKEKIND;
using FUNCFLAGS = System.Runtime.InteropServices.ComTypes.FUNCFLAGS;
using Rubberduck.InternalApi.Model;
using Rubberduck.InternalApi.Extensions;
using Rubberduck.InternalApi.Model.Declarations.Symbols;

namespace Rubberduck.Parsing.Model.ComReflection;

internal enum DispId
{
    Collect = -8,           //The method you are calling through Invoke is an accessor function. 
    Destructor = -7,        //The C++ destructor function for the object. 
    Construtor = -6,        //The C++ constructor function for the object. 
    Evaluate = -5,          //This method is implicitly invoked when the ActiveX client encloses the arguments in square brackets.
    NewEnum = -4,           //It returns an enumerator object that supports IEnumVARIANT.
    PropertyPut = -3,       //The parameter that receives the value of an assignment in a PROPERTYPUT. 
    Unknown = -1,           //The value returned by IDispatch::GetIDsOfNames to indicate that a member or parameter name was not found.
    Value = 0               //The default member for the object.
}

[DataContract]
[KnownType(typeof(ComBase))]
[DebuggerDisplay("{" + nameof(MemberDeclaration) + "}")]
public class ComMember : ComBase
{
    [DataMember(IsRequired = true)]
    public bool IsHidden { get; private set; }

    [DataMember(IsRequired = true)]
    public bool IsRestricted { get; private set; }

    [DataMember(IsRequired = true)]
    public bool ReturnsWithEventsObject { get; private set; }

    [DataMember(IsRequired = true)]
    public bool IsDefault { get; private set; }

    [DataMember(IsRequired = true)]
    public bool IsEnumerator { get; private set; }

    //This member is called on an interface when a bracketed expression is dereferenced.
    [DataMember(IsRequired = true)]
    public bool IsEvaluateFunction { get; private set; }

    [DataMember(IsRequired = true)]
    public ComParameter AsTypeName { get; private set; } = ComParameter.Void;

    [DataMember(IsRequired = true)]
    private List<ComParameter> _parameters = new();

    //See https://docs.microsoft.com/en-us/windows/desktop/midl/retval
    //"Parameters with the [retval] attribute are not displayed in user-oriented browsers."
    public IEnumerable<ComParameter> Parameters => _parameters.Where(param => !param.IsReturnValue);

    public ComMember(IComBase parent, ITypeInfo info, FUNCDESC funcDesc) : base(parent, info, funcDesc)
    {
        LoadParameters(funcDesc, info);
        var flags = (FUNCFLAGS)funcDesc.wFuncFlags;
        IsHidden = flags.HasFlag(FUNCFLAGS.FUNCFLAG_FHIDDEN);
        IsRestricted = flags.HasFlag(FUNCFLAGS.FUNCFLAG_FRESTRICTED);
        ReturnsWithEventsObject = flags.HasFlag(FUNCFLAGS.FUNCFLAG_FSOURCE);
        IsDefault = Index == (int)DispId.Value;
        IsEnumerator = Index == (int)DispId.NewEnum;
        IsEvaluateFunction = Index == (int)DispId.Evaluate;
        SetDeclarationType(funcDesc, info);
    }

    private void SetDeclarationType(FUNCDESC funcDesc, ITypeInfo info)
    {
        var returnsHResult = (VarEnum)funcDesc.elemdescFunc.tdesc.vt == VarEnum.VT_HRESULT;
        var returnsVoid = (VarEnum)funcDesc.elemdescFunc.tdesc.vt == VarEnum.VT_VOID;

        if (funcDesc.invkind.HasFlag(INVOKEKIND.INVOKE_PROPERTYGET))
        {
            Type = DeclarationType.PropertyGet;
        }
        else if (funcDesc.invkind.HasFlag(INVOKEKIND.INVOKE_PROPERTYPUT))
        {
            Type = DeclarationType.PropertyLet;
        }
        else if (funcDesc.invkind.HasFlag(INVOKEKIND.INVOKE_PROPERTYPUTREF))
        {
            Type = DeclarationType.PropertySet;
        }
        else if (returnsVoid || !_parameters.Any(param => param.IsReturnValue) && returnsHResult)
        {
            Type = DeclarationType.Procedure;
        }
        else
        {
            Type = DeclarationType.Function;
        }

        if (Type == DeclarationType.Function || Type == DeclarationType.PropertyGet)
        {
            var returnType = new ComParameter(this, funcDesc.elemdescFunc, info, string.Empty);
            if (!_parameters.Any())
            {
                AsTypeName = returnType;
            }
            else
            {
                var retval = _parameters.FirstOrDefault(x => x.IsReturnValue);
                AsTypeName = retval ?? returnType;
            }
        }
    }

    private void LoadParameters(FUNCDESC funcDesc, ITypeInfo info)
    {
        var names = new string[255];
        info.GetNames(Index, names, names.Length, out _);

        for (var index = 0; index < funcDesc.cParams; index++)
        {
            var paramPtr = new IntPtr(funcDesc.lprgelemdescParam.ToInt64() + Marshal.SizeOf(typeof(ELEMDESC)) * index);
            var elemDesc = Marshal.PtrToStructure<ELEMDESC>(paramPtr);
            var param = new ComParameter(this, elemDesc, info, names[index + 1] ?? $"{index}unnamedParameter");
            _parameters.Add(param);
        }

        // See https://docs.microsoft.com/en-us/windows/desktop/midl/propput
        // "A function that has the [propput] attribute must also have, as its last parameter, a parameter that has the [in] attribute."
        if (funcDesc.invkind.HasFlag(INVOKEKIND.INVOKE_PROPERTYPUTREF) ||
            funcDesc.invkind.HasFlag(INVOKEKIND.INVOKE_PROPERTYPUT))
        {
            AsTypeName = _parameters.Last();
            _parameters = _parameters.Take(funcDesc.cParams - 1).ToList();
            return;
        }

        if (Parameters.Any() && funcDesc.cParamsOpt == -1)
        {
            Parameters.Last().IsParamArray = true;
        }
    }

    internal Symbol ToSymbol(WorkspaceFileUri parentUri)
    {
        var children = Parameters.Select(e => e.ToSymbol(parentUri.GetChildSymbolUri(Name)));
        Symbol symbol;
        switch (Type)
        {
            case DeclarationType.Function:
                symbol = new FunctionSymbol(Name, parentUri, InternalApi.Model.Accessibility.Public, children.OfType<ParameterSymbol>(), AsTypeName?.TypeName)
                {
                    IsUserDefined = false,
                    Detail = Documentation?.DocString ?? string.Empty,
                };
                break;
            case DeclarationType.Procedure:
                symbol = new ProcedureSymbol(Name, parentUri, InternalApi.Model.Accessibility.Public, children.OfType<ParameterSymbol>())
                {
                    IsUserDefined = false,
                    Detail = Documentation?.DocString ?? string.Empty,
                };
                break;
            case DeclarationType.PropertyGet:
                symbol = new PropertyGetSymbol(Name, parentUri, InternalApi.Model.Accessibility.Public, children.OfType<ParameterSymbol>(), AsTypeName?.TypeName)
                {
                    IsUserDefined = false,
                    Detail = Documentation?.DocString ?? string.Empty,
                };
                break;
            case DeclarationType.PropertyLet:
                symbol = new PropertyLetSymbol(Name, parentUri, InternalApi.Model.Accessibility.Public, children.OfType<ParameterSymbol>())
                {
                    IsUserDefined = false,
                    Detail = Documentation?.DocString ?? string.Empty,
                };
                break;
            case DeclarationType.PropertySet:
                symbol = new PropertySetSymbol(Name, parentUri, InternalApi.Model.Accessibility.Public, children.OfType<ParameterSymbol>())
                {
                    IsUserDefined = false,
                    Detail = Documentation?.DocString ?? string.Empty,
                };
                break;
            case DeclarationType.Event:
                symbol = new EventSymbol(Name, parentUri, InternalApi.Model.Accessibility.Public, children.OfType<ParameterSymbol>())
                {
                    IsUserDefined = false,
                    Detail = Documentation?.DocString ?? string.Empty,
                };
                break;
            default:
                throw new NotSupportedException();
        }
        return symbol;
    }

    private string MemberDeclaration
    {
        get
        {
            var type = string.Empty;
            switch (Type)
            {
                case DeclarationType.Function:
                    type = "Function";
                    break;
                case DeclarationType.Procedure:
                    type = "Sub";
                    break;
                case DeclarationType.PropertyGet:
                    type = "Property Get";
                    break;
                case DeclarationType.PropertyLet:
                    type = "Property Let";
                    break;
                case DeclarationType.PropertySet:
                    type = "Property Set";
                    break;
                case DeclarationType.Event:
                    type = "Event";
                    break;
            }
            return $"{(IsHidden || IsRestricted ? "Private" : "Public")} {type} {Name}{(AsTypeName == null ? string.Empty : $" As {AsTypeName.TypeName}")}";
        }
    }
}
