using System;
using System.Collections.Generic;
using System.Text;
using JetBrains.Annotations;
using UnityEditor.Graphing;
using System.Globalization;
using UnityEngine.Profiling;

namespace UnityEditor.ShaderGraph
{
    struct ShaderStringMapping
    {
        public AbstractMaterialNode node { get; set; }
        //        public List<AbstractMaterialNode> nodes { get; set; }
        public int startIndex { get; set; }
        public int count { get; set; }
    }

    class ShaderStringBuilder : IDisposable
    {
        enum ScopeType
        {
            Indent,
            Block,
            BlockSemicolon
        }

        StringBuilder m_StringBuilder;
        Stack<ScopeType> m_ScopeStack;
        int m_IndentationLevel;
        ShaderStringMapping m_CurrentMapping;
        List<ShaderStringMapping> m_Mappings;
        bool m_HumanReadable;

        const string k_IndentationString = "    ";
        const string k_NewLineString = "\n";

        internal AbstractMaterialNode currentNode
        {
            get { return m_CurrentMapping.node; }
            set
            {
                m_CurrentMapping.count = m_StringBuilder.Length - m_CurrentMapping.startIndex;
                if (m_CurrentMapping.count > 0)
                    m_Mappings.Add(m_CurrentMapping);
                m_CurrentMapping.node = value;
                m_CurrentMapping.startIndex = m_StringBuilder.Length;
                m_CurrentMapping.count = 0;
            }
        }

        internal List<ShaderStringMapping> mappings
        {
            get { return m_Mappings; }
        }

        public ShaderStringBuilder(int indentationLevel = 0, int stringBuilderSize = 8192, bool humanReadable = false)
        {
            IncreaseIndent(indentationLevel);
            m_StringBuilder = new StringBuilder(stringBuilderSize);
            m_ScopeStack = new Stack<ScopeType>();
            m_Mappings = new List<ShaderStringMapping>();
            m_CurrentMapping = new ShaderStringMapping();
            m_HumanReadable = humanReadable;
        }

        public void AppendNewLine()
        {
            m_StringBuilder.Append(k_NewLineString);
        }

        private void AppendLine(string value, int startIndex, int count)
        {
            if (value.Length > 0)
            {
                TryAppendIndentation();
                m_StringBuilder.Append(value, startIndex, count);
            }
            AppendNewLine();
        }

        public void AppendLine(string value)
        {
            if (!string.IsNullOrEmpty(value))
            {
                TryAppendIndentation();
                m_StringBuilder.Append(value);
            }
            AppendNewLine();
        }

        public void AddLine(string v0) { TryAppendIndentation(); Append(v0); AppendNewLine(); }
        public void AddLine(string v0, string v1) { TryAppendIndentation(); Append(v0); Append(v1); AppendNewLine(); }
        public void AddLine(string v0, string v1, string v2) { TryAppendIndentation(); Append(v0); Append(v1); Append(v2); AppendNewLine(); }
        public void AddLine(string v0, string v1, string v2, string v3) { TryAppendIndentation(); Append(v0); Append(v1); Append(v2); Append(v3); AppendNewLine(); }
        public void AddLine(string v0, string v1, string v2, string v3, string v4) { TryAppendIndentation(); Append(v0); Append(v1); Append(v2); Append(v3); Append(v4); AppendNewLine(); }
        public void AddLine(string v0, string v1, string v2, string v3, string v4, string v5) { TryAppendIndentation(); Append(v0); Append(v1); Append(v2); Append(v3); Append(v4); Append(v5); AppendNewLine(); }
        public void AddLine(string v0, string v1, string v2, string v3, string v4, string v5, string v6) { TryAppendIndentation(); Append(v0); Append(v1); Append(v2); Append(v3); Append(v4); Append(v5); Append(v6); AppendNewLine(); }
        public void AddLine(string v0, string v1, string v2, string v3, string v4, string v5, string v6, string v7) { TryAppendIndentation(); Append(v0); Append(v1); Append(v2); Append(v3); Append(v4); Append(v5); Append(v6); Append(v7); AppendNewLine(); }

        [StringFormatMethod("formatString")]
        public void AppendLine(string formatString, params object[] args)
        {
            TryAppendIndentation();
            m_StringBuilder.AppendFormat(CultureInfo.InvariantCulture, formatString, args);
            AppendNewLine();
        }

        static readonly char[] LineSeparators = new[] { '\n', '\r'};
        public void AppendLines(string lines)
        {
            if (string.IsNullOrEmpty(lines))
                return;

            int startSearchIndex = 0;
            int indexOfNextBreak = lines.IndexOfAny(LineSeparators);
            while (indexOfNextBreak >= 0)
            {
                AppendLine(lines, startSearchIndex, indexOfNextBreak - startSearchIndex);
                startSearchIndex = indexOfNextBreak + 1;
                indexOfNextBreak = lines.IndexOfAny(LineSeparators, startSearchIndex);
            }

            if (startSearchIndex < lines.Length)
            {
                AppendLine(lines, startSearchIndex, lines.Length - startSearchIndex);
            }
        }

