// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

#nullable disable

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;

namespace CSharpSyntaxGenerator
{
    internal abstract class AbstractFileWriter
    {
        private readonly TextWriter _writer;
        private readonly Tree _tree;
        private readonly IDictionary<string, string> _parentMap;
        private readonly ILookup<string, string> _childMap;

        private readonly IDictionary<string, Node> _nodeMap;
        private readonly IDictionary<string, TreeType> _typeMap;

        private const int INDENT_SIZE = 4;
        private int _indentLevel;
        private bool _needIndent = true;

        protected AbstractFileWriter(TextWriter writer, Tree tree, CancellationToken cancellationToken)
        {
            _writer = writer;
            _tree = tree;
            _nodeMap = tree.Types.OfType<Node>().ToDictionary(n => n.Name);
            _typeMap = tree.Types.ToDictionary(n => n.Name);
            _parentMap = tree.Types.ToDictionary(n => n.Name, n => n.Base);
            _parentMap.Add(tree.Root, null);
            _childMap = tree.Types.ToLookup(n => n.Base, n => n.Name);

            CancellationToken = cancellationToken;
        }

        protected IDictionary<string, string> ParentMap { get { return _parentMap; } }
        protected ILookup<string, string> ChildMap { get { return _childMap; } }
        protected Tree Tree { get { return _tree; } }
        protected CancellationToken CancellationToken { get; }

        #region Output helpers

        protected void Indent()
        {
            _indentLevel++;
        }

        protected void Unindent()
        {
            if (_indentLevel <= 0)
            {
                throw new InvalidOperationException("Cannot unindent from base level");
            }
            _indentLevel--;
        }

        protected void Write(string msg)
        {
            WriteIndentIfNeeded();
            _writer.Write(msg);
        }

        protected void WriteLine()
        {
            WriteLine("");
        }

        protected void WriteLine(string msg)
        {
            CancellationToken.ThrowIfCancellationRequested();

            if (msg != "")
            {
                WriteIndentIfNeeded();
            }

            _writer.WriteLine(msg);
            _needIndent = true; //need an indent after each line break
        }

        protected void WriteLineWithoutIndent(string msg)
        {
            _writer.WriteLine(msg);
            _needIndent = true; //need an indent after each line break
        }

        private void WriteIndentIfNeeded()
        {
            if (_needIndent)
            {
                _writer.Write(new string(' ', _indentLevel * INDENT_SIZE));
                _needIndent = false;
            }
        }

        /// <summary>
        /// Joins all the values together in <paramref name="values"/> into one string with each
        /// value separated by a comma.  Values can be either <see cref="string"/>s or <see
        /// cref="IEnumerable{T}"/>s of <see cref="string"/>.  All of these are flattened into a
        /// single sequence that is joined. Empty strings are ignored.
        /// </summary>
        protected string CommaJoin(params object[] values)
            => Join(", ", values);

        protected string Join(string separator, params object[] values)
            => string.Join(separator, values.SelectMany(v => (v switch
            {
                string s => new[] { s },
                IEnumerable<string> ss => ss,
                _ => throw new InvalidOperationException("Join must be passed strings or collections of strings")
            }).Where(s => s != "")));

        protected void OpenBlock()
        {
            WriteLine("{");
            Indent();
        }

        protected void CloseBlock(string extra = "")
        {
            Unindent();
            WriteLine("}" + extra);
        }

        #endregion Output helpers

        #region Node helpers

        protected static string OverrideOrNewModifier(Field field)
        {
            return IsOverride(field) ? "override " : IsNew(field) ? "new " : "";
        }

        protected static bool CanBeField(Field field)
        {
            return field.Type != "SyntaxToken" && !IsAnyList(field.Type) && !IsOverride(field) && !IsNew(field);
        }

        protected static string GetFieldType(Field field, bool green)
        {
            // Fields in red trees are lazily initialized, with null as the uninitialized value
            return getNullableAwareType(field.Type, optionalOrLazy: IsOptional(field) || !green, green);

            static string getNullableAwareType(string fieldType, bool optionalOrLazy, bool green)
            {
                if (IsAnyList(fieldType))
                {
                    if (optionalOrLazy)
                        return green ? "GreenNode?" : "SyntaxNode?";
                    else
                        return green ? "GreenNode?" : "SyntaxNode";
                }

                switch (fieldType)
                {
                    case var _ when !optionalOrLazy:
                        return fieldType;

                    case "bool":
                    case "SyntaxToken" when !green:
                        return fieldType;

                    default:
                        return fieldType + "?";
                }
            }
        }

        protected bool IsDerivedOrListOfDerived(string baseType, string derivedType)
        {
            return IsDerivedType(baseType, derivedType)
                || ((IsNodeList(derivedType) || IsSeparatedNodeList(derivedType))
                    && IsDerivedType(baseType, GetElementType(derivedType)));
        }

        protected static bool IsSeparatedNodeList(string typeName)
        {
            return typeName.StartsWith("SeparatedSyntaxList<", StringComparison.Ordinal);
        }

