using UnityEngine;
using UnityEngine.UI;
using System.Collections.Generic;

#if UNITY_EDITOR
using UnityEditor;
#endif


public delegate bool ConsoleCommandExecute(string[] args, ref string output);



public class DebugConsole : MonoBehaviour
{
    private class ConsoleCommand
    {
        public ConsoleCommandExecute execute = null;
        public int expectedArgumentCount = -1;
        public string format = null;

        private ConsoleCommand() { }

        public ConsoleCommand(ConsoleCommandExecute _execute, int _expectedArgumentCount = -1, string _format = null)
        {
            execute = _execute;
            expectedArgumentCount = _expectedArgumentCount;
            format = _format;
        }
    }



    private const int maxHistoryLogLines = 25;

    private static DebugConsole _instance = null;

    private GameObject panel = null;
    private InputField input = null;
    private Text history = null;

    private List<string> inputHistory = new List<string>();
    private int curInputHistoryIndex = -1;

    private string lastTabText = null;
    private string lastTabCommand = null;

    private string lastAutoInput = null;
    private int historyLineCount = 0;

    private Dictionary<string, ConsoleCommand> consoleCommands = new Dictionary<string, ConsoleCommand>();



    private static DebugConsole Instance
    {
        get
        {
#if UNITY_EDITOR
            if (_instance == null)
            {
                string debugConsoleAssetPath = null;
                string[] possibleAssets = AssetDatabase.FindAssets("DebugConsole t:GameObject");
                if (possibleAssets.Length > 0)
                {
                    debugConsoleAssetPath = AssetDatabase.GUIDToAssetPath(possibleAssets[0]);
                }

                if (debugConsoleAssetPath != null)
                {
                    GameObject debugConsolePrefab = AssetDatabase.LoadAssetAtPath<GameObject>(debugConsoleAssetPath);
                    if (debugConsolePrefab != null)
                    {
                        GameObject newDebugConsole = GameObject.Instantiate(debugConsolePrefab);
                        if (newDebugConsole != null)
                        {
                            _instance = newDebugConsole.GetComponent<DebugConsole>();
                            if (_instance == null)
                            {
                                _instance = newDebugConsole.AddComponent<DebugConsole>();
                            }
                        }
                    }
                }
            }
#endif
            return _instance;
        }
    }

    public bool IsOpen
    {
        get
        {
            return panel != null && panel.activeInHierarchy;
        }
    }



    void Awake()
    {
#if UNITY_EDITOR
        if (_instance != null)
        {
            Destroy(gameObject);
            return;
        }

        _instance = this;
        DontDestroyOnLoad(gameObject);
#else
        Destroy(gameObject);
#endif
    }

    void Start()
    {
#if UNITY_EDITOR
        Transform panelChildTransform = transform.FindChild("Panel");
        panel = panelChildTransform != null ? panelChildTransform.gameObject : null;
        if (panel != null)
        {
            Transform inputChildTransform = panel.transform.FindChild("Input");
            input = inputChildTransform != null ? inputChildTransform.GetComponent<InputField>() : null;
            if (input != null)
            {
                input.onValueChanged.AddListener(this.OnInputChanged);
                input.onEndEdit.AddListener(this.OnInputCommand);
            }

            Transform historyChildTransform = panel.transform.FindChild("History");
            history = historyChildTransform != null ? historyChildTransform.GetComponentInChildren<Text>() : null;
        }
#endif
    }

