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

namespace ScriptTool
{
    public class Compiler : ICompiler
    {
        private static int[] virtualWidths;
        private static int[] renderWidths;
        private static int[] virtualWidthsSaturn;
        private static int[] renderWidthsSaturn;

        public IEnumerable<IControlCode> ControlCodes { get; set; }
        public Dictionary<string, int> AddressMap { get; set; }
        public Func<byte[], int, bool> ControlCodePredicate { get; set; }

        static Compiler()
        {
            byte[] widths = Asset.ReadAllBytes("m2-widths-main.bin");
            byte[] saturnWidths = Asset.ReadAllBytes("m2-widths-saturn.bin");
            virtualWidths = new int[widths.Length / 2];
            renderWidths = new int[widths.Length / 2];
            virtualWidthsSaturn = new int[saturnWidths.Length / 2];
            renderWidthsSaturn = new int[saturnWidths.Length / 2];

            for (int i = 0; i < widths.Length; i += 2)
            {
                virtualWidths[i / 2] = widths[i];
                renderWidths[i / 2] = widths[i + 1];
                virtualWidthsSaturn[i / 2] = saturnWidths[i];
                renderWidthsSaturn[i / 2] = saturnWidths[i + 1];
            }
        }

        public Compiler(IEnumerable<IControlCode> controlCodes,
            Func<byte[], int, bool> controlCodePredicate)
        {
            ControlCodes = controlCodes;
            ControlCodePredicate = controlCodePredicate;

            AddressMap = new Dictionary<string, int>();
        }

        public static bool IsHexByte(string str)
        {
            try
            {
                Convert.ToByte(str, 16);
                return true;
            }
            catch
            {
                return false;
            }
        }

        private byte GetByte(char c, IDictionary<byte, string> charLookup)
        {
            return charLookup.First(kv => kv.Value[0] == c).Key; // lazy
        }

        public void ScanString(string str, ref int referenceAddress, IDictionary<byte, string> charLookup, bool scanCodesOnly)
        {
            ISet<IControlCode> codes;
            IList<string> references;
            ScanString(str, ref referenceAddress, charLookup, scanCodesOnly, out references, out codes);
        }

        public void ScanString(string str, IDictionary<byte, string> charLookup, bool scanCodesOnly,
            out IList<string> references)
        {
            int temp = 0;
            ISet<IControlCode> codes;
            ScanString(str, ref temp, charLookup, scanCodesOnly, out references, out codes);
        }

        public void ScanString(string str, IDictionary<byte, string> charLookup, bool scanCodesOnly,
            out ISet<IControlCode> codes)
        {
            int temp = 0;
            IList<string> references;
            ScanString(str, ref temp, charLookup, scanCodesOnly, out references, out codes);
        }

        public void ScanString(string str, IDictionary<byte, string> charLookup, bool scanCodesOnly,
            out IList<string> references, out ISet<IControlCode> controlCodes)
        {
            int temp = 0;
            ScanString(str, ref temp, charLookup, scanCodesOnly, out references, out controlCodes);
        }

        public void ScanString(string str, ref int referenceAddress, IDictionary<byte, string> charLookup, bool scanCodesOnly,
            out IList<string> references, out ISet<IControlCode> controlCodes)
        {
            references = new List<string>();
            controlCodes = new HashSet<IControlCode>();

            for (int i = 0; i < str.Length; )
            {
                if (str[i] == '[')
                {
                    if (str.IndexOf(']', i + 1) == -1)
                        throw new Exception("Opening bracket has no matching closing bracket: position " + i);

                    string[] codeStrings = str.Substring(i + 1, str.IndexOf(']', i + 1) - i - 1)
                        .Split(' ');

                    IControlCode code = ControlCodes.FirstOrDefault(c => c.IsMatch(codeStrings));
                    if (!controlCodes.Contains(code))
                    {
                        controlCodes.Add(code);
                    }

                    foreach (var codeString in codeStrings)
                    {
                        if (codeString[0] == '_')
                        {
                            if (codeString[codeString.Length - 1] != '_')
                                throw new Exception("Reference has no closing underscore: position " + i);

                            if (codeString.Length <= 2)
                                throw new Exception("Reference is empty: position " + i);

                            if (!scanCodesOnly)
                                referenceAddress += 4;

                            references.Add(codeString.Substring(1, codeString.Length - 2));
                        }
                        else if (IsHexByte(codeString))
                        {
                            if (!scanCodesOnly)
                                referenceAddress++;
                        }
                        else
                        {
                            throw new Exception(String.Format(
                                "Encountered invalid code string at position {0}: {1}", i, codeString));
                        }
                    }

                    i = str.IndexOf(']', i + 1) + 1;
                }
                else if (str[i] == ']')
                {
                    throw new Exception("Closing bracket has no matching opening bracket: position " + i);
                }
                else if (str[i] == '^')
                {
                    if (str.IndexOf('^', i + 1) == -1)
                        throw new Exception("Label has no matching closing caret: position " + i);

                    string label = str.Substring(i + 1, str.IndexOf('^', i + 1) - i - 1);

                    if (AddressMap.ContainsKey(label))
                        throw new Exception("Label " + label + " already defined: position " + i);

                    if (!scanCodesOnly)
                        AddressMap.Add(label, referenceAddress);

                    i = str.IndexOf('^', i + 1) + 1;
                }
                else
                {
                    if (!(str[i] == '\r') && !(str[i] == '\n'))
                    {
                        if (!scanCodesOnly)
                        {
                            GetByte(str[i], charLookup); // just check if it's valid
                            referenceAddress++;
                        }
                    }
                    i++;
                }
            }
        }

