/*==============================================================================
Copyright (c) 2015-2016 PTC Inc. All Rights Reserved. Confidential and Proprietary - 
Protected under copyright and other laws.
Vuforia is a trademark of PTC Inc., registered in the United States and other 
countries.   
==============================================================================*/


using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using UnityEditor;
using UnityEditor.Callbacks;
using UnityEngine;

namespace Vuforia.EditorClasses
{
    /// <summary>
    /// Purpose of this post build script is to post process the project.pbxproj file
    /// generated by Unity to add any additional Libraries, Frameworks or build paths
    /// needed to build a QCAR app then add calls to the QCAR library into the Unity
    /// generated C code
    /// </summary>
    class PostProcessBuildPlayer
    {
        #region NESTED

        private class Framework
        {
            public Framework(string name, string id, string fileRefId)
            {
                Name = name;
                Id = id;
                FileRefId = fileRefId;
            }

            public readonly string Name;
            public readonly string Id;
            public readonly string FileRefId;
        }

        private class ResFile : Framework
        {
            public ResFile(string name, string id, string fileRefId, string lastKnownType)
                : base(name, id, fileRefId)
            {
                LastKnownType = lastKnownType;
            }

            public readonly string LastKnownType;
        }

        #endregion // NESTED



        #region PRIVATE_MEMBER_VARIABLES

        // These ids have been generated by creating a project using Xcode then
        // extracting the values from the generated project.pbxproj.  The format of this
        // file is not documented by Apple so the correct algorithm for generating these
        // ids is unknown

        private const string AVFOUNDATION_ID = "CCE8C2AB135C7CDD000D8035";
        private const string AVFOUNDATION_FILEREFID = "CCE8C2AA135C7CDD000D8035";

        private const string COREVIDEO_ID = "CC375CE01316C2C5004F0FDD";
        private const string COREVIDEO_FILEREFID = "CC375CDF1316C2C5004F0FDD";

        private const string COREMEDIA_ID = "CC375CE51316C2D3004F0FDD";
        private const string COREMEDIA_FILEREFID = "CC375CE41316C2D3004F0FDD";

        private const string SECURITY_ID = "CCE8C2BA135C7EA3000D8035";
        private const string SECURITY_FILEREFID = "CCE8C2B9135C7EA3000D8035";

        private const string QCARDIR_ID = "CC9FCA1D1445D76E004F4DC3";
        private const string QCARDIR_FILEREFID = "CC9FCA171445D76E004F4DC3";

        private const string VUFORIADIR_ID = "8C4ECDDD1C5F637A0070D641";
        private const string VUFORIADIR_FILEREFID = "8C4ECDDC1C5F637A0070D641";


        // List of all the frameworks to be added to the project
        private static readonly Framework[] _frameworks = new[]
        {
            new Framework("AVFoundation.framework", AVFOUNDATION_ID, AVFOUNDATION_FILEREFID),
            new Framework("CoreMedia.framework", COREMEDIA_ID, COREMEDIA_FILEREFID),
            new Framework("CoreVideo.framework", COREVIDEO_ID, COREVIDEO_FILEREFID),
            new Framework("Security.framework", SECURITY_ID, SECURITY_FILEREFID),
        };

        private static readonly ResFile[] _resFiles = new[]
        {
            new ResFile("QCAR", QCARDIR_ID, QCARDIR_FILEREFID, "folder"), 
            new ResFile("Vuforia", VUFORIADIR_ID, VUFORIADIR_FILEREFID, "folder"), 
        };

        #endregion // PRIVATE_MEMBER_VARIABLES



        #region PRIVATE_METHODS

        private static void AddBuildFile(StreamWriter pbxProj, Framework framework)
        {
            var subsection = "Resources";
            if (framework.Name.EndsWith("framework"))
                subsection = "Frameworks";
            Debug.Log("Adding build file " + framework.Name);


            pbxProj.WriteLine("\t\t" + framework.Id + " /* " + framework.Name + " in " + subsection +
                              " */ = {isa = PBXBuildFile; fileRef = " + framework.FileRefId + " /* " + framework.Name +
                              " */; };");

        }

        private static void AddResFileReference(StreamWriter pbxProj, ResFile resfile)
        {
            Debug.Log("Adding data file reference " + resfile.Name);

            var id = resfile.FileRefId;
            var lastKnownFileType = resfile.LastKnownType;
            var name = resfile.Name;

            pbxProj.WriteLine("\t\t" + id + " /* " + name +
                              " */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = " +
                              lastKnownFileType + "; name = " + name + "; path = Data/Raw/" + name +
                              "; sourceTree = SOURCE_ROOT; };");
        }

        private static void AddFrameworkFileReference(StreamWriter pbxProj, Framework framework)
        {
            var id = framework.FileRefId;
            var name = framework.Name;

            Debug.Log("Adding framework file reference " + name);
            pbxProj.WriteLine("\t\t" + id + " /* " + name +
                              " */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = " + name +
                              "; path = System/Library/Frameworks/" + name + "; sourceTree = SDKROOT; };");
        }

        private static void AddFrameworksBuildPhase(StreamWriter pbxProj, Framework framework)
        {
            var id = framework.Id;
            var name = framework.Name;
            Debug.Log("Adding build phase " + name);
            pbxProj.WriteLine("\t\t\t\t" + id + " /* " + name + " in Frameworks */,");
        }

        private static void AddResourcesBuildPhase(StreamWriter pbxProj, ResFile resfile)
        {
            var id = resfile.Id;
            var name = resfile.Name;
            Debug.Log("Adding build phase " + name);
            pbxProj.WriteLine("\t\t\t\t" + id + " /* " + name + " in Resources */,");
        }

        private static void AddGroup(StreamWriter pbxProj, Framework framework)
        {
            var id = framework.FileRefId;
            var name = framework.Name;
            Debug.Log("Add group " + name);
            pbxProj.WriteLine("\t\t\t\t" + id + " /* " + name + " */,");
        }

        private static string[] ReadExistingFiles(string[] lines)
        {
            var beginPbxbuildfileSection = false;
            var existingFiles = new List<string>();

            int i = 0;
            var line = lines[i];
            while (line.Length < 6 || line.Substring(3, 3) != "End")
            {
                if(!beginPbxbuildfileSection)
                    beginPbxbuildfileSection = line.Length > 3 && line.Substring(3).StartsWith("Begin PBXBuildFile");
                else
                    existingFiles.Add(line.Split(new char[0], StringSplitOptions.RemoveEmptyEntries)[2]);
                i = i + 1;
                line = lines[i];
            }

            return existingFiles.ToArray();
        }


        /// <summary>
        /// Processes the given xcode project to add or change the supplied parameters
        /// </summary>
        /// <param name="xCodeProjFileName">filename of the Xcode project to change</param>
        /// <param name="frameworks">list of Apple standard frameworks to add to the project</param>
        /// <param name="resFiles">list resource files added to the project</param>
        private static void ProcessPbxProj(string xCodeProjFileName, Framework[] frameworks, ResFile[] resFiles)
        {
            // Open up the file generated by Unity and read into memory as a list of lines for processing
            var pbxprojFilename = Path.Combine(xCodeProjFileName, "project.pbxproj");
            var lines = File.ReadAllLines(pbxprojFilename);


            // Work out which of the resfiles exist and remove them if they don't, this
            // There may not be a qcar resource folder if no targets are used
            var newResFiles = new List<ResFile>();
            foreach (var rf in resFiles)
                if (Directory.Exists(Path.Combine(xCodeProjFileName, "../Data/Raw/" + rf.Name)))
                {
                    newResFiles.Add(rf);
                }
            resFiles = newResFiles.ToArray();

            // Next open up an empty project.pbxproj for writing and iterate over the old
            // file copying the original file and inserting anything extra we need
            var pbxproj = File.CreateText(pbxprojFilename);

            // As we iterate through the list we'll record which section of the
            // project.pbxproj we are currently in
            var section = "";

            // We use these booleans to decide whether we have already added the list of
            // build files to the link line.  This is needed because there could be multiple
            // build targets and they are not named in the project.pbxproj
            var frameworksBuildAdded = false;
            var resBuildAdded = false;

            // Build a list of the files already added to the project.  Then use it to
            // avoid adding anything to the project twice
            var existingFiles = ReadExistingFiles(lines);
            var filteredFrameworks = new List<Framework>();
            foreach (var framework in frameworks)
                if (!existingFiles.Contains(framework.Name))
                    filteredFrameworks.Add(framework);
            frameworks = filteredFrameworks.ToArray();

            var filteredResFiles = new List<ResFile>();
            foreach (var resFile in resFiles)
                if (!existingFiles.Contains(resFile.Name))
                    filteredResFiles.Add(resFile);
            resFiles = filteredResFiles.ToArray();



            // Now iterate through the project adding any new lines where needed
            for (int i = 0; i < lines.Length; i++)
            {
                var line = lines[i];


                // Disable BITCODE
                if (line.TrimStart().StartsWith("ENABLE_BITCODE"))
                {
                    line = line.Replace("YES", "NO");
                }

                pbxproj.WriteLine(line);

                // Each section starts with a comment such as
                // /* Begin PBXBuildFile section */"
                if (line.Length > 3 && line.Substring(3).StartsWith("Begin"))
                {
                    section = line.Split(' ')[2];
                    if (section == "PBXBuildFile")
                    {
                        foreach (var framework in frameworks)
                            AddBuildFile(pbxproj, framework);
                        foreach (var resfile in resFiles)
                            AddBuildFile(pbxproj, resfile);
                    }
                    if (
                        section == "PBXFileReference")
                    {
                        foreach (var framework in frameworks)
                            AddFrameworkFileReference(pbxproj, framework);
                        foreach (var resfile in resFiles)
                            AddResFileReference(pbxproj, resfile);
                    }
                }
                if (line.Length > 3 && line.Substring(3).StartsWith("End"))
                {
                    section = "";
                }

                if (section == "PBXFrameworksBuildPhase")
                {
                    if (line.Trim().StartsWith("files"))
                        if (!frameworksBuildAdded)
                            foreach (var framework in frameworks)
                            {
                                AddFrameworksBuildPhase(pbxproj, framework);
                                frameworksBuildAdded = true;
                            }
                }

                // The PBXResourcesBuildPhase section is what appears in XCode as "Link
                // Binary With Libraries".  As with the frameworks we make the assumption the
                // first target is always "Unity-iPhone" as the name of the target itself is
                // not listed in project.pbxproj
                if (section == "PBXResourcesBuildPhase")
                {
                    if (line.Trim().StartsWith("files"))
                        if (!resBuildAdded)
                            foreach (var resfile in resFiles)
                            {
                                AddResourcesBuildPhase(pbxproj, resfile);
                                resBuildAdded = true;
                            }
                }

                // The PBXGroup is the section that appears in XCode as "Copy Bundle Resources". 
                if (section == "PBXGroup")
                {
                    if (line.Trim().StartsWith("children") &&
                        (lines[i - 2].Trim().Split(' ')[2] == "CustomTemplate"))
                    {
                        foreach (var resfile in resFiles)
                            AddGroup(pbxproj, resfile);
                        foreach (var framework in frameworks)
                            AddGroup(pbxproj, framework);
                    }
                }

                // The PBXShellScriptBuildPhase appears in Xcode 4 as "Run Script", we need to delete the QCAR
                // directory from the app to avoid a duplicate copy
                if (section == "PBXShellScriptBuildPhase")
                {
                    if (line.Trim().StartsWith("shellScript"))
                    {
                        pbxproj.Flush(); 
                        pbxproj.BaseStream.Seek(-3, SeekOrigin.Current);
                        
                        pbxproj.WriteLine("\\nrm -rf \\\"$TARGET_BUILD_DIR/$PRODUCT_NAME.app/Data/Raw/QCAR\\\"\";");
                    }
                }

                // change for Unity 4.2 because header search path needs to be in HEADER_SEARCH_PATHS group
                if (section == "XCBuildConfiguration")
                {
                    if (line.Trim().StartsWith("HEADER_SEARCH_PATHS = ("))
                        pbxproj.WriteLine("\t\t\t\t\t\"$(SRCROOT)/Libraries\",");
                }

                //add C++11 support by explicitly linking to libc++
                if (line.Trim().StartsWith("OTHER_LDFLAGS = ("))
                {
                    pbxproj.WriteLine("\t\t\t\t\t\"-lc++\",");
                }

            }
            pbxproj.Close();
        }

        #endregion // PRIVATE_METHODS



        [PostProcessBuildAttribute(1)]
        public static void OnPostprocessBuild(BuildTarget target, string pathToBuiltProject)
        {
#if UNITY_5_0
            if (target == BuildTarget.iPhone)
#else // UNITY_5_1 or above
            if (target == BuildTarget.iOS)
#endif
            {
                var xCodeProjFullPath = Path.Combine(pathToBuiltProject, "Unity-iPhone.xcodeproj");

                Debug.Log("xCode Project " + xCodeProjFullPath);
                ProcessPbxProj(xCodeProjFullPath, _frameworks, _resFiles);
            }
        }
    }
}
