using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using UnityEditor.ShaderGraph.Internal;
using UnityEngine.Profiling;

namespace UnityEditor.ShaderGraph
{
    static class ShaderSpliceUtil
    {
        private static char[] channelNames =
        { 'x', 'y', 'z', 'w' };


        private static char[] whitespace =
        { ' ', '\t', '\r', '\n', '\f'};
        public static string GetChannelSwizzle(int firstChannel, int channelCount)
        {
            System.Text.StringBuilder result = new System.Text.StringBuilder();
            int lastChannel = System.Math.Min(firstChannel + channelCount - 1, 4);
            for (int index = firstChannel; index <= lastChannel; index++)
            {
                result.Append(channelNames[index]);
            }
            return result.ToString();
        }

        // returns the offset of the first non-whitespace character, in the range [start, end] inclusive ... will return end if none found
        private static int SkipWhitespace(string str, int start, int end)
        {
            int index = start;
            while (index < end)
            {
                char c = str[index];
                bool containsWhiteSpace = false;
                foreach (var whiteSpaceChar in whitespace)
                {
                    if (c == whiteSpaceChar)
                    {
                        containsWhiteSpace = true;
                        break;
                    }
                }
                if (!containsWhiteSpace)
                {
                    break;
                }
                index++;
            }
            return index;
        }

        public class TemplatePreprocessor
        {
            // inputs
            ActiveFields activeFields;
            Dictionary<string, string> namedFragments;
            string[] templatePaths;
            bool isDebug;

            // intermediates
            HashSet<string> includedFiles;
            Dictionary<string, string[]> includeCache; // this cache is reused across passes

            // outputs
            ShaderStringBuilder result;
            AssetCollection assetCollection;

            public TemplatePreprocessor(ActiveFields activeFields, Dictionary<string, string> namedFragments, bool isDebug, string[] templatePaths, AssetCollection assetCollection, bool humanReadable, Dictionary<string, string[]> includeCache, ShaderStringBuilder outShaderCodeResult = null)
            {
                this.activeFields = activeFields;
                this.namedFragments = namedFragments;
                this.isDebug = isDebug;
                this.templatePaths = templatePaths;
                this.assetCollection = assetCollection;
                this.result = outShaderCodeResult ?? new ShaderStringBuilder(humanReadable: humanReadable);
                includedFiles = new HashSet<string>();
                this.includeCache = includeCache;
            }

            public ShaderStringBuilder GetShaderCode()
            {
                return result;
            }

            public void ProcessTemplateFile(string filePath)
            {
                if (!includedFiles.Contains(filePath) && (includeCache.ContainsKey(filePath) || File.Exists(filePath)))
                {
                    includedFiles.Add(filePath);

                    if (assetCollection != null)
                    {
                        UnityEngine.GUID guid = AssetDatabase.GUIDFromAssetPath(filePath);
                        if (!guid.Empty())
                            assetCollection.AddAssetDependency(guid, AssetCollection.Flags.SourceDependency);
                    }

                    string[] templateLines;
                    if (!includeCache.TryGetValue(filePath, out templateLines))
                    {
                        templateLines = File.ReadAllLines(filePath);
                        includeCache.TryAdd(filePath, templateLines);
                    }                    foreach (string line in templateLines)
                    {
                        ProcessTemplateLine(line, 0, line.Length);
                    }
                }
            }

            private struct Token
            {
                public string s;
                public int start;
                public int end;

                public Token(string s, int start, int end)
                {
                    this.s = s;
                    this.start = start;
                    this.end = end;
                }

                public static Token Invalid()
                {
                    return new Token(null, 0, 0);
                }

                public bool IsValid()
                {
                    return (s != null);
                }

                public bool Is(string other)
                {
                    int len = end - start;
                    return (other.Length == len) && (0 == string.CompareOrdinal(s, start, other, 0, len));
                }

                public string GetString()
                {
                    int len = end - start;
                    if (len > 0)
                    {
                        return s.Substring(start, end - start);
                    }
                    return null;
                }
            }

