using System;
using Microsoft.CodeAnalysis;
using Semmle.Extraction.CSharp.Populators;
using System.Linq;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Semmle.Extraction.Entities;

namespace Semmle.Extraction.CSharp.Entities
{
    public class Parameter : CachedSymbol<IParameterSymbol>, IExpressionParentEntity
    {
        protected IEntity Parent;
        protected readonly Parameter Original;

        protected Parameter(Context cx, IParameterSymbol init, IEntity parent, Parameter original)
            : base(cx, init)
        {
            Parent = parent;
            Original = original ?? this;
        }

        public override Microsoft.CodeAnalysis.Location ReportingLocation => symbol.GetSymbolLocation();

        public enum Kind
        {
            None, Ref, Out, Params, This, In
        }

        protected virtual int Ordinal
        {
            get
            {
                // For some reason, methods of kind ReducedExtension
                // omit the "this" parameter, so the parameters are
                // actually numbered from 1.
                // This is to be consistent from the original (unreduced) extension method.
                var method = symbol.ContainingSymbol as IMethodSymbol;
                bool isReducedExtension = method != null && method.MethodKind == MethodKind.ReducedExtension;
                return symbol.Ordinal + (isReducedExtension ? 1 : 0);
            }
        }

        Kind ParamKind
        {
            get
            {
                switch (symbol.RefKind)
                {
                    case RefKind.Out:
                        return Kind.Out;
                    case RefKind.Ref:
                        return Kind.Ref;
                    case RefKind.In:
                        return Kind.In;
                    default:
                        if (symbol.IsParams) return Kind.Params;

                        if (Ordinal == 0)
                        {
                            var method = symbol.ContainingSymbol as IMethodSymbol;
                            if (method != null && method.IsExtensionMethod) return Kind.This;
                        }
                        return Kind.None;
                }
            }
        }

        public static Parameter Create(Context cx, IParameterSymbol param, IEntity parent, Parameter original = null) =>
            ParameterFactory.Instance.CreateEntity(cx, param, parent, original);

        /// <summary>
        /// Gets the parameter entity for <paramref name="param"/> which must
        /// already have been created.
        /// </summary>
        public static Parameter GetAlreadyCreated(Context cx, IParameterSymbol param) =>
            ParameterFactory.Instance.CreateEntity(cx, param, null, null);

        public override IId Id
        {
            get
            {
                // This is due to a bug in Roslyn when ValueTuple.cs is extracted.
                // The parameter symbols don't match up properly so we don't have a parent.
                if (Parent == null)
                    Parent = Method.Create(Context, symbol.ContainingSymbol as IMethodSymbol);
                return new Key(Parent, "_", Ordinal, ";parameter");
            }
        }

        public override bool NeedsPopulation => true;

        string Name
        {
            get
            {
                // Very rarely, two parameters have the same name according to the data model.
                // This breaks our database constraints.
                // Generate an impossible name to ensure that it doesn't conflict.
                int conflictingCount = symbol.ContainingSymbol.GetParameters().Count(p => p.Ordinal < symbol.Ordinal && p.Name == symbol.Name);
                return conflictingCount > 0 ? symbol.Name + "`" + conflictingCount : symbol.Name;
            }
        }

        public override void Populate()
        {
            ExtractAttributes();
            ExtractNullability(symbol.NullableAnnotation);
            ExtractRefKind(symbol.RefKind);

            if (symbol.Name != Original.symbol.Name)
                Context.ModelError(symbol, "Inconsistent parameter declaration");

            var type = Type.Create(Context, symbol.Type);
            Context.Emit(Tuples.@params(this, Name, type.TypeRef, Ordinal, ParamKind, Parent, Original));

            foreach (var l in symbol.Locations)
                Context.Emit(Tuples.param_location(this, Context.Create(l)));

            if (!IsSourceDeclaration || !symbol.FromSource())
                return;

            BindComments();

            if (IsSourceDeclaration)
                foreach (var syntax in symbol.DeclaringSyntaxReferences.
                    Select(d => d.GetSyntax()).
                    OfType<ParameterSyntax>().
                    Where(s => s.Type != null))
                    TypeMention.Create(Context, syntax.Type, this, type);

            if (symbol.HasExplicitDefaultValue && Context.Defines(symbol))
            {
                // This is a slight bug in the dbscheme
                // We should really define param_default(param, string)
                // And use parameter child #0 to encode the default expression.
                var defaultValue = GetParameterDefaultValue(symbol);
                if (defaultValue == null)
                {
                    // In case this parameter belongs to an accessor of an indexer, we need
                    // to get the default value from the corresponding parameter belonging
                    // to the indexer itself
                    var method = (IMethodSymbol)symbol.ContainingSymbol;
                    if (method != null)
                    {
                        var i = method.Parameters.IndexOf(symbol);
                        var indexer = (IPropertySymbol)method.AssociatedSymbol;
                        if (indexer != null)
                            defaultValue = GetParameterDefaultValue(indexer.Parameters[i]);
                    }
                }

                if (defaultValue != null)
                {
                    Context.PopulateLater(() =>
                    {
                        Expression.Create(Context, defaultValue.Value, this, 0);
                    });
                }
            }
        }

        public override bool IsSourceDeclaration => symbol.IsSourceDeclaration();

        bool IExpressionParentEntity.IsTopLevelParent => true;

        static EqualsValueClauseSyntax GetParameterDefaultValue(IParameterSymbol parameter)
        {
            var syntax = parameter.DeclaringSyntaxReferences.Select(@ref => @ref.GetSyntax()).OfType<ParameterSyntax>().FirstOrDefault();
            return syntax != null ? syntax.Default : null;
        }

        class ParameterFactory : ICachedEntityFactory<(IParameterSymbol, IEntity, Parameter), Parameter>
        {
            public static readonly ParameterFactory Instance = new ParameterFactory();

            public Parameter Create(Context cx, (IParameterSymbol, IEntity, Parameter) init) => new Parameter(cx, init.Item1, init.Item2, init.Item3);
        }

        public override TrapStackBehaviour TrapStackBehaviour => TrapStackBehaviour.OptionalLabel;
    }

    class VarargsType : Type
    {
        VarargsType(Context cx)
            : base(cx, null) { }

        public override void Populate()
        {
            Context.Emit(Tuples.types(this, Kinds.TypeKind.ARGLIST, "__arglist"));
            Context.Emit(Tuples.parent_namespace(this, Namespace.Create(Context, Context.Compilation.GlobalNamespace)));
            Modifier.HasModifier(Context, this, "public");
        }

        public override bool NeedsPopulation => true;

        public sealed override IId Id => new Key("__arglist;type");

        public override int GetHashCode()
        {
            return 98735267;
        }

        public override bool Equals(object obj)
        {
            return obj != null && obj.GetType() == typeof(VarargsType);
        }

        public static VarargsType Create(Context cx) => VarargsTypeFactory.Instance.CreateEntity(cx, null);

        class VarargsTypeFactory : ICachedEntityFactory<string, VarargsType>
        {
            public static readonly VarargsTypeFactory Instance = new VarargsTypeFactory();

            public VarargsType Create(Context cx, string init) => new VarargsType(cx);
        }
    }

    class VarargsParam : Parameter
    {
        VarargsParam(Context cx, Method methodKey)
            : base(cx, null, methodKey, null) { }

        public override void Populate()
        {
            var typeKey = VarargsType.Create(Context);
            // !! Maybe originaldefinition is wrong
            Context.Emit(Tuples.@params(this, "", typeKey, Ordinal, Kind.None, Parent, this));
            Context.Emit(Tuples.param_location(this, GeneratedLocation.Create(Context)));
        }

        protected override int Ordinal => ((Method)Parent).OriginalDefinition.symbol.Parameters.Length;

        public override int GetHashCode()
        {
            return 9873567;
        }

        public override bool Equals(object obj)
        {
            return obj != null && obj.GetType() == typeof(VarargsParam);
        }

        public static VarargsParam Create(Context cx, Method method) => VarargsParamFactory.Instance.CreateEntity(cx, method);

        class VarargsParamFactory : ICachedEntityFactory<Method, VarargsParam>
        {
            public static readonly VarargsParamFactory Instance = new VarargsParamFactory();

            public VarargsParam Create(Context cx, Method init) => new VarargsParam(cx, init);
        }
    }

    class ConstructedExtensionParameter : Parameter
    {
        readonly ITypeSymbol ConstructedType;

        ConstructedExtensionParameter(Context cx, Method method, Parameter original)
            : base(cx, original.symbol, method, original)
        {
            ConstructedType = method.symbol.ReceiverType;
        }

        public override void Populate()
        {
            var typeKey = Type.Create(Context, ConstructedType);
            Context.Emit(Tuples.@params(this, Original.symbol.Name, typeKey.TypeRef, 0, Kind.This, Parent, Original));
            Context.Emit(Tuples.param_location(this, Original.Location));
        }

        public override int GetHashCode() => symbol.GetHashCode() + 31 * ConstructedType.GetHashCode();

        public override bool Equals(object obj)
        {
            var other = obj as ConstructedExtensionParameter;
            if (other == null || other.GetType() != typeof(ConstructedExtensionParameter))
                return false;

            return Equals(symbol, other.symbol) && Equals(ConstructedType, other.ConstructedType);
        }

        public static ConstructedExtensionParameter Create(Context cx, Method method, Parameter parameter) =>
            ExtensionParamFactory.Instance.CreateEntity(cx, (method, parameter));

        class ExtensionParamFactory : ICachedEntityFactory<(Method, Parameter), ConstructedExtensionParameter>
        {
            public static readonly ExtensionParamFactory Instance = new ExtensionParamFactory();

            public ConstructedExtensionParameter Create(Context cx, (Method, Parameter) init) =>
                new ConstructedExtensionParameter(cx, init.Item1, init.Item2);
        }
    }
}