        public void Append(string value)
        {
            m_StringBuilder.Append(value);
        }

        public void Append(string value, int start, int count)
        {
            m_StringBuilder.Append(value, start, count);
        }

        [StringFormatMethod("formatString")]
        public void Append(string formatString, params object[] args)
        {
            m_StringBuilder.AppendFormat(formatString, args);
        }

        public void AppendSpaces(int count)
        {
            m_StringBuilder.Append(' ', count);
        }

        public void TryAppendIndentation()
        {
            if (m_HumanReadable)
            {
                for (var i = 0; i < m_IndentationLevel; i++)
                    m_StringBuilder.Append(k_IndentationString);
            }
        }

        public IDisposable IndentScope()
        {
            m_ScopeStack.Push(ScopeType.Indent);
            IncreaseIndent();
            return this;
        }

        public IDisposable BlockScope()
        {
            AppendLine("{");
            IncreaseIndent();
            m_ScopeStack.Push(ScopeType.Block);
            return this;
        }

        public IDisposable BlockSemicolonScope()
        {
            AppendLine("{");
            IncreaseIndent();
            m_ScopeStack.Push(ScopeType.BlockSemicolon);
            return this;
        }

        public void IncreaseIndent()
        {
            m_IndentationLevel++;
        }

        public void IncreaseIndent(int level)
        {
            for (var i = 0; i < level; i++)
                IncreaseIndent();
        }

        public void DecreaseIndent()
        {
            m_IndentationLevel--;
        }

        public void DecreaseIndent(int level)
        {
            for (var i = 0; i < level; i++)
                DecreaseIndent();
        }

        public void Dispose()
        {
            if (m_ScopeStack.Count == 0)
                return;

            switch (m_ScopeStack.Pop())
            {
                case ScopeType.Indent:
                    DecreaseIndent();
                    break;
                case ScopeType.Block:
                    DecreaseIndent();
                    AppendLine("}");
                    break;
                case ScopeType.BlockSemicolon:
                    DecreaseIndent();
                    AppendLine("};");
                    break;
            }
        }

        public void Concat(ShaderStringBuilder other)
        {
            // First re-add all the mappings from `other`, such that their mappings are transformed.
            foreach (var mapping in other.m_Mappings)
            {
                currentNode = mapping.node;

                // Use `AppendLines` to indent according to the current indentation.
                if (m_HumanReadable)
                {
                    AppendLines(other.ToString(mapping.startIndex, mapping.count));
                }
                else
                {
                    Append(other.ToString(mapping.startIndex, mapping.count));
                }
            }
            currentNode = other.currentNode;
            if (m_HumanReadable)
            {
                AppendLines(other.ToString(other.m_CurrentMapping.startIndex, other.length - other.m_CurrentMapping.startIndex));
            }
            else
            {
                Append(other.ToString(other.m_CurrentMapping.startIndex, other.length - other.m_CurrentMapping.startIndex));
            }
        }

        public void ReplaceInCurrentMapping(string oldValue, string newValue)
        {
            Profiler.BeginSample("ReplaceInCurrentMapping");
            int start = m_CurrentMapping.startIndex;
            int end = m_StringBuilder.Length - start;
            m_StringBuilder.Replace(oldValue, newValue, start, end);
            Profiler.EndSample();
        }

        public void Replace(string oldValue, string newValue, int start, int end)
        {
            m_StringBuilder.Replace(oldValue, newValue, start, end);
        }

        public string ToCodeBlock()
        {
            // Remove new line
            if (m_StringBuilder.Length > 0)
                m_StringBuilder.Length = m_StringBuilder.Length - 1;

            if (m_HumanReadable)
            {
                // Set indentations
                m_StringBuilder.Replace(Environment.NewLine, Environment.NewLine + k_IndentationString);
            }

            return m_StringBuilder.ToString();
        }

        public void ToCodeBlock(int startIndex, int length, bool forceHumanReadable)
        {
            if (forceHumanReadable || m_HumanReadable)
            {
                // Set indentations
                m_StringBuilder.Replace(Environment.NewLine, Environment.NewLine + k_IndentationString, startIndex, length);
            }
        }

        public override string ToString()
        {
            return m_StringBuilder.ToString();
        }

        public string ToString(int startIndex, int length)
        {
            return m_StringBuilder.ToString(startIndex, length);
        }

        internal void Clear()
        {
            m_StringBuilder.Length = 0;
        }

        internal int length
        {
            get { return m_StringBuilder.Length; }
            set { m_StringBuilder.Length = value; }
        }
    }
}