/*
    Copyright 2017-2020 Katy Coe - http://www.hearthcode.org - http://www.djkaty.com

    All rights reserved.
*/

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;

namespace Il2CppInspector.Reflection
{
    public class ParameterInfo
    {
        // IL2CPP-specific data
        public Il2CppParameterDefinition Definition { get; }
        public int Index { get; }
        public ulong DefaultValueMetadataAddress { get; }

        // Information/flags about the parameter
        public ParameterAttributes Attributes { get; }

        // Custom attributes for this parameter
        public IEnumerable<CustomAttributeData> CustomAttributes => CustomAttributeData.GetCustomAttributes(this);

        // True if the parameter has a default value
        public bool HasDefaultValue => (Attributes & ParameterAttributes.HasDefault) != 0;

        // Default value for the parameter
        public object DefaultValue { get; }

        public bool IsIn => (Attributes & ParameterAttributes.In) != 0;
        public bool IsOptional => (Attributes & ParameterAttributes.Optional) != 0;
        public bool IsOut => (Attributes & ParameterAttributes.Out) != 0;
        public bool IsRetval => (Attributes & ParameterAttributes.Retval) != 0;

        // The method in which the parameter is defined
        public MethodBase DeclaringMethod { get; }

        // Name of parameter
        public string Name { get; }
        public string CSharpSafeName => Constants.Keywords.Contains(Name) ? "@" + Name : Name;

        // Type of this parameter
        private readonly int paramTypeReference;
        public TypeInfo ParameterType => DeclaringMethod.Assembly.Model.TypesByReferenceIndex[paramTypeReference];

        // Zero-indexed position of the parameter in parameter list
        public int Position { get; }

        // Create a parameter. Specify paramIndex == -1 for a return type parameter
        public ParameterInfo(Il2CppInspector pkg, int paramIndex, MethodBase declaringMethod) {
            Index = paramIndex;
            DeclaringMethod = declaringMethod;

            if (paramIndex == -1) {
                Position = -1;
                paramTypeReference = declaringMethod.Definition.returnType;
                Attributes |= ParameterAttributes.Retval;
                return;
            }

            Definition = pkg.Params[Index];
            Name = pkg.Strings[Definition.nameIndex];

            // Handle unnamed/obfuscated parameter names
            if (string.IsNullOrEmpty(Name))
                Name = string.Format($"param_{Index:x8}");

            Position = paramIndex - declaringMethod.Definition.parameterStart;
            paramTypeReference = Definition.typeIndex;
            var paramType = pkg.TypeReferences[paramTypeReference];

            if ((paramType.attrs & Il2CppConstants.PARAM_ATTRIBUTE_HAS_DEFAULT) != 0)
                Attributes |= ParameterAttributes.HasDefault;
            if ((paramType.attrs & Il2CppConstants.PARAM_ATTRIBUTE_OPTIONAL) != 0)
                Attributes |= ParameterAttributes.Optional;
            if ((paramType.attrs & Il2CppConstants.PARAM_ATTRIBUTE_IN) != 0)
                Attributes |= ParameterAttributes.In;
            if ((paramType.attrs & Il2CppConstants.PARAM_ATTRIBUTE_OUT) != 0)
                Attributes |= ParameterAttributes.Out;
            if ((paramType.attrs & Il2CppConstants.PARAM_ATTRIBUTE_RESERVED_MASK) != 0)
                Attributes |= ParameterAttributes.ReservedMask;
            if ((paramType.attrs & Il2CppConstants.PARAM_ATTRIBUTE_HAS_FIELD_MARSHAL) != 0)
                Attributes |= ParameterAttributes.HasFieldMarshal;

            if (Position == -1)
                Attributes |= ParameterAttributes.Retval;

            // Default initialization value if present
            if (pkg.ParameterDefaultValue.TryGetValue(paramIndex, out (ulong address, object variant) value)) {
                DefaultValue = value.variant;
                DefaultValueMetadataAddress = value.address;
            }
        }

        // Create a concrete type parameter from a generic type parameter
        public ParameterInfo(Il2CppModel model, ParameterInfo generic, TypeInfo concrete) {

            DeclaringMethod = generic.DeclaringMethod;
            Name = generic.Name;
            Position = generic.Position;
            Attributes = generic.Attributes;

            // Search for the concrete type's TypeRef index to store as the parameter type reference index
            paramTypeReference = Array.IndexOf(model.TypesByReferenceIndex, concrete);

            DefaultValue = generic.DefaultValue;
            DefaultValueMetadataAddress = generic.DefaultValueMetadataAddress;
        }

        // ref will be handled as part of the type name
        public string GetModifierString() =>
              (IsIn && !IsOut ? "in " : "")
            + (IsOut && !IsIn ? "out " : "") 
            + (IsIn && IsOut  ? "[In, Out] " : "")
            + (!IsIn && !IsOut && ParameterType.IsByRef ? "ref " : "");

        private string getCSharpSignatureString(Scope scope) => $"{GetModifierString()}{ParameterType.GetScopedCSharpName(scope, omitRef: true)}";
        public string GetSignatureString() => $"{GetModifierString()}{ParameterType.FullName}";

        public string GetParameterString(Scope usingScope, bool emitPointer = false, bool compileAttributes = false) => IsRetval? null :
              $"{CustomAttributes.ToString(usingScope, inline: true, emitPointer: emitPointer, mustCompile: compileAttributes).Replace("[ParamArray]", "params")}"
            + (Position == 0 && DeclaringMethod.GetCustomAttributes("System.Runtime.CompilerServices.ExtensionAttribute").Any()? "this ":"")
            + $"{getCSharpSignatureString(usingScope)} {CSharpSafeName}"
            + (IsOptional? " = " + DefaultValue.ToCSharpValue(ParameterType, usingScope) 
            + (emitPointer && !(DefaultValue is null)? $" /* Metadata: 0x{(uint) DefaultValueMetadataAddress:X8} */" : "") : "");

        public string GetReturnParameterString(Scope scope) => !IsRetval? null : getCSharpSignatureString(scope);

        public override string ToString() => ParameterType.Name + " " + Name;
    }
}