using UnityEngine;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Text;
using System.Text.RegularExpressions;

namespace Everyplay.XCodeEditor
{
public class PBXParser
{
    public const string PBX_HEADER_TOKEN = "// !$*UTF8*$!\n";
    public const char WHITESPACE_SPACE = ' ';
    public const char WHITESPACE_TAB = '\t';
    public const char WHITESPACE_NEWLINE = '\n';
    public const char WHITESPACE_CARRIAGE_RETURN = '\r';
    public const char ARRAY_BEGIN_TOKEN = '(';
    public const char ARRAY_END_TOKEN = ')';
    public const char ARRAY_ITEM_DELIMITER_TOKEN = ',';
    public const char DICTIONARY_BEGIN_TOKEN = '{';
    public const char DICTIONARY_END_TOKEN = '}';
    public const char DICTIONARY_ASSIGN_TOKEN = '=';
    public const char DICTIONARY_ITEM_DELIMITER_TOKEN = ';';
    public const char QUOTEDSTRING_BEGIN_TOKEN = '"';
    public const char QUOTEDSTRING_END_TOKEN = '"';
    public const char QUOTEDSTRING_ESCAPE_TOKEN = '\\';
    public const char END_OF_FILE = (char) 0x1A;
    public const string COMMENT_BEGIN_TOKEN = "/*";
    public const string COMMENT_END_TOKEN = "*/";
    public const string COMMENT_LINE_TOKEN = "//";
    private const int BUILDER_CAPACITY = 20000;
    private char[] data;
    private int index;
    private int indent;

    public PBXDictionary Decode(string data)
    {
        if (!data.StartsWith(PBX_HEADER_TOKEN.TrimEnd('\r', '\n')))
        {
            Debug.Log("Wrong file format.");
            return null;
        }

        data = data.Substring(13);
        this.data = data.ToCharArray();
        index = 0;
        return (PBXDictionary) ParseValue();
    }

    public string Encode(PBXDictionary pbxData)
    {
        indent = 0;

        StringBuilder builder = new StringBuilder(PBX_HEADER_TOKEN, BUILDER_CAPACITY);
        bool success = SerializeValue(pbxData, builder);

        return (success ? builder.ToString() : null);
    }

    #region Move

    private char NextToken()
    {
        SkipWhitespaces();
        return StepForeward();
    }

    private string Peek(int step = 1)
    {
        string sneak = string.Empty;
        for (int i = 1; i <= step; i++)
        {
            if (data.Length - 1 < index + i)
            {
                break;
            }
            sneak += data[index + i];
        }
        return sneak;
    }

    private bool SkipWhitespaces()
    {
        bool whitespace = false;
        while (Regex.IsMatch(StepForeward().ToString(), @"\s"))
        {
            whitespace = true;
        }

        StepBackward();

        if (SkipComments())
        {
            whitespace = true;
            SkipWhitespaces();
        }

        return whitespace;
    }

    private bool SkipComments()
    {
        string s = string.Empty;
        string tag = Peek(2);
        switch (tag)
        {
        case COMMENT_BEGIN_TOKEN:
        {
            while (Peek(2).CompareTo(COMMENT_END_TOKEN) != 0)
            {
                s += StepForeward();
            }
            s += StepForeward(2);
            break;
        }
        case COMMENT_LINE_TOKEN:
        {
            while (!Regex.IsMatch(StepForeward().ToString(), @"\n"))
            {
                continue;
            }

            break;
        }
        default:
            return false;
        }
        return true;
    }

    private char StepForeward(int step = 1)
    {
        index = Math.Min(data.Length, index + step);
        return data[index];
    }

    private char StepBackward(int step = 1)
    {
        index = Math.Max(0, index - step);
        return data[index];
    }

    #endregion
    #region Parse

    private object ParseValue()
    {
        switch (NextToken())
        {
        case END_OF_FILE:
            Debug.Log("End of file");
            return null;
        case DICTIONARY_BEGIN_TOKEN:
            return ParseDictionary();
        case ARRAY_BEGIN_TOKEN:
            return ParseArray();
        case QUOTEDSTRING_BEGIN_TOKEN:
            return ParseString();
        default:
            StepBackward();
            return ParseEntity();
        }
    }

    private PBXDictionary ParseDictionary()
    {
        SkipWhitespaces();
        PBXDictionary dictionary = new PBXDictionary();
        string keyString = string.Empty;
        object valueObject = null;

        bool complete = false;
        while (!complete)
        {
            switch (NextToken())
            {
            case END_OF_FILE:
                Debug.Log("Error: reached end of file inside a dictionary: " + index);
                complete = true;
                break;

            case DICTIONARY_ITEM_DELIMITER_TOKEN:
                keyString = string.Empty;
                valueObject = null;
                break;

            case DICTIONARY_END_TOKEN:
                keyString = string.Empty;
                valueObject = null;
                complete = true;
                break;

            case DICTIONARY_ASSIGN_TOKEN:
                valueObject = ParseValue();
                dictionary.Add(keyString, valueObject);
                break;

            default:
                StepBackward();
                keyString = ParseValue() as string;
                break;
            }
        }
        return dictionary;
    }

    private PBXList ParseArray()
    {
        PBXList list = new PBXList();
        bool complete = false;
        while (!complete)
        {
            switch (NextToken())
            {
            case END_OF_FILE:
                Debug.Log("Error: Reached end of file inside a list: " + list);
                complete = true;
                break;
            case ARRAY_END_TOKEN:
                complete = true;
                break;
            case ARRAY_ITEM_DELIMITER_TOKEN:
                break;
            default:
                StepBackward();
                list.Add(ParseValue());
                break;
            }
        }
        return list;
    }

    private object ParseString()
    {
        string s = string.Empty;
        s += "\"";
        char c = StepForeward();
        while (c != QUOTEDSTRING_END_TOKEN)
        {
            s += c;

            if (c == QUOTEDSTRING_ESCAPE_TOKEN)
            {
                s += StepForeward();
            }

            c = StepForeward();
        }
        s += "\"";
        return s;
    }

    //there has got to be a better way to do this
    private string GetDataSubstring(int begin, int length)
    {
        string res = string.Empty;


        for (int i = begin; i < begin + length && i < data.Length; i++)
        {
            res += data[i];
        }
        return res;
    }

    private int CountWhitespace(int pos)
    {
        int i = 0;
        for (int currPos = pos; currPos < data.Length && Regex.IsMatch(GetDataSubstring(currPos, 1), @"[;,\s=]"); i++, currPos++)
        {
        }
        return i;
    }

    private string ParseCommentFollowingWhitespace()
    {
        int currIdx = index + 1;
        int whitespaceLength = CountWhitespace(currIdx);
        currIdx += whitespaceLength;

        if (currIdx + 1 >= data.Length)
        {
            return "";
        }

        if (data[currIdx] == '/' && data[currIdx + 1] == '*')
        {
            while (!GetDataSubstring(currIdx, 2).Equals(COMMENT_END_TOKEN))
            {
                if (currIdx >= data.Length)
                {
                    Debug.LogError("Unterminated comment found in .pbxproj file.  Bad things are probably going to start happening");
                    return "";
                }

                currIdx++;
            }

            return GetDataSubstring(index + 1, (currIdx - index + 1));
        }
        else
        {
            return "";
        }
    }

    private object ParseEntity()
    {
        string word = string.Empty;

        while (!Regex.IsMatch(Peek(), @"[;,\s=]"))
        {
            word += StepForeward();
        }

        string comment = ParseCommentFollowingWhitespace();
        if (comment.Length > 0)
        {
            word += comment;
            index += comment.Length;
        }

        if (word.Length != 24 && Regex.IsMatch(word, @"^\d+$"))
        {
            return Int32.Parse(word);
        }

        return word;
    }

    #endregion
    #region Serialize

    private void AppendNewline(StringBuilder builder)
    {
        builder.Append(WHITESPACE_NEWLINE);
        for (int i = 0; i < indent; i++)
        {
            builder.Append(WHITESPACE_TAB);
        }
    }

