using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.ComTypes;
using System.Runtime.Serialization;
using Rubberduck.InternalApi.Extensions;
using Rubberduck.InternalApi.Model;
using Rubberduck.InternalApi.Model.Declarations.Symbols;
using Rubberduck.VBEditor.Utility;
using ELEMDESC = System.Runtime.InteropServices.ComTypes.ELEMDESC;
using PARAMFLAG = System.Runtime.InteropServices.ComTypes.PARAMFLAG;
using TYPEATTR = System.Runtime.InteropServices.ComTypes.TYPEATTR;
using TYPEDESC = System.Runtime.InteropServices.ComTypes.TYPEDESC;
using TYPEKIND = System.Runtime.InteropServices.ComTypes.TYPEKIND;

namespace Rubberduck.Parsing.Model.ComReflection;

[DataContract]
[KnownType(typeof(ComTypeName))]
[DebuggerDisplay("{" + nameof(DeclarationName) + "}")]
public class ComParameter
{
    public static ComParameter Void = new() { _typeName = ComTypeName.Void };

    [DataMember(IsRequired = true)]
    public string Name { get; private set; }

    public string DeclarationName => $"{(IsOptional ? "Optional " : string.Empty)}{(IsByRef ? "ByRef" : "ByVal")} {Name} As {TypeName}{(IsOptional && DefaultValue != null ? " = " : string.Empty)}{(IsOptional && DefaultValue != null ? _typeName.IsEnumMember ? DefaultAsEnum : DefaultValue : string.Empty)}";

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

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

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

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

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

    [DataMember(IsRequired = true)]
    public object DefaultValue { get; private set; }
    public bool HasDefaultValue => DefaultValue != null;

    public string DefaultAsEnum
    {
        get
        {
            if (!_typeName.IsEnumMember || !HasDefaultValue || !ComProject.KnownEnumerations.TryGetValue(_typeName.EnumGuid, out ComEnumeration? enumType))
            {
                return string.Empty;
            }
            var member = enumType.Members.FirstOrDefault(m => m.Value == (int)DefaultValue);
            return member != null ? member.Name : string.Empty;
        }
    }

    [DataMember(IsRequired = true)]
    private ComTypeName _typeName;
    public string TypeName => _typeName.Name;

    [DataMember(IsRequired = true)]
    ComMember Parent { get; set; }
    public ComProject? Project => Parent?.Project;

    private ComParameter() { }

    public ComParameter(ComMember parent, ELEMDESC elemDesc, ITypeInfo info, string name)
    {
        Debug.Assert(name != null, "Parameter name is null");

        Parent = parent;
        Name = name;
        var paramDesc = elemDesc.desc.paramdesc;
        GetParameterType(elemDesc.tdesc, info);
        IsOptional = paramDesc.wParamFlags.HasFlag(PARAMFLAG.PARAMFLAG_FOPT);
        IsReturnValue = paramDesc.wParamFlags.HasFlag(PARAMFLAG.PARAMFLAG_FRETVAL);
        if (!paramDesc.wParamFlags.HasFlag(PARAMFLAG.PARAMFLAG_FHASDEFAULT) || string.IsNullOrEmpty(name))
        {
            return;
        }

        //lpVarValue points to a PARAMDESCEX structure, but we don't care about the cBytes here at all. 
        //Offset and dereference the VARIANTARG directly.
        var defValue = new ComVariant(paramDesc.lpVarValue + Marshal.SizeOf(typeof(ulong)));
        DefaultValue = defValue.Value;
    }

    //This overload should only be used for retrieving the TypeName from a random TYPEATTR. TODO: This really belongs somewhere else.
    public ComParameter(TYPEATTR attributes, ITypeInfo info)
    {
        GetParameterType(attributes.tdescAlias, info);
    }

    private void GetParameterType(TYPEDESC desc, ITypeInfo info)
    {
        var vt = (VarEnum)desc.vt;
        TYPEDESC tdesc;

        if (vt == VarEnum.VT_PTR)
        {
            tdesc = Marshal.PtrToStructure<TYPEDESC>(desc.lpValue);
            GetParameterType(tdesc, info);
            IsByRef = true;
        }
        else if (vt == VarEnum.VT_USERDEFINED)
        {
            int href;
            unchecked
            {
                href = (int)(desc.lpValue.ToInt64() & 0xFFFFFFFF);
            }

            try
            {
                info.GetRefTypeInfo(href, out ITypeInfo refTypeInfo);
                refTypeInfo.GetTypeAttr(out IntPtr attribPtr);
                using (DisposalActionContainer.Create(attribPtr, refTypeInfo.ReleaseTypeAttr))
                {
                    var attribs = Marshal.PtrToStructure<TYPEATTR>(attribPtr);
                    var type = new ComDocumentation(refTypeInfo, ComDocumentation.LibraryIndex).Name;
                    if (attribs.typekind == TYPEKIND.TKIND_ENUM)
                    {
                        _typeName = new ComTypeName(Project, type, attribs.guid, Guid.Empty);
                    }
                    else if (attribs.typekind == TYPEKIND.TKIND_ALIAS)
                    {
                        _typeName = new ComTypeName(Project, type, Guid.Empty, attribs.guid);
                    }
                    else
                    {
                        _typeName = new ComTypeName(Project, type);
                    }
                }
            }
            catch (COMException)
            {
                _typeName = new ComTypeName(Project, Tokens.Object);
            }
        }
        else if (vt == VarEnum.VT_SAFEARRAY || vt == VarEnum.VT_CARRAY || vt.HasFlag(VarEnum.VT_ARRAY))
        {
            tdesc = Marshal.PtrToStructure<TYPEDESC>(desc.lpValue);
            GetParameterType(tdesc, info);
            IsArray = true;
        }
        else if (vt == VarEnum.VT_HRESULT)
        {
            _typeName = new ComTypeName(Project, Tokens.Long);
        }
        else
        {
            _typeName = new ComTypeName(Project, ComVariant.TypeNames.TryGetValue(vt, out string? result) ? result : Tokens.Object);
        }
    }

    internal Symbol ToSymbol(WorkspaceUri parentUri)
    {
        ParameterSymbol symbol;
        var modifier = IsByRef 
            ? ParameterModifier.ExplicitByRef 
            : ParameterModifier.ExplicitByVal;

        if (IsParamArray)
        {
            symbol = new ParamArrayParameterSymbol(Name, parentUri, TypeName);
        }
        else if (IsOptional)
        {
            symbol = new OptionalParameterSymbol(Name, parentUri, modifier, TypeName, null);
        }
        else
        {
            symbol = new ParameterSymbol(Name, parentUri, modifier, TypeName);
        }

        return symbol;
    }
}