    void Update()
    {
#if UNITY_EDITOR
        if (panel == null)
        {
            return;
        }

        // toggle the debug console if CTRL+` is pressed
        if (Input.GetKeyDown(KeyCode.BackQuote) && (Input.GetKey(KeyCode.LeftControl) || Input.GetKey(KeyCode.RightControl)))
        {
            panel.SetActive(!IsOpen);

            if (IsOpen && input != null)
            {
                input.Select();
                input.ActivateInputField();
            }
        }

        // don't update any further if the debug console isn't active
        if (!IsOpen)
        {
            return;
        }

        // if the input has focus, update any auto-complete helpers
        if (input != null && input.isFocused)
        {
            // up == backwards in input history
            string newInputText = null;
            if (Input.GetKeyDown(KeyCode.UpArrow))
            {
                if (curInputHistoryIndex < 0)
                {
                    curInputHistoryIndex = inputHistory.Count - 1;
                    newInputText = inputHistory[curInputHistoryIndex];
                }
                else if (curInputHistoryIndex > 0)
                {
                    curInputHistoryIndex--;
                    newInputText = inputHistory[curInputHistoryIndex];
                }
            }
            // down == forwards in input history
            else if (Input.GetKeyDown(KeyCode.DownArrow))
            {
                if (curInputHistoryIndex >= 0 && curInputHistoryIndex < inputHistory.Count - 1)
                {
                    curInputHistoryIndex++;
                    newInputText = inputHistory[curInputHistoryIndex];
                }
            }
            // tab == cycle through messages that start with the current input text
            else if (Input.GetKeyDown(KeyCode.Tab))
            {
                string inputText = input.text.Trim();
                if (!inputText.Contains(" "))
                {
                    if (lastTabText == null)
                    {
                        lastTabText = inputText;
                    }

                    bool foundLastCommand = (lastTabCommand == null);
                    foreach (KeyValuePair<string, ConsoleCommand> kvp in consoleCommands)
                    {
                        if (foundLastCommand)
                        {
                            if (kvp.Key.StartsWith(lastTabText))
                            {
                                newInputText = kvp.Key;
                                lastTabCommand = newInputText;
                                break;
                            }
                        }
                        else if (kvp.Key == lastTabCommand)
                        {
                            foundLastCommand = true;
                        }
                    }

                    if (newInputText == null)
                    {
                        newInputText = lastTabText;
                        lastTabText = null;
                        lastTabCommand = null;
                    }
                }
            }

            // update the debug console input text if needed
            if (newInputText != null)
            {
                lastAutoInput = newInputText;

                input.text = newInputText;
                input.Select();
                input.ActivateInputField();
                input.caretPosition = input.text.Length;
            }
            else if (Input.GetKeyDown(KeyCode.UpArrow) || Input.GetKeyDown(KeyCode.DownArrow))
            {
                input.caretPosition = input.text.Length;
            }
        }
#endif
    }

    private void OnInputChanged(string text)
    {
#if UNITY_EDITOR
        if (input == null)
        {
            return;
        }

        // when the text changes by any means other than a tab auto-complete,
        // clear out the tab auto-complete tracking
        if (input.text != lastAutoInput)
        {
            lastTabText = null;
            lastTabCommand = null;
            curInputHistoryIndex = -1;
        }
#endif
    }

    private void OnInputCommand(string text)
    {
#if UNITY_EDITOR
        if (!IsOpen || input == null)
        {
            return;
        }

        // grab the input text and clear it out
        string inputText = input.text.Trim();

        input.text = "";
        input.Select();
        input.ActivateInputField();

        if (inputText.Length <= 0)
        {
            return;
        }

        // add the input to the history
        inputHistory.Add(inputText);
        if (inputHistory.Count > 10)
        {
            inputHistory.RemoveAt(0);
        }
        curInputHistoryIndex = -1;

        // parse all arguments, including quoted text
        int curChar = 0;
        int argStartChar = -1;
        bool bQuoted = false;

        List<string> args = new List<string>();
        while (curChar < inputText.Length)
        {
            if (!System.Char.IsLetterOrDigit(inputText[curChar]) &&
                inputText[curChar] != '"' &&
                inputText[curChar] != ' ' &&
                inputText[curChar] != '_' &&
                inputText[curChar] != '-' &&
                inputText[curChar] != '\'')
            {
                Log("Invalid console character, '" + inputText[curChar] + "'.", false);
                return;
            }

            if (argStartChar < 0)
            {
                if (inputText[curChar] != ' ')
                {
                    argStartChar = curChar;
                    bQuoted = (inputText[curChar] == '"');
                }
            }
            else
            {
                if (bQuoted)
                {
                    if (inputText[curChar] == '"')
                    {
                        int quotedStart = argStartChar + 1;

                        argStartChar = -1;
                        bQuoted = false;

                        if (curChar <= quotedStart)
                        {
                            continue;
                        }

                        if (curChar + 1 < inputText.Length)
                        {
                            if (inputText[curChar + 1] != ' ')
                            {
                                Log("Invalid end quote, space expected.", false);
                                return;
                            }
                        }

                        string quotedArg = inputText.Substring(quotedStart, curChar - quotedStart);
                        args.Add(quotedArg);
                    }
                }
                else
                {
                    if (inputText[curChar] == '"')
                    {
                        Log("Invalid start quote, space expected.", false);
                        return;
                    }

                    if (inputText[curChar] == ' ')
                    {
                        string arg = inputText.Substring(argStartChar, curChar - argStartChar);
                        args.Add(arg);

                        argStartChar = -1;
                    }
                }
            }

            curChar++;
        }

        if (argStartChar >= 0)
        {
            if (bQuoted)
            {
                Log("Invalid start quote, end quote expected.", false);
                return;
            }

            string arg = inputText.Substring(argStartChar);
            args.Add(arg);
        }

        if (args.Count <= 0)
        {
            return;
        }

        // validate the console command
        string commandName = args[0].ToLower();
        if (!consoleCommands.ContainsKey(commandName))
        {
            Log("Invalid command, '" + commandName + "'.", false);
            return;
        }

        ConsoleCommand command = consoleCommands[commandName];
        if (command.expectedArgumentCount >= 0 && args.Count - 1 < command.expectedArgumentCount)
        {
            string error = "Invalid arguments for command '" + commandName + "'.";
            if (command.format != null && command.format.Length > 0)
            {
                error += "  Expected format: '" + command.format + "'.";
            }

            Log(error, false);
            return;
        }

        // execute the console command
        args.RemoveAt(0);

        string[] commandArgs = null;
        if (args.Count > 0)
        {
            commandArgs = new string[args.Count];
            args.CopyTo(commandArgs);
        }

        string output = "";
        bool success = false;
        try
        {
            success = command.execute(commandArgs, ref output);
        }
        catch (System.Exception)
        {
        }

        string executeOutput = "Executed command '" + inputText + "': ";
        if (success)
        {
            executeOutput += "success";
        }
        else
        {
            executeOutput += "failure";
        }
        Log(executeOutput);

        if (output.Length > 0)
        {
            Log("    -Output: " + output);
        }
#endif
    }

    private int CountNewLines(string text)
    {
        int newLineCount = 0;

        int curIndex = 0;
        while (curIndex < text.Length)
        {
            curIndex = text.IndexOf('\n', curIndex);
            if (curIndex < 0)
            {
                break;
            }

            newLineCount++;
            while (System.Environment.NewLine.Contains(text[curIndex].ToString()))
            {
                curIndex++;
            }
        }

        return newLineCount;
    }

    public static void Log(string log, bool echoToOutput = true)
    {
        if (echoToOutput)
        {
            Debug.Log(log);
        }

#if UNITY_EDITOR
        if (Instance.history == null)
        {
            return;
        }

        // cap the number of new lines
        Instance.historyLineCount += Instance.CountNewLines(log) + 1;
        if (Instance.historyLineCount > maxHistoryLogLines)
        {
            int newLinesToRemove = Instance.historyLineCount - maxHistoryLogLines;

            int newHistoryStartIndex = 0;
            while (newLinesToRemove > 0)
            {
                newHistoryStartIndex = Instance.history.text.IndexOf('\n', newHistoryStartIndex);
                if (newHistoryStartIndex < 0)
                {
                    newHistoryStartIndex = 0;
                    break;
                }

                while (newHistoryStartIndex < Instance.history.text.Length &&
                       System.Environment.NewLine.Contains(Instance.history.text[newHistoryStartIndex].ToString()))
                {
                    newHistoryStartIndex++;
                }
                newLinesToRemove--;
                Instance.historyLineCount--;
            }

            if (newHistoryStartIndex > 0)
            {
                string newHistoryText = Instance.history.text.Substring(newHistoryStartIndex);
                Instance.history.text = newHistoryText;
            }
        }

        Instance.history.text += log + System.Environment.NewLine;
#endif
    }

    public static void AddConsoleCommand(string commandName, ConsoleCommandExecute func, int expectedArgumentCount = -1, string format = null)
    {
#if UNITY_EDITOR
        string commandNameLower = commandName.Trim().ToLower();
        if (commandNameLower.Contains(" "))
        {
            return;
        }

        if (Instance.consoleCommands.ContainsKey(commandNameLower))
        {
            return;
        }

        Instance.consoleCommands[commandNameLower] = new ConsoleCommand(func, expectedArgumentCount, format);
#endif
    }

    public static void RemoveConsoleCommand(ConsoleCommandExecute func)
    {
#if UNITY_EDITOR
        List<string> commandsToRemove = new List<string>();
        foreach (KeyValuePair<string, ConsoleCommand> kvp in Instance.consoleCommands)
        {
            if (kvp.Value.execute == func)
            {
                commandsToRemove.Add(kvp.Key);
            }
        }

        foreach (string commandName in commandsToRemove)
        {
            Instance.consoleCommands.Remove(commandName);
        }
#endif
    }
}
