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


namespace UnityEditor.XCodeEditor
{
    public class PBXResolver
    {
        private class PBXResolverReverseIndex : Dictionary<string, string> { }

        private PBXDictionary objects;
        private string rootObject;
        private PBXResolverReverseIndex index;

        public PBXResolver(PBXDictionary pbxData)
        {
            this.objects = (PBXDictionary)pbxData["objects"];
            this.index = new PBXResolverReverseIndex();
            this.rootObject = (string)pbxData["rootObject"];
            BuildReverseIndex();
        }

        private void BuildReverseIndex()
        {
            foreach (KeyValuePair<string, object> pair in this.objects)
            {
                if (pair.Value is PBXBuildPhase)
                {
                    foreach (string guid in ((PBXBuildPhase)pair.Value).files)
                    {
                        index[guid] = pair.Key;
                    }
                }
                else if (pair.Value is PBXGroup)
                {
                    foreach (string guid in ((PBXGroup)pair.Value).children)
                    {
                        index[guid] = pair.Key;
                    }
                }
            }
        }

        public string ResolveName(string guid)
        {

            if (!this.objects.ContainsKey(guid))
            {
                Debug.LogWarning(this + " ResolveName could not resolve " + guid);
                return string.Empty; //"UNRESOLVED GUID:" + guid;
            }

            object entity = this.objects[guid];

            if (entity is PBXBuildFile)
            {
                return ResolveName(((PBXBuildFile)entity).fileRef);
            }
            else if (entity is PBXFileReference)
            {
                PBXFileReference casted = (PBXFileReference)entity;
                return casted.name != null ? casted.name : casted.path;
            }
            else if (entity is PBXGroup)
            {
                PBXGroup casted = (PBXGroup)entity;
                return casted.name != null ? casted.name : casted.path;
            }
            else if (entity is PBXProject || guid == this.rootObject)
            {
                return "Project object";
            }
            else if (entity is PBXFrameworksBuildPhase)
            {
                return "Frameworks";
            }
            else if (entity is PBXResourcesBuildPhase)
            {
                return "Resources";
            }
            else if (entity is PBXShellScriptBuildPhase)
            {
                return "ShellScript";
            }
            else if (entity is PBXSourcesBuildPhase)
            {
                return "Sources";
            }
            else if (entity is PBXCopyFilesBuildPhase)
            {
                return "CopyFiles";
            }
            else if (entity is XCConfigurationList)
            {
                XCConfigurationList casted = (XCConfigurationList)entity;
                //Debug.LogWarning ("XCConfigurationList " + guid + " " + casted.ToString());

                if (casted.data.ContainsKey("defaultConfigurationName"))
                {
                    //Debug.Log ("XCConfigurationList " + (string)casted.data[ "defaultConfigurationName" ] + " " + guid);
                    return (string)casted.data["defaultConfigurationName"];
                }

                return null;
            }
            else if (entity is PBXNativeTarget)
            {
                PBXNativeTarget obj = (PBXNativeTarget)entity;
                //Debug.LogWarning ("PBXNativeTarget " + guid + " " + obj.ToString());

                if (obj.data.ContainsKey("name"))
                {
                    //Debug.Log ("PBXNativeTarget " + (string)obj.data[ "name" ] + " " + guid);
                    return (string)obj.data["name"];
                }

                return null;
            }
            else if (entity is XCBuildConfiguration)
            {
                XCBuildConfiguration obj = (XCBuildConfiguration)entity;
                //Debug.LogWarning ("XCBuildConfiguration UNRESOLVED GUID:" + guid + " " + (obj==null?"":obj.ToString()));

                if (obj.data.ContainsKey("name"))
                {
                    //Debug.Log ("XCBuildConfiguration " + (string)obj.data[ "name" ] + " " + guid + " " + (obj==null?"":obj.ToString()));
                    return (string)obj.data["name"];
                }

            }
            else if (entity is PBXObject)
            {
                PBXObject obj = (PBXObject)entity;

                if (obj.data.ContainsKey("name"))
                    Debug.Log("PBXObject " + (string)obj.data["name"] + " " + guid + " " + (obj == null ? "" : obj.ToString()));
                return (string)obj.data["name"];
            }

            //return "UNRESOLVED GUID:" + guid;
            //Debug.LogWarning ("UNRESOLVED GUID:" + guid);
            return null;
        }