        public void CompileString(string str, IList<byte> buffer, ref int referenceAddress, IDictionary<byte, string> charLookup)
        {
            CompileString(str, buffer, ref referenceAddress, charLookup, -1);
        }

        public void CompileString(string str, IList<byte> buffer, ref int referenceAddress, IDictionary<byte, string> charLookup, int padLength)
        {
            int previousBufferSize = buffer.Count;

            for (int i = 0; i < str.Length; )
            {
                if (str[i] == '[')
                {
                    if (str.IndexOf(']', i + 1) == -1)
                        throw new Exception("Opening bracket has no matching closing bracket: position " + i);

                    string[] codeStrings = str.Substring(i + 1, str.IndexOf(']', i + 1) - i - 1)
                        .Split(' ');

                    // Match the code
                    IControlCode code = ControlCodes.FirstOrDefault(c => c.IsMatch(codeStrings));

                    if (code == null)
                    {
                        // Direct copy
                        for (int j = 0; j < codeStrings.Length; j++)
                        {
                            if (!IsHexByte(codeStrings[j]))
                                throw new Exception("Code string for unrecognized control code block must be a byte literal: position " + i);

                            byte value = byte.Parse(codeStrings[j], System.Globalization.NumberStyles.HexNumber);
                            if (buffer != null)
                                buffer.Add(value);
                            referenceAddress++;
                        }
                    }

                    else
                    {
                        // Validate
                        if (!code.IsValid(codeStrings))
                            throw new Exception("Invalid control code: position " + i);

                        // Parse
                        code.Compile(codeStrings, buffer, ref referenceAddress, AddressMap);
                    }

                    i = str.IndexOf(']', i + 1) + 1;
                }
                else if (str[i] == ']')
                {
                    throw new Exception("Closing bracket has no matching opening bracket: position " + i);
                }
                else if (str[i] == '^')
                {
                    if (str.IndexOf('^', i + 1) == -1)
                        throw new Exception("Label has no matching closing caret: position " + i);

                    i = str.IndexOf('^', i + 1) + 1;
                }
                else
                {
                    if (!(str[i] == '\r') && !(str[i] == '\n'))
                    {
                        byte value = GetByte(str[i], charLookup);

                        if (buffer != null)
                            buffer.Add(value);

                        referenceAddress++;
                    }
                    i++;
                }
            }

            // Pad the remaining bytes
            if (padLength != -1)
            {
                int bytesWritten = buffer.Count - previousBufferSize;

                if (bytesWritten > padLength)
                    throw new Exception("Exceeded pad length: wrote " + bytesWritten +
                        " bytes, but the pad length is " + padLength + " bytes");

                for (int i = bytesWritten; i < padLength; i++)
                {
                    if (buffer != null)
                        buffer.Add(0);

                    referenceAddress++;
                }
            }
        }

        public string StripText(string str)
        {
            if (str == null)
            {
                return null;
            }

            var sb = new StringBuilder();

            for (int i = 0; i < str.Length; )
            {
                if (str[i] == '[')
                {
                    if (str.IndexOf(']', i + 1) == -1)
                        throw new Exception("Opening bracket has no matching closing bracket: position " + i);

                    sb.Append(str.Substring(i, str.IndexOf(']', i + 1) - i + 1));

                    i = str.IndexOf(']', i + 1) + 1;
                }
                else if (str[i] == ']')
                {
                    throw new Exception("Closing bracket has no matching opening bracket: position " + i);
                }
                else if (str[i] == '^')
                {
                    if (str.IndexOf('^', i + 1) == -1)
                        throw new Exception("Label has no matching closing caret: position " + i);

                    sb.Append(str.Substring(i, str.IndexOf('^', i + 1) - i + 1));

                    i = str.IndexOf('^', i + 1) + 1;
                }
                else
                {
                    i++;
                }
            }

            return sb.ToString();
        }

