// 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 Microsoft.AspNetCore.Razor.Language.Intermediate;

namespace Microsoft.AspNetCore.Razor.Language.CodeGeneration
{
    internal class DefaultCodeRenderingContext : CodeRenderingContext
    {
        private readonly Stack<IntermediateNode> _ancestors;
        private readonly RazorCodeDocument _codeDocument;
        private readonly DocumentIntermediateNode _documentNode;
        private readonly List<ScopeInternal> _scopes;

        public DefaultCodeRenderingContext(
            CodeWriter codeWriter,
            IntermediateNodeWriter nodeWriter,
            RazorCodeDocument codeDocument,
            DocumentIntermediateNode documentNode,
            RazorCodeGenerationOptions options)
        {
            if (codeWriter == null)
            {
                throw new ArgumentNullException(nameof(codeWriter));
            }

            if (nodeWriter == null)
            {
                throw new ArgumentNullException(nameof(nodeWriter));
            }

            if (codeDocument == null)
            {
                throw new ArgumentNullException(nameof(codeDocument));
            }

            if (documentNode == null)
            {
                throw new ArgumentNullException(nameof(documentNode));
            }

            if (options == null)
            {
                throw new ArgumentNullException(nameof(options));
            }

            CodeWriter = codeWriter;
            _codeDocument = codeDocument;
            _documentNode = documentNode;
            Options = options;

            _ancestors = new Stack<IntermediateNode>();
            Diagnostics = new RazorDiagnosticCollection();
            Items = new ItemCollection();
            SourceMappings = new List<SourceMapping>();
            LinePragmas = new List<LinePragma>();

            var diagnostics = _documentNode.GetAllDiagnostics();
            for (var i = 0; i < diagnostics.Count; i++)
            {
                Diagnostics.Add(diagnostics[i]);
            }

            var newLineString = codeDocument.Items[NewLineString];
            if (newLineString != null)
            {
                // Set new line character to a specific string regardless of platform, for testing purposes.
                codeWriter.NewLine = (string)newLineString;
            }

            Items[NewLineString] = codeDocument.Items[NewLineString];
            Items[SuppressUniqueIds] = codeDocument.Items[SuppressUniqueIds];

            _scopes = new List<ScopeInternal>();
            _scopes.Add(new ScopeInternal(nodeWriter));
        }

        // This will be initialized by the document writer when the context is 'live'.
        public IntermediateNodeVisitor Visitor { get; set; }

        public override IEnumerable<IntermediateNode> Ancestors => _ancestors;

        internal Stack<IntermediateNode> AncestorsInternal => _ancestors;

        public override CodeWriter CodeWriter { get; }

        public override RazorDiagnosticCollection Diagnostics { get; }

        public override string DocumentKind { get; }

        public override ItemCollection Items { get; }

        public List<SourceMapping> SourceMappings { get; }

        internal List<LinePragma> LinePragmas { get; }

        public override IntermediateNodeWriter NodeWriter => Current.Writer;

        public override RazorCodeGenerationOptions Options { get; }

        public override IntermediateNode Parent => _ancestors.Count == 0 ? null : _ancestors.Peek();

        public override RazorSourceDocument SourceDocument => _codeDocument.Source;

        private ScopeInternal Current => _scopes[_scopes.Count - 1];

        public override void AddSourceMappingFor(IntermediateNode node)
        {
            if (node == null)
            {
                throw new ArgumentNullException(nameof(node));
            }

            if (node.Source == null)
            {
                return;
            }

            if (SourceDocument.FilePath != null &&
                !string.Equals(SourceDocument.FilePath, node.Source.Value.FilePath, StringComparison.OrdinalIgnoreCase))
            {
                // We don't want to generate line mappings for imports.
                return;
            }

            var source = node.Source.Value;
            var generatedLocation = new SourceSpan(CodeWriter.Location, source.Length);
            var sourceMapping = new SourceMapping(source, generatedLocation);

            SourceMappings.Add(sourceMapping);
        }

        public override void RenderChildren(IntermediateNode node)
        {
            if (node == null)
            {
                throw new ArgumentNullException(nameof(node));
            }

            _ancestors.Push(node);

            for (var i = 0; i < node.Children.Count; i++)
            {
                Visitor.Visit(node.Children[i]);
            }

            _ancestors.Pop();
        }

        public override void RenderChildren(IntermediateNode node, IntermediateNodeWriter writer)
        {
            if (node == null)
            {
                throw new ArgumentNullException(nameof(node));
            }

            if (writer == null)
            {
                throw new ArgumentNullException(nameof(writer));
            }

            _scopes.Add(new ScopeInternal(writer));
            _ancestors.Push(node);

            for (var i = 0; i < node.Children.Count; i++)
            {
                Visitor.Visit(node.Children[i]);
            }

            _ancestors.Pop();
            _scopes.RemoveAt(_scopes.Count - 1);
        }

        public override void RenderNode(IntermediateNode node)
        {
            if (node == null)
            {
                throw new ArgumentNullException(nameof(node));
            }

            Visitor.Visit(node);
        }

        public override void RenderNode(IntermediateNode node, IntermediateNodeWriter writer)
        {
            if (node == null)
            {
                throw new ArgumentNullException(nameof(node));
            }

            if (writer == null)
            {
                throw new ArgumentNullException(nameof(writer));
            }

            _scopes.Add(new ScopeInternal(writer));

            Visitor.Visit(node);

            _scopes.RemoveAt(_scopes.Count - 1);
        }

        public override void AddLinePragma(LinePragma linePragma)
        {
            LinePragmas.Add(linePragma);
        }

        private struct ScopeInternal
        {
            public ScopeInternal(IntermediateNodeWriter writer)
            {
                Writer = writer;
            }

            public IntermediateNodeWriter Writer { get; }
        }
    }
}
