using System.Collections.Generic;
using System.IO;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;

namespace Semmle.Extraction.CSharp.Entities
{
    internal enum AttributeKind
    {
        Default = 0,
        Return = 1,
        Assembly = 2,
        Module = 3,
    }

    internal class Attribute : CachedEntity<AttributeData>, IExpressionParentEntity
    {
        bool IExpressionParentEntity.IsTopLevelParent => true;

        private readonly AttributeSyntax? attributeSyntax;
        private readonly IEntity entity;
        private readonly AttributeKind kind;

        private Attribute(Context cx, AttributeData attributeData, IEntity entity, AttributeKind kind)
            : base(cx, attributeData)
        {
            this.attributeSyntax = attributeData.ApplicationSyntaxReference?.GetSyntax() as AttributeSyntax;
            this.entity = entity;
            this.kind = kind;
        }

        public override void WriteId(EscapingTextWriter trapFile)
        {
            if (ReportingLocation?.IsInSource == true)
            {
                trapFile.WriteSubId(Location);
                trapFile.Write(";attribute");
            }
            else
            {
                trapFile.Write('*');
            }
        }

        public sealed override void WriteQuotedId(EscapingTextWriter trapFile)
        {
            if (ReportingLocation?.IsInSource == true)
            {
                base.WriteQuotedId(trapFile);
            }
            else
            {
                trapFile.Write('*');
            }
        }

        public override void Populate(TextWriter trapFile)
        {
            // In this case, we don't extract the attribute again, as it was extracted using * ID
            // originally and we re-use that.
            if (Context.OnlyScaffold && (ReportingLocation is null || !ReportingLocation.IsInSource))
            {
                return;
            }

            var type = Type.Create(Context, Symbol.AttributeClass);
            trapFile.attributes(this, kind, type.TypeRef, entity);

            if (Context.OnlyScaffold)
            {
                return;
            }

            WriteLocationToTrap(trapFile.attribute_location, this, Location);

            if (attributeSyntax is not null)
            {
                WriteLocationToTrap(trapFile.attribute_location, this, Assembly.CreateOutputAssembly(Context));

                TypeMention.Create(Context, attributeSyntax.Name, this, type);
            }

            ExtractArguments(trapFile);
        }

        private void ExtractArguments(TextWriter trapFile)
        {
            var ctorArguments = attributeSyntax?.ArgumentList?.Arguments.Where(a => a.NameEquals is null).ToList();

            var childIndex = 0;
            for (var i = 0; i < Symbol.ConstructorArguments.Length; i++)
            {
                var constructorArgument = Symbol.ConstructorArguments[i];
                var paramName = Symbol.AttributeConstructor?.Parameters[i].Name;
                var argSyntax = ctorArguments?.SingleOrDefault(a => a.NameColon is not null && a.NameColon.Name.Identifier.Text == paramName);

                var isParamsParameter = false;

                if (argSyntax is null &&                            // couldn't find named argument
                    ctorArguments?.Count > childIndex &&            // there're more arguments
                    ctorArguments[childIndex].NameColon is null)    // the argument is positional
                {
                    // The current argument is not named
                    // so the previous ones were also not named
                    // so the child index matches the parameter index.
                    isParamsParameter = Symbol.AttributeConstructor?.Parameters[childIndex].IsParams == true;
                    argSyntax = ctorArguments[childIndex];
                }

                CreateExpressionFromArgument(
                    constructorArgument,
                    argSyntax?.Expression,
                    this,
                    childIndex++);

                if (isParamsParameter &&
                    ctorArguments is not null)
                {
                    // The current argument is a params argument, so we're processing all the remaining arguments:
                    while (childIndex < ctorArguments.Count)
                    {
                        if (ctorArguments[childIndex].Expression is null)
                        {
                            // This shouldn't happen
                            continue;
                        }

                        CreateExpressionFromArgument(
                            constructorArgument,
                            ctorArguments[childIndex].Expression,
                            this,
                            childIndex);

                        childIndex++;
                    }
                }
            }

            foreach (var namedArgument in Symbol.NamedArguments)
            {
                var expr = CreateExpressionFromArgument(
                    namedArgument.Value,
                    attributeSyntax?.ArgumentList?.Arguments.Single(a => a.NameEquals?.Name?.Identifier.Text == namedArgument.Key).Expression,
                    this,
                    childIndex++);

                if (expr is not null)
                {
                    trapFile.expr_argument_name(expr, namedArgument.Key);
                }
            }
        }

        private Expression? CreateExpressionFromArgument(TypedConstant constant, ExpressionSyntax? syntax, IExpressionParentEntity parent,
            int childIndex)
        {
            return syntax is null
                ? Expression.CreateGenerated(Context, constant, parent, childIndex, Location)
                : Expression.Create(Context, syntax, parent, childIndex);
        }

        public override TrapStackBehaviour TrapStackBehaviour => TrapStackBehaviour.OptionalLabel;

        public override Microsoft.CodeAnalysis.Location? ReportingLocation => attributeSyntax?.Name.GetLocation();

        private Location? location;

        private Location Location =>
            location ??= Context.CreateLocation(attributeSyntax is null
                ? entity.ReportingLocation
                : attributeSyntax.Name.GetLocation());

        public override bool NeedsPopulation => true;

        private static void ExtractAttributes(Context cx, IEnumerable<AttributeData> attributes, IEntity entity, AttributeKind kind)
        {
            foreach (var attribute in attributes)
            {
                Create(cx, attribute, entity, kind);
            }
        }

        public static void ExtractAttributes(Context cx, ISymbol symbol, IEntity entity)
        {
            ExtractAttributes(cx, symbol.GetAttributes(), entity, AttributeKind.Default);
            if (symbol is IMethodSymbol method)
            {
                ExtractAttributes(cx, method.GetReturnTypeAttributes(), entity, AttributeKind.Return);
            }
        }


        public static Attribute Create(Context cx, AttributeData attributeData, IEntity entity, AttributeKind kind)
        {
            var init = (attributeData, entity, kind);
            return AttributeFactory.Instance.CreateEntity(cx, attributeData, init);
        }

        private class AttributeFactory : CachedEntityFactory<(AttributeData attributeData, IEntity receiver, AttributeKind kind), Attribute>
        {
            public static readonly AttributeFactory Instance = new AttributeFactory();

            public override Attribute Create(Context cx, (AttributeData attributeData, IEntity receiver, AttributeKind kind) init) =>
                new Attribute(cx, init.attributeData, init.receiver, init.kind);
        }
    }
}