        protected static bool IsNodeList(string typeName)
        {
            return typeName.StartsWith("SyntaxList<", StringComparison.Ordinal);
        }

        public static bool IsAnyNodeList(string typeName)
        {
            return IsNodeList(typeName) || IsSeparatedNodeList(typeName);
        }

        protected bool IsNodeOrNodeList(string typeName)
        {
            return IsNode(typeName) || IsNodeList(typeName) || IsSeparatedNodeList(typeName) || typeName == "SyntaxNodeOrTokenList";
        }

        protected static string GetElementType(string typeName)
        {
            if (!typeName.Contains("<"))
                return string.Empty;
            int iStart = typeName.IndexOf('<');
            int iEnd = typeName.IndexOf('>', iStart + 1);
            if (iEnd < iStart)
                return string.Empty;
            var sub = typeName.Substring(iStart + 1, iEnd - iStart - 1);
            return sub;
        }

        protected static bool IsAnyList(string typeName)
        {
            return IsNodeList(typeName) || IsSeparatedNodeList(typeName) || typeName == "SyntaxNodeOrTokenList";
        }

        protected bool IsDerivedType(string typeName, string derivedTypeName)
        {
            if (typeName == derivedTypeName)
                return true;
            if (derivedTypeName != null && _parentMap.TryGetValue(derivedTypeName, out var baseType))
            {
                return IsDerivedType(typeName, baseType);
            }
            return false;
        }

        protected static bool IsRoot(Node n)
        {
            return n.Root != null && string.Compare(n.Root, "true", true) == 0;
        }

        protected bool IsNode(string typeName)
        {
            return _parentMap.ContainsKey(typeName);
        }

        protected Node GetNode(string typeName)
            => _nodeMap.TryGetValue(typeName, out var node) ? node : null;

        protected TreeType GetTreeType(string typeName)
            => _typeMap.TryGetValue(typeName, out var node) ? node : null;

        private static bool IsTrue(string val)
            => val != null && string.Compare(val, "true", true) == 0;

        protected static bool IsOptional(Field f)
            => IsTrue(f.Optional);

        protected static bool IsOverride(Field f)
            => IsTrue(f.Override);

        protected static bool IsNew(Field f)
            => IsTrue(f.New);

        protected static bool HasErrors(Node n)
        {
            return n.Errors == null || string.Compare(n.Errors, "true", true) == 0;
        }

        protected static string CamelCase(string name)
        {
            if (char.IsUpper(name[0]))
            {
                name = char.ToLowerInvariant(name[0]) + name.Substring(1);
            }
            return FixKeyword(name);
        }

        protected static string FixKeyword(string name)
        {
            if (IsKeyword(name))
            {
                return "@" + name;
            }
            return name;
        }

        protected static string StripPost(string name, string post)
        {
            return name.EndsWith(post, StringComparison.Ordinal)
                ? name.Substring(0, name.Length - post.Length)
                : name;
        }

        protected static bool IsKeyword(string name)
        {
            switch (name)
            {
                case "bool":
                case "byte":
                case "sbyte":
                case "short":
                case "ushort":
                case "int":
                case "uint":
                case "long":
                case "ulong":
                case "double":
                case "float":
                case "decimal":
                case "string":
                case "char":
                case "object":
                case "typeof":
                case "sizeof":
                case "null":
                case "true":
                case "false":
                case "if":
                case "else":
                case "while":
                case "for":
                case "foreach":
                case "do":
                case "switch":
                case "case":
                case "default":
                case "lock":
                case "try":
                case "throw":
                case "catch":
                case "finally":
                case "goto":
                case "break":
                case "continue":
                case "return":
                case "public":
                case "private":
                case "internal":
                case "protected":
                case "static":
                case "readonly":
                case "sealed":
                case "const":
                case "new":
                case "override":
                case "abstract":
                case "virtual":
                case "partial":
                case "ref":
                case "out":
                case "in":
                case "where":
                case "params":
                case "this":
                case "base":
                case "namespace":
                case "using":
                case "class":
                case "struct":
                case "interface":
                case "delegate":
                case "checked":
                case "get":
                case "set":
                case "add":
                case "remove":
                case "operator":
                case "implicit":
                case "explicit":
                case "fixed":
                case "extern":
                case "event":
                case "enum":
                case "unsafe":
                    return true;
                default:
                    return false;
            }
        }

        protected List<Kind> GetKindsOfFieldOrNearestParent(TreeType nd, Field field)
        {
            while ((field.Kinds is null || field.Kinds.Count == 0) && IsOverride(field))
            {
                nd = GetTreeType(nd.Base);
                field = (nd switch
                {
                    Node node => node.Fields,
                    AbstractNode abstractNode => abstractNode.Fields,
                    _ => throw new InvalidOperationException("Unexpected node type.")
                }).Single(f => f.Name == field.Name);
            }

            return field.Kinds.Distinct().ToList();
        }

        #endregion Node helpers
    }
}