        public IList<string> FormatPreviewM12(string str, out IList<int> widths, IDictionary<byte, string> charLookup)
        {
            var sb = new StringBuilder();
            widths = new List<int>();
            int currentWidth = 0;
            bool useSaturnWidths = false;

            var strings = new List<string>();

            for (int i = 0; i < str.Length; )
            {
                if (str[i] == '[')
                {
                    if (str.IndexOf(']', i + 1) == -1)
                        throw new Exception("Opening bracket has no matching closing bracket: position " + i);

                    string[] codeStrings = str.Substring(i + 1, str.IndexOf(']', i + 1) - i - 1)
                        .Split(' ');

                    M12ControlCode code = (M12ControlCode)ControlCodes.FirstOrDefault(c => c.IsMatch(codeStrings));

                    foreach (var codeString in codeStrings)
                    {
                        if (codeString[0] == '_')
                        {
                            if (codeString[codeString.Length - 1] != '_')
                                throw new Exception("Reference has no closing underscore: position " + i);

                            if (codeString.Length <= 2)
                                throw new Exception("Reference is empty: position " + i);
                        }
                        else if (!IsHexByte(codeString))
                        {
                            throw new Exception(String.Format(
                                "Encountered invalid code string at position {0}: {1}", i, codeString));
                        }
                    }

                    i = str.IndexOf(']', i + 1) + 1;

                    if (code == null)
                    {
                        // Not matched to anything -- check if it's a valid character sequence
                        foreach (var codeString in codeStrings)
                        {
                            if (!IsHexByte(codeString))
                            {
                                sb.Append("[INVALID]");
                            }
                            else
                            {
                                byte b = Convert.ToByte(codeString, 16);
                                if (!charLookup.ContainsKey(b))
                                {
                                    sb.Append("[INVALID]");
                                }
                                else
                                {
                                    sb.Append(charLookup[b]);
                                    currentWidth += virtualWidths[b - 0x50];
                                }
                            }
                        }
                    }
                    else
                    {
                        switch (code.Identifier)
                        {
                            case 0xC:
                            case 0xD:
                            case 0xE:
                            case 0xF:
                            case 0x10:
                            case 0x11:
                            case 0x12:
                            case 0x15:
                            case 0x2D:
                            case 0x9F:
                            case 0xAD:
                                // Name code
                                sb.Append("[NAME]");
                                currentWidth += 60;
                                break;

                            case 0x1A:
                                // Name (60/96) or item (80/??) or number (36/72).
                                sb.Append("[NAME]");
                                currentWidth += 80;
                                break;

                            case 0x0:
                            case 0x1:
                            case 0x2:
                            case 0x3:
                                // Line break
                                strings.Add(sb.ToString());
                                sb.Clear();
                                widths.Add(currentWidth);
                                currentWidth = 0;
                                break;

                            case 0x20:
                                sb.Append("[SMAAAASH!!]");
                                currentWidth += 72;
                                break;

                            case 0x21:
                                sb.Append("[YOU WON!]");
                                currentWidth += 72;
                                break;

                            case 0x23:
                            case 0x63:
                            case 0xB7:
                                sb.Append("[MONEY]");
                                currentWidth += 36;
                                break;

                            case 0x24:
                            case 0x25:
                            case 0x26:
                            case 0x27:
                            case 0x28:
                            case 0x29:
                            case 0x2A:
                            case 0x2B:
                                sb.Append("[STAT]");
                                currentWidth += 18;
                                break;

                            case 0x1F:
                                sb.Append("_");
                                currentWidth += 10;
                                break;

                            case 0x7B:
                                useSaturnWidths = true;
                                break;

                            case 0x7C:
                                useSaturnWidths = false;
                                break;
                        }
                    }
                }
                else if (str[i] == ']')
                {
                    throw new Exception("Closing bracket has no matching opening bracket: position " + i);
                }
                else if (str[i] == '^')
                {
                    if (str.IndexOf('^', i + 1) == -1)
                        throw new Exception("Label has no matching closing caret: position " + i);

                    string label = str.Substring(i + 1, str.IndexOf('^', i + 1) - i - 1);

                    i = str.IndexOf('^', i + 1) + 1;
                }
                else
                {
                    if (!(str[i] == '\r') && !(str[i] == '\n'))
                    {
                        sb.Append(str[i]);
                        int v = GetByte(str[i], charLookup) - 0x50;
                        currentWidth += useSaturnWidths ? virtualWidthsSaturn[v] : virtualWidths[v];
                    }
                    i++;
                }
            }

            if (sb.Length > 0)
            {
                strings.Add(sb.ToString());
                widths.Add(currentWidth);
            }

            return strings;
        }

        public IControlCode GetLastControlCode(string str)
        {
            if (str.Length < 2)
                return null;

            if (!(str[str.Length - 1] == ']'))
                return null;

            int lastOpenBracket = str.LastIndexOf('[');

            if (lastOpenBracket < str.LastIndexOf(']', str.Length - 2))
                return null;

            var codeStrings = str.Substring(lastOpenBracket + 1, str.Length - lastOpenBracket - 2)
                .Split(' ');

            return ControlCodes.FirstOrDefault(c => c.IsMatch(codeStrings));
        }
    }
}