        public string ResolveBuildPhaseNameForFile(string guid)
        {
            if (this.objects.ContainsKey(guid))
            {
                object obj = this.objects[guid];

                if (obj is PBXObject)
                {
                    PBXObject entity = (PBXObject)obj;

                    if (this.index.ContainsKey(entity.guid))
                    {
                        string parent_guid = this.index[entity.guid];

                        if (this.objects.ContainsKey(parent_guid))
                        {
                            object parent = this.objects[parent_guid];

                            if (parent is PBXBuildPhase)
                            {
                                string ret = ResolveName(((PBXBuildPhase)parent).guid);
                                //Debug.Log ("ResolveBuildPhaseNameForFile = " + ret);
                                return ret;
                            }
                        }
                    }
                }
            }

            return null;
        }

    }

    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 PBXResolver resolver;

        public PBXDictionary Decode(string data)
        {
            if (!data.StartsWith(PBX_HEADER_TOKEN))
            {
                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, bool readable = false)
        {
            this.resolver = new PBXResolver(pbxData);
            StringBuilder builder = new StringBuilder(PBX_HEADER_TOKEN, BUILDER_CAPACITY);

            bool success = SerializeValue(pbxData, builder, readable);
            this.resolver = null;

            // Xcode adds newline at the end of file
            builder.Append("\n");

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

        #region Pretty Print

        private void Indent(StringBuilder builder, int deep)
        {
            builder.Append("".PadLeft(deep, '\t'));
        }

        private void Endline(StringBuilder builder, bool useSpace = false)
        {
            builder.Append(useSpace ? " " : "\n");
        }

        private string marker = null;
        private void MarkSection(StringBuilder builder, string name)
        {
            if (marker == null && name == null) return;

            if (marker != null && name != marker)
            {
                builder.Append(String.Format("/* End {0} section */\n", marker));
            }

            if (name != null && name != marker)
            {
                builder.Append(String.Format("\n/* Begin {0} section */\n", name));
            }

            marker = name;
        }

        private bool GUIDComment(string guid, StringBuilder builder)
        {
            string filename = this.resolver.ResolveName(guid);
            string location = this.resolver.ResolveBuildPhaseNameForFile(guid);

            //Debug.Log( "RESOLVE " + guid + ": " + filename + " in " + location );

            if (filename != null)
            {
                if (location != null)
                {
                    //Debug.Log( "GUIDComment " + guid + " " + String.Format( " /* {0} in {1} */", filename, location )  );
                    builder.Append(String.Format(" /* {0} in {1} */", filename, location));
                }
                else
                {
                    //Debug.Log( "GUIDComment " + guid + " " + String.Format( " /* {0} */", filename) );
                    builder.Append(String.Format(" /* {0} */", filename));
                }
                return true;
            }
            else
            {
                //string other = this.resolver.ResolveConfigurationNameForFile( guid );
                //Debug.Log("GUIDComment " + guid + " [no filename]");
            }

            return false;
        }

        #endregion

        #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();
                        if (!dictionary.ContainsKey(keyString))
                        {
                            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;
            char c = StepForeward();
            while (c != QUOTEDSTRING_END_TOKEN)
            {
                s += c;

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

                c = StepForeward();
            }

            return s;
        }

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

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

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

            return word;
        }

        #endregion
        #region Serialize

        private bool SerializeValue(object value, StringBuilder builder, bool readable = false, int indent = 0)
        {
            if (value == null)
            {
                builder.Append("null");
            }
            else if (value is PBXObject)
            {
                SerializeDictionary(((PBXObject)value).data, builder, readable, indent);
            }
            else if (value is Dictionary<string, object>)
            {
                SerializeDictionary((Dictionary<string, object>)value, builder, readable, indent);
            }
            else if (value.GetType().IsArray)
            {
                SerializeArray(new ArrayList((ICollection)value), builder, readable, indent);
            }
            else if (value is ArrayList)
            {
                SerializeArray((ArrayList)value, builder, readable, indent);
            }
            else if (value is string)
            {
                SerializeString((string)value, builder, false, readable);
            }
            else if (value is Char)
            {
                SerializeString(Convert.ToString((char)value), builder, false, readable);
            }
            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 readable = false, int indent = 0)
        {
            builder.Append(DICTIONARY_BEGIN_TOKEN);
            if (readable) Endline(builder);

            foreach (KeyValuePair<string, object> pair in dictionary)
            {
                // output section banner if necessary
                if (readable && indent == 1) MarkSection(builder, pair.Value.GetType().Name);

                // indent KEY
                if (readable) Indent(builder, indent + 1);

                // KEY
                SerializeString(pair.Key, builder, false, readable);

                // =
                // FIX ME: cannot resolve mode because readable = false for PBXBuildFile/Reference sections
                builder.Append(String.Format(" {0} ", DICTIONARY_ASSIGN_TOKEN));

                // VALUE
                // do not pretty-print PBXBuildFile or PBXFileReference as Xcode does
                //Debug.Log ("about to serialize " + pair.Value.GetType () + " " + pair.Value);
                SerializeValue(pair.Value, builder, (readable &&
                    (pair.Value.GetType() != typeof(PBXBuildFile)) &&
                    (pair.Value.GetType() != typeof(PBXFileReference))
                ), indent + 1);

                // end statement
                builder.Append(DICTIONARY_ITEM_DELIMITER_TOKEN);

                // FIX ME: negative readable in favor of nice output for PBXBuildFile/Reference sections
                Endline(builder, !readable);
            }

            // output last section banner
            if (readable && indent == 1) MarkSection(builder, null);

            // indent }
            if (readable) Indent(builder, indent);

            builder.Append(DICTIONARY_END_TOKEN);

            return true;
        }

        private bool SerializeArray(ArrayList anArray, StringBuilder builder, bool readable = false, int indent = 0)
        {
            builder.Append(ARRAY_BEGIN_TOKEN);
            if (readable) Endline(builder);

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

                if (readable) Indent(builder, indent + 1);

                if (!SerializeValue(value, builder, readable, indent + 1))
                {
                    return false;
                }

                builder.Append(ARRAY_ITEM_DELIMITER_TOKEN);

                // FIX ME: negative readable in favor of nice output for PBXBuildFile/Reference sections
                Endline(builder, !readable);
            }

            if (readable) Indent(builder, indent);
            builder.Append(ARRAY_END_TOKEN);

            return true;
        }

        private bool SerializeString(string aString, StringBuilder builder, bool useQuotes = false, bool readable = false)
        {
            //Debug.Log ("SerializeString " + aString);
            // Is a GUID?
            // Note: Unity3d generates mixed-case GUIDs, Xcode use uppercase GUIDs only.
            if (Regex.IsMatch(aString, @"^[A-Fa-f0-9]{24}$"))
            {
                builder.Append(aString);
                GUIDComment(aString, builder);
                return true;
            }

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

            // FIX ME: Original regexp was: @"^[A-Za-z0-9_.]+$", we use modified regexp with '/-' allowed
            //		   to workaround Unity bug when all PNGs had "Libraries/" (group name) added to their paths after append
            if (!Regex.IsMatch(aString, @"^[A-Za-z0-9_./-]+$"))
            {
                useQuotes = true;
            }

            if (useQuotes)
                builder.Append(QUOTEDSTRING_BEGIN_TOKEN);

            builder.Append(aString);

            if (useQuotes)
                builder.Append(QUOTEDSTRING_END_TOKEN);

            return true;
        }

        #endregion
    }
}