    private void AppendLineDelim(StringBuilder builder, bool newline)
    {
        if (newline)
        {
            AppendNewline(builder);
        }
        else
        {
            builder.Append(WHITESPACE_SPACE);
        }
    }

    private bool SerializeValue(object value, StringBuilder builder)
    {
        bool internalNewlines = false;
        if (value is PBXObject)
        {
            internalNewlines = ((PBXObject) value).internalNewlines;
        }
        else if (value is PBXDictionary)
        {
            internalNewlines = ((PBXDictionary) value).internalNewlines;
        }
        else if (value is PBXList)
        {
            internalNewlines = ((PBXList) value).internalNewlines;
        }

        if (value == null)
        {
            builder.Append("null");
        }
        else if (value is PBXObject)
        {
            SerializeDictionary(((PBXObject) value).data, builder, internalNewlines);
        }
        else if (value is PBXDictionary)
        {
            SerializeDictionary((Dictionary<string, object>)value, builder, internalNewlines);
        }
        else if (value is Dictionary<string, object>)
        {
            SerializeDictionary((Dictionary<string, object>)value, builder, internalNewlines);
        }
        else if (value.GetType().IsArray)
        {
            SerializeArray(new ArrayList((ICollection) value), builder, internalNewlines);
        }
        else if (value is ArrayList)
        {
            SerializeArray((ArrayList) value, builder, internalNewlines);
        }
        else if (value is string)
        {
            SerializeString((string) value, builder);
        }
        else if (value is Char)
        {
            SerializeString(Convert.ToString((char) value), builder);
        }
        else if (value is bool)
        {
            builder.Append(Convert.ToInt32(value).ToString());
        }
        else if (value.GetType().IsPrimitive)
        {
            builder.Append(Convert.ToString(value));
        }
        else
        {
            Debug.LogWarning("Error: unknown object of type " + value.GetType().Name);
            return false;
        }

        return true;
    }

    private bool SerializeDictionary(Dictionary<string, object> dictionary, StringBuilder builder, bool internalNewlines)
    {
        builder.Append(DICTIONARY_BEGIN_TOKEN);
        if (dictionary.Count > 0)
        {
            indent++;
        }
        if (internalNewlines)
        {
            AppendNewline(builder);
        }

        int i = 0;
        foreach (KeyValuePair<string, object> pair in dictionary)
        {
            SerializeString(pair.Key, builder);
            builder.Append(WHITESPACE_SPACE);
            builder.Append(DICTIONARY_ASSIGN_TOKEN);
            builder.Append(WHITESPACE_SPACE);
            SerializeValue(pair.Value, builder);
            builder.Append(DICTIONARY_ITEM_DELIMITER_TOKEN);

            if (i == dictionary.Count - 1)
            {
                indent--;
            }
            AppendLineDelim(builder, internalNewlines);
            i++;
        }

        builder.Append(DICTIONARY_END_TOKEN);
        return true;
    }

    private bool SerializeArray(ArrayList anArray, StringBuilder builder, bool internalNewlines)
    {
        builder.Append(ARRAY_BEGIN_TOKEN);
        if (anArray.Count > 0)
        {
            indent++;
        }
        if (internalNewlines)
        {
            AppendNewline(builder);
        }


        for (int i = 0; i < anArray.Count; i++)
        {
            object value = anArray[i];

            if (!SerializeValue(value, builder))
            {
                return false;
            }

            builder.Append(ARRAY_ITEM_DELIMITER_TOKEN);

            if (i == anArray.Count - 1)
            {
                indent--;
            }
            AppendLineDelim(builder, internalNewlines);
        }

        builder.Append(ARRAY_END_TOKEN);
        return true;
    }

    private bool SerializeString(string aString, StringBuilder builder)
    {
        // Is a GUID?
        if (PBXObject.IsGuid(aString))
        {
            builder.Append(aString);
            return true;
        }

        // Is an empty string?
        if (string.IsNullOrEmpty(aString))
        {
            builder.Append(QUOTEDSTRING_BEGIN_TOKEN);
            builder.Append(QUOTEDSTRING_END_TOKEN);
            return true;
        }

        builder.Append(aString);

        return true;
    }

    #endregion
}
}