            public void ProcessTemplateLine(string line, int start, int end)
            {
                bool appendEndln = true;

                int cur = start;
                while (cur < end)
                {
                    // find an escape code '$'
                    int dollar = line.IndexOf('$', cur, end - cur);
                    if (dollar < 0)
                    {
                        // no escape code found in the remaining code -- just append the rest verbatim
                        AppendSubstring(line, cur, true, end, false);
                        break;
                    }
                    else
                    {
                        // found $ escape sequence
                        Token command = ParseIdentifier(line, dollar + 1, end);
                        if (!command.IsValid())
                        {
                            Error("ERROR: $ must be followed by a command string (if, splice, or include)", line, dollar + 1);
                            break;
                        }
                        else
                        {
                            if (command.Is("include"))
                            {
                                ProcessIncludeCommand(command, end);
                                appendEndln = false;
                                break;      // include command always ignores the rest of the line, error or not
                            }
                            else if (command.Is("splice"))
                            {
                                if (!ProcessSpliceCommand(command, end, ref cur))
                                {
                                    // error, skip the rest of the line
                                    break;
                                }
                            }
                            else
                            {
                                // let's see if it is a predicate
                                Token predicate = ParseUntil(line, dollar + 1, end, ':');
                                if (!predicate.IsValid())
                                {
                                    Error("ERROR: unrecognized command: " + command.GetString(), line, command.start);
                                    break;
                                }
                                else
                                {
                                    if (!ProcessPredicate(predicate, end, ref cur, ref appendEndln))
                                    {
                                        break;  // skip the rest of the line
                                    }
                                }
                            }
                        }
                    }
                }

                if (appendEndln)
                {
                    result.AppendNewLine();
                }
            }

            private void ProcessIncludeCommand(Token includeCommand, int lineEnd)
            {
                if (Expect(includeCommand.s, includeCommand.end, '('))
                {
                    Token param = ParseString(includeCommand.s, includeCommand.end + 1, lineEnd);

                    if (!param.IsValid())
                    {
                        Error("ERROR: $include expected a string file path parameter", includeCommand.s, includeCommand.end + 1);
                    }
                    else
                    {
                        bool found = false;
                        string includeLocation = null;

                        // Use reverse order in the array, higher number element have higher priority in case $include exist in several directories
                        for (int i = templatePaths.Length - 1; i >= 0; i--)
                        {
                            string templatePath = templatePaths[i];
                            includeLocation = Path.Combine(templatePath, param.GetString());
                            bool cacheHit = includeCache.ContainsKey(includeLocation);
                            if (cacheHit ||  File.Exists(includeLocation))                            {
                                found = true;
                                break;
                            }
                        }

                        if (!found)
                        {
                            string errorStr = "ERROR: $include cannot find file : " + param.GetString() + ". Looked into:\n";

                            foreach (string templatePath in templatePaths)
                            {
                                errorStr += "// " + templatePath + "\n";
                            }

                            Error(errorStr, includeCommand.s, param.start);
                        }
                        else
                        {
                            int oldLength = result.length;
                            // Wrap in debug mode
                            if (isDebug)
                            {
                                result.AppendLine("//-------------------------------------------------------------------------------------");
                                result.AppendLine("// TEMPLATE INCLUDE : " + param.GetString());
                                result.AppendLine("//-------------------------------------------------------------------------------------");
                                result.AppendNewLine();
                            }

                            // Recursively process templates
                            ProcessTemplateFile(includeLocation);

                            // Wrap in debug mode
                            if (isDebug)
                            {
                                result.AppendNewLine();
                                result.AppendLine("//-------------------------------------------------------------------------------------");
                                result.AppendLine("// END TEMPLATE INCLUDE : " + param.GetString());
                                result.AppendLine("//-------------------------------------------------------------------------------------");
                            }

                            result.ToCodeBlock(oldLength, result.length - oldLength, true);
                        }
                    }
                }
            }

            private bool ProcessSpliceCommand(Token spliceCommand, int lineEnd, ref int cur)
            {
                if (!Expect(spliceCommand.s, spliceCommand.end, '('))
                {
                    return false;
                }
                else
                {
                    Token param = ParseUntil(spliceCommand.s, spliceCommand.end + 1, lineEnd, ')');
                    if (!param.IsValid())
                    {
                        Error("ERROR: splice command is missing a ')'", spliceCommand.s, spliceCommand.start);
                        return false;
                    }
                    else
                    {
                        // append everything before the beginning of the escape sequence
                        AppendSubstring(spliceCommand.s, cur, true, spliceCommand.start - 1, false);

                        // find the named fragment
                        string name = param.GetString();     // unfortunately this allocates a new string
                        string fragment;
                        if ((namedFragments != null) && namedFragments.TryGetValue(name, out fragment))
                        {
                            // splice the fragment
                            result.Append(fragment);
                        }
                        else
                        {
                            // no named fragment found
                            result.Append("/* WARNING: $splice Could not find named fragment '{0}' */", name);
                        }

                        // advance to just after the ')' and continue parsing
                        cur = param.end + 1;
                    }
                }
                return true;
            }

