// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Razor.Language.CodeGeneration;
using Microsoft.AspNetCore.Razor.Language.Intermediate;

namespace Microsoft.AspNetCore.Razor.Language
{
    public abstract class DocumentClassifierPassBase : IntermediateNodePassBase, IRazorDocumentClassifierPass
    {
        protected abstract string DocumentKind { get; }

        protected IReadOnlyList<ICodeTargetExtension> TargetExtensions { get; private set; }

        protected override void OnInitialized()
        {
            var feature = Engine.GetFeature<IRazorTargetExtensionFeature>();
            TargetExtensions = feature?.TargetExtensions.ToArray() ?? Array.Empty<ICodeTargetExtension>();
        }

        protected sealed override void ExecuteCore(RazorCodeDocument codeDocument, DocumentIntermediateNode documentNode)
        {
            if (documentNode.DocumentKind != null)
            {
                return;
            }

            if (!IsMatch(codeDocument, documentNode))
            {
                return;
            }

            documentNode.DocumentKind = DocumentKind;
            documentNode.Target = CreateTarget(codeDocument, documentNode.Options);
            if (documentNode.Target == null)
            {
                throw new InvalidOperationException($"{nameof(CreateTarget)} must return a non-null {nameof(CodeTarget)}.");
            }

            Rewrite(codeDocument, documentNode);
        }

        private void Rewrite(RazorCodeDocument codeDocument, DocumentIntermediateNode documentNode)
        {
            // Rewrite the document from a flat structure to use a sensible default structure,
            // a namespace and class declaration with a single 'razor' method.
            var children = new List<IntermediateNode>(documentNode.Children);
            documentNode.Children.Clear();

            var @namespace = new NamespaceDeclarationIntermediateNode();
            @namespace.Annotations[CommonAnnotations.PrimaryNamespace] = CommonAnnotations.PrimaryNamespace;

            var @class = new ClassDeclarationIntermediateNode();
            @class.Annotations[CommonAnnotations.PrimaryClass] = CommonAnnotations.PrimaryClass;

            var method = new MethodDeclarationIntermediateNode();
            method.Annotations[CommonAnnotations.PrimaryMethod] = CommonAnnotations.PrimaryMethod;

            var documentBuilder = IntermediateNodeBuilder.Create(documentNode);

            var namespaceBuilder = IntermediateNodeBuilder.Create(documentBuilder.Current);
            namespaceBuilder.Push(@namespace);

            var classBuilder = IntermediateNodeBuilder.Create(namespaceBuilder.Current);
            classBuilder.Push(@class);

            var methodBuilder = IntermediateNodeBuilder.Create(classBuilder.Current);
            methodBuilder.Push(method);

            var visitor = new Visitor(documentBuilder, namespaceBuilder, classBuilder, methodBuilder);

            for (var i = 0; i < children.Count; i++)
            {
                visitor.Visit(children[i]);
            }

            // Note that this is called at the *end* of rewriting so that user code can see the tree
            // and look at its content to make a decision.
            OnDocumentStructureCreated(codeDocument, @namespace, @class, method);
        }

        protected abstract bool IsMatch(RazorCodeDocument codeDocument, DocumentIntermediateNode documentNode);

        // virtual to allow replacing the code target wholesale.
        protected virtual CodeTarget CreateTarget(RazorCodeDocument codeDocument, RazorCodeGenerationOptions options)
        {
            return CodeTarget.CreateDefault(codeDocument, options, (builder) =>
            {
                for (var i = 0; i < TargetExtensions.Count; i++)
                {
                    builder.TargetExtensions.Add(TargetExtensions[i]);
                }

                ConfigureTarget(builder);
            });
        }

        protected virtual void ConfigureTarget(CodeTargetBuilder builder)
        {
            // Intentionally empty.
        }

        protected virtual void OnDocumentStructureCreated(
            RazorCodeDocument codeDocument,
            NamespaceDeclarationIntermediateNode @namespace,
            ClassDeclarationIntermediateNode @class,
            MethodDeclarationIntermediateNode @method)
        {
            // Intentionally empty.
        }

        private class Visitor : IntermediateNodeVisitor
        {
            private readonly IntermediateNodeBuilder _document;
            private readonly IntermediateNodeBuilder _namespace;
            private readonly IntermediateNodeBuilder _class;
            private readonly IntermediateNodeBuilder _method;

            public Visitor(IntermediateNodeBuilder document, IntermediateNodeBuilder @namespace, IntermediateNodeBuilder @class, IntermediateNodeBuilder method)
            {
                _document = document;
                _namespace = @namespace;
                _class = @class;
                _method = method;
            }

            public override void VisitUsingDirective(UsingDirectiveIntermediateNode node)
            {
                var children = _namespace.Current.Children;
                var i = children.Count - 1;
                for (; i >= 0; i--)
                {
                    var child = children[i];
                    if (child is UsingDirectiveIntermediateNode)
                    {
                        break;
                    }
                }

                _namespace.Insert(i + 1, node);
            }

            public override void VisitDefault(IntermediateNode node)
            {
                if (node is MemberDeclarationIntermediateNode)
                {
                    _class.Add(node);
                    return;
                }

                _method.Add(node);
            }
        }
    }
}
