// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using Microsoft.Extensions.Internal;

namespace Microsoft.AspNetCore.Razor.Language.Legacy
{
    internal class Block : SyntaxTreeNode
    {
        public Block(BlockBuilder source)
            : this(source.Type, source.Children, source.ChunkGenerator)
        {
            source.Reset();
        }

        protected Block(BlockKind? type, IReadOnlyList<SyntaxTreeNode> children, IParentChunkGenerator generator)
        {
            if (type == null)
            {
                throw new InvalidOperationException(LegacyResources.Block_Type_Not_Specified);
            }

            Type = type.Value;
            Children = children;
            ChunkGenerator = generator;

            // Perf: Avoid allocating an enumerator.
            for (var i = 0; i < Children.Count; i++)
            {
                Children[i].Parent = this;
            }
        }
        public IParentChunkGenerator ChunkGenerator { get; }

        public BlockKind Type { get; }

        public IReadOnlyList<SyntaxTreeNode> Children { get; }

        public override bool IsBlock => true;

        public override SourceLocation Start
        {
            get
            {
                var child = Children.FirstOrDefault();
                if (child == null)
                {
                    return SourceLocation.Zero;
                }
                else
                {
                    return child.Start;
                }
            }
        }

        public override int Length => Children.Sum(child => child.Length);

        public virtual IEnumerable<Span> Flatten()
        {
            // Perf: Avoid allocating an enumerator.
            for (var i = 0; i < Children.Count; i++)
            {
                var element = Children[i];
                var span = element as Span;
                if (span != null)
                {
                    yield return span;
                }
                else
                {
                    var block = element as Block;
                    foreach (Span childSpan in block.Flatten())
                    {
                        yield return childSpan;
                    }
                }
            }
        }

        public Span FindFirstDescendentSpan()
        {
            SyntaxTreeNode current = this;
            while (current != null && current.IsBlock)
            {
                current = ((Block)current).Children.FirstOrDefault();
            }
            return current as Span;
        }

        public Span FindLastDescendentSpan()
        {
            SyntaxTreeNode current = this;
            while (current != null && current.IsBlock)
            {
                current = ((Block)current).Children.LastOrDefault();
            }
            return current as Span;
        }

        public virtual Span LocateOwner(SourceChange change) => LocateOwner(change, Children);

        protected static Span LocateOwner(SourceChange change, IEnumerable<SyntaxTreeNode> elements)
        {
            // Ask each child recursively
            Span owner = null;
            foreach (var element in elements)
            {
                var span = element as Span;
                if (span == null)
                {
                    owner = ((Block)element).LocateOwner(change);
                }
                else
                {
                    if (change.Span.AbsoluteIndex < span.Start.AbsoluteIndex)
                    {
                        // Early escape for cases where changes overlap multiple spans
                        // In those cases, the span will return false, and we don't want to search the whole tree
                        // So if the current span starts after the change, we know we've searched as far as we need to
                        break;
                    }
                    owner = span.EditHandler.OwnsChange(span, change) ? span : owner;
                }

                if (owner != null)
                {
                    break;
                }
            }
            return owner;
        }

        public override string ToString()
        {
            return string.Format(
                CultureInfo.CurrentCulture,
                "{0} Block at {1}::{2} (Gen:{3})",
                Type,
                Start,
                Length,
                ChunkGenerator);
        }

        public override bool Equals(object obj)
        {
            var other = obj as Block;
            return other != null &&
                Type == other.Type &&
                Equals(ChunkGenerator, other.ChunkGenerator) &&
                ChildrenEqual(Children, other.Children);
        }

        public override int GetHashCode()
        {
            var hashCodeCombiner = HashCodeCombiner.Start();
            hashCodeCombiner.Add(Type);
            hashCodeCombiner.Add(ChunkGenerator);
            hashCodeCombiner.Add(Children);

            return hashCodeCombiner;
        }

        private static bool ChildrenEqual(IEnumerable<SyntaxTreeNode> left, IEnumerable<SyntaxTreeNode> right)
        {
            IEnumerator<SyntaxTreeNode> leftEnum = left.GetEnumerator();
            IEnumerator<SyntaxTreeNode> rightEnum = right.GetEnumerator();
            while (leftEnum.MoveNext())
            {
                if (!rightEnum.MoveNext() || // More items in left than in right
                    !Equals(leftEnum.Current, rightEnum.Current))
                {
                    // Nodes are not equal
                    return false;
                }
            }
            if (rightEnum.MoveNext())
            {
                // More items in right than left
                return false;
            }
            return true;
        }

        public override bool EquivalentTo(SyntaxTreeNode node)
        {
            var other = node as Block;
            if (other == null || other.Type != Type)
            {
                return false;
            }

            return Enumerable.SequenceEqual(Children, other.Children, EquivalenceComparer.Default);
        }

        public override int GetEquivalenceHash()
        {
            var hashCodeCombiner = HashCodeCombiner.Start();
            hashCodeCombiner.Add(Type);
            foreach (var child in Children)
            {
                hashCodeCombiner.Add(child.GetEquivalenceHash());
            }

            return hashCodeCombiner.CombinedHash;
        }

        public override void Accept(ParserVisitor visitor)
        {
            visitor.VisitBlock(this);
        }

        private class EquivalenceComparer : IEqualityComparer<SyntaxTreeNode>
        {
            public static readonly EquivalenceComparer Default = new EquivalenceComparer();

            private EquivalenceComparer()
            {
            }

            public bool Equals(SyntaxTreeNode nodeX, SyntaxTreeNode nodeY)
            {
                if (nodeX == nodeY)
                {
                    return true;
                }

                return nodeX != null && nodeX.EquivalentTo(nodeY);
            }

            public int GetHashCode(SyntaxTreeNode node)
            {
                if (node == null)
                {
                    throw new ArgumentNullException(nameof(node));
                }

                return node.GetEquivalenceHash();
            }
        }
    }
}