            private bool ContainsCommand(string source, int begin, int end)
            {
                for (int i = begin; i < end && i < source.Length; ++i)
                {
                    if (source[i] == '$')
                        return true;
                }
                return false;
            }

            private bool ProcessPredicate(Token predicate, int endLine, ref int cur, ref bool appendEndln)
            {
                // eval if(param)
                var fieldName = predicate.GetString();
                var nonwhitespace = SkipWhitespace(predicate.s, predicate.end + 1, endLine);

                if (!fieldName.StartsWith("features", StringComparison.Ordinal) && activeFields.permutationCount > 0)
                {
                    var passedPermutations = activeFields.allPermutations.instances.Where(i => i.Contains(fieldName)).ToList();
                    if (passedPermutations.Count > 0)
                    {
                        var ifdefs = KeywordUtil.GetKeywordPermutationSetConditional(
                            passedPermutations.Select(i => i.permutationIndex).ToList()
                        );
                        result.AppendLine(ifdefs);

                        //Append the rest of the line
                        var content = predicate.s;
                        if (ContainsCommand(content, nonwhitespace, endLine))
                        {
                            content = content.Substring(nonwhitespace, endLine - nonwhitespace);
                            ProcessTemplateLine(content, 0, content.Length);
                        }
                        else
                        {
                            AppendSubstring(predicate.s, nonwhitespace, true, endLine, false);
                        }

                        result.AppendNewLine();
                        result.AppendLine("#endif");
                        return false;
                    }
                    else
                    {
                        appendEndln = false; //if line isn't active, remove whitespace
                    }
                    return false;
                }
                else
                {
                    // eval if(param)
                    bool contains = activeFields.baseInstance.Contains(fieldName);
                    if (contains)
                    {
                        // predicate is active
                        // append everything before the beginning of the escape sequence
                        AppendSubstring(predicate.s, cur, true, predicate.start - 1, false);

                        // continue parsing the rest of the line, starting with the first nonwhitespace character
                        cur = nonwhitespace;
                        return true;
                    }
                    else
                    {
                        // predicate is not active
                        if (isDebug)
                        {
                            // append everything before the beginning of the escape sequence
                            AppendSubstring(predicate.s, cur, true, predicate.start - 1, false);
                            // append the rest of the line, commented out
                            result.Append("// ");
                            AppendSubstring(predicate.s, nonwhitespace, true, endLine, false);
                        }
                        else
                        {
                            // don't append anything
                            appendEndln = false;
                        }
                        return false;
                    }
                }
            }

            private static bool IsLetter(char c)
            {
                return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z');
            }

            private static bool IsLetterOrDigit(char c)
            {
                return IsLetter(c) || Char.IsDigit(c);
            }

            private Token ParseIdentifier(string code, int start, int end)
            {
                if (start < end)
                {
                    char c = code[start];
                    if (IsLetter(c) || (c == '_'))
                    {
                        int cur = start + 1;
                        while (cur < end)
                        {
                            c = code[cur];
                            if (!(IsLetterOrDigit(c) || (c == '_')))
                                break;
                            cur++;
                        }
                        return new Token(code, start, cur);
                    }
                }
                return Token.Invalid();
            }

            private Token ParseString(string line, int start, int end)
            {
                if (Expect(line, start, '"'))
                {
                    return ParseUntil(line, start + 1, end, '"');
                }
                return Token.Invalid();
            }

            private Token ParseUntil(string line, int start, int end, char endChar)
            {
                int cur = start;
                while (cur < end)
                {
                    if (line[cur] == endChar)
                    {
                        return new Token(line, start, cur);
                    }
                    cur++;
                }
                return Token.Invalid();
            }

            private bool Expect(string line, int location, char expected)
            {
                if ((location < line.Length) && (line[location] == expected))
                {
                    return true;
                }
                Error("Expected '" + expected + "'", line, location);
                return false;
            }

            private void Error(string error, string line, int location)
            {
                // append the line for context
                result.Append("\n");
                result.Append("// ");
                AppendSubstring(line, 0, true, line.Length, false);
                result.Append("\n");

                // append the location marker, and error description
                result.Append("// ");
                result.AppendSpaces(location);
                result.Append("^ ");
                result.Append(error);
                result.Append("\n");
            }

            // an easier to use version of substring Append() -- explicit inclusion on each end, and checks for positive length
            private void AppendSubstring(string str, int start, bool includeStart, int end, bool includeEnd)
            {
                if (!includeStart)
                {
                    start++;
                }
                if (!includeEnd)
                {
                    end--;
                }
                int count = end - start + 1;
                if (count > 0)
                {
                    result.Append(str, start, count);
                }
            }
        }
    }
}