﻿using System.Collections;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
using System.IO;
using System.Text.RegularExpressions;
using System.Text;
using System.Security.Cryptography;
using System;
using System.Linq;

namespace SimplifyXR{
    public class DLLSceneRepairTool : EditorWindow
    {
        #region Fields
        //The Map with the original GUIDs and FileIDs
        TextAsset theMap;
        //The path to the Map Directory
        string pathToMapFile = "/SimplifyXR";
        //The path to the Scene Repair Directory
        string pathOfSceneDirectory = "/SimplifyXR";
        //The GUID of the Runtime DLL (mac)
        string MacRuntimeDLLGuid = "e5f848a0d50d0bc49b70675a297a0100";
        //The GUID of the Runtime DLL (windows)
        string WindowsRuntimeDLLGuid = "3306c982ebe8887469f47b8327295b8c";
        //The GUID of the Vuforia DLL (mac)
        string VuforiaDLLGuid = "8e37628612871d645be9d85b2b16177b";
        //The GUID of the VisionLib DLL (mac)
        string MacVisionLibDLLGuid = "489c66acabb81e74abe2f00762ff2806";
        //The GUID of the VisionLib DLL (windows)
        string WindowsVisionLibDLLGuid = "64717dd1f2c16634f9bd5dde08c80138";
        //Holds the FileIDs with the GUIDs
        Dictionary<string,string> LoadedMapGUIDKey = new Dictionary<string,string>();
        //Holds the FileIDs with the GUIDs
        Dictionary<string,string> LoadedMapFileIDKey = new Dictionary<string,string>();
        //Holds which DLL the script is associated with
        Dictionary<string,string> DLLID = new Dictionary<string,string>();
        //Helper Delegate
        delegate void ProcessScene(SceneAsset toConvert);
        //How to Convert the lines
        delegate string HowToConvert(string originalLine);
        //Indicate that scenes will be converted
        bool convertScenes = true;
        //Indicate that prefabs will be converted
        bool convertPrefabs = false;
        //Indicate that scriptable objects will be converted
        bool convertScriptableObjects =false;
        //Convert to windows DLL based scenes
        bool convertToWindows = false;
        #endregion

        #region Drawing
        [MenuItem("SimpleAR Pro/Utilities/Repair Scene", false, 100)]
        static void Init()
        {
            var window = (DLLSceneRepairTool)EditorWindow.GetWindow(typeof(DLLSceneRepairTool), false, "RepairTool", true);
            window.titleContent = new GUIContent("RepairTool");
            window.Show();
        }


        [MenuItem("SimpleAR Pro/Utilities/Regenerate GUIDs", false, 100)]
        public static void RegenerateDirectiveGUIDs()
        {
            SequenceSceneSave[] sequences = FindObjectsOfType<SequenceSceneSave>();
            sequences.ToList().ForEach(sequence =>
            {
                var nodes = sequence.savedNodeCanvas.nodes;
                nodes.ForEach(node =>
                {
                    TypedNode typedNode = (TypedNode)node;
                    DirectiveNode directiveNode = (DirectiveNode)typedNode;
                    Directive currentDirective = sequence.GetDirective(directiveNode.directiveGUID);
                    currentDirective.GUID = String.IsNullOrEmpty(currentDirective.GUID) ?
                    System.Guid.NewGuid().ToString() : currentDirective.GUID;
                });
            });
        }


        protected void OnGUI()
        {
            EditorGUILayout.Space();
            EditorGUILayout.LabelField("Repair Tool");

            DrawUILine();

            EditorGUILayout.Space();
            EditorGUILayout.LabelField("Enter the path of the folder with all scripts to map");
            EditorGUILayout.Space();
            pathToMapFile = EditorGUILayout.TextField("Directory to Map",pathToMapFile);
            EditorGUILayout.Space();
            if(GUILayout.Button("Generate GUID Map")){
                string directory = Application.dataPath + pathToMapFile;
                CreateMapFileOfDirectory(directory);
            }
            EditorGUILayout.Space();

            DrawUILine();

            EditorGUILayout.BeginHorizontal();
            EditorGUILayout.BeginVertical();
            EditorGUILayout.LabelField("Enter the following information to convert scenes from Source to DLL");
            EditorGUILayout.Space();
            pathOfSceneDirectory = EditorGUILayout.TextField("Path to Asset Directory",pathOfSceneDirectory);
            EditorGUILayout.Space();
            theMap = EditorGUILayout.ObjectField("The GUID Map",theMap,typeof(TextAsset),false) as TextAsset;
            EditorGUILayout.Space();
            MacRuntimeDLLGuid = EditorGUILayout.TextField("Mac Runtime DLL GUID",MacRuntimeDLLGuid);
            EditorGUILayout.Space();
            WindowsRuntimeDLLGuid = EditorGUILayout.TextField("Windows Runtime DLL GUID",WindowsRuntimeDLLGuid);
            EditorGUILayout.Space();
            VuforiaDLLGuid = EditorGUILayout.TextField("Vuforia DLL GUID",VuforiaDLLGuid);
            EditorGUILayout.Space();
            MacVisionLibDLLGuid = EditorGUILayout.TextField("Mac VisionLib DLL GUID",MacVisionLibDLLGuid);
            EditorGUILayout.Space();
            WindowsVisionLibDLLGuid = EditorGUILayout.TextField("Windows VisionLib DLL GUID",WindowsVisionLibDLLGuid);
            EditorGUILayout.Space();
            EditorGUILayout.EndVertical();
            EditorGUILayout.Space();
            
            EditorGUILayout.BeginVertical();
            EditorGUILayout.Space();

            convertScenes = EditorGUILayout.Toggle("Convert Scenes",convertScenes);
            convertPrefabs = EditorGUILayout.Toggle("Convert Prefabs",convertPrefabs);
            convertScriptableObjects = EditorGUILayout.Toggle("Convert ScriptableObjects",convertScriptableObjects);


            if(GUILayout.Button("Convert Source To Mac DLL")){
                convertToWindows = false;
                ExecuteSourceToDLLConversion();

            }
            if(GUILayout.Button("Convert Source To Windows DLL")){
                convertToWindows = true;
                ExecuteSourceToDLLConversion();

            }
            if(GUILayout.Button("Convert DLL To Source")){
                if (!CheckVersions())
                    return;
 
                LoadedMapFileIDKey = ExtractMapDictionary(theMap,false);
                string directory = Application.dataPath + pathOfSceneDirectory;
                ProcessAllScenesAtPath(directory ,ConvertDLLLineToSource);
            }

            EditorGUILayout.EndVertical();
            EditorGUILayout.EndHorizontal();
        }
        #endregion

        #region Helper Methods
        /// <summary>
        /// Check to see if user has the correct version GUID map
        /// </summary>
        /// <returns>True if versions match, false otherwise.</returns>
        bool CheckVersions()
        {
            if(!IsCurrentVersionMap(theMap)){
                if(!EditorUtility.DisplayDialog("Version Mismatch","The version of the map and the version of SimplifyXR do not match. Proceeding will likely result in broken links.","Proceed","Cancel")){
                    Debug.LogError("User aborted conversion due to map version number mismatch");
                    return false;
                }
            }   

            if(theMap == null){
                Debug.LogError("Cannot do conversion. Please assign a map to proceed");
                return false;
            }

            return true;
        }
        void ExecuteSourceToDLLConversion(){
            if (!CheckVersions())
                    return;
            
            LoadedMapGUIDKey = ExtractMapDictionary(theMap,true);
            string directory = Application.dataPath + pathOfSceneDirectory;
            ProcessAllScenesAtPath(directory ,ConvertSourceLineToDLL);
        }

        //Recurses through the passed path and performs a process on the files at that location
        void ProcessAllScenesAtPath(string path,HowToConvert TheLineConversion){

            //Check that files will be processed
            if(!convertScenes && !convertPrefabs && !convertScriptableObjects){
                Debug.LogError("Not converting Scene, Prefabs, or ScriptableObject files. Check the bool values for these file types");
                return;
            }
            //Ensure the GUIDs are to correct length
            if(WindowsRuntimeDLLGuid.Length != 32 || MacRuntimeDLLGuid.Length != 32 || VuforiaDLLGuid.Length != 32 || WindowsVisionLibDLLGuid.Length != 32 || MacVisionLibDLLGuid.Length != 32){
                Debug.LogError("Please Enter value GUIDs to continue processing the files.");
                return; 
            }

            //Begin Processing
            List<string> fileEntries = new List<string>();

            if(convertScenes)
                fileEntries = Directory.GetFiles(path,"*.unity").ToList();

            if(convertPrefabs)
                fileEntries.AddRange(Directory.GetFiles(path,"*.prefab"));

            if(convertScriptableObjects)
                fileEntries.AddRange(Directory.GetFiles(path,"*.asset"));

            foreach(string fileName in fileEntries){
                string relativePath = fileName.Replace(Application.dataPath,"");
                relativePath = "Assets" + relativePath;
                ConvertAsset(relativePath, TheLineConversion);
            }

            // Recurse into subdirectories of this directory.
            string [] subdirectoryEntries = Directory.GetDirectories(path);
            foreach(string subdirectory in subdirectoryEntries)
                ProcessAllScenesAtPath(subdirectory,TheLineConversion);

        }
        //Convert a Asset from Source to DLL
        Regex scriptReg = new Regex(@"m_Script: {fileID: (|-)\d+, guid: \w{32}, type: \d}");
        readonly string assemblyReg = @",.*\d?.\d?.\d,";
        readonly string assemblySource = ", Assembly-CSharp, Version=0.0.0.0,";

        void ConvertAsset(string toConvert,HowToConvert TheLineConversion){

            string path = toConvert;
            Debug.LogFormat("Starting conversion of the scene at {0}",path);

            string[] originalFile = File.ReadAllLines(path);
            string[] newFile = new string[originalFile.Length];

            int j = 0;
            string assemblyReplace = ", SimplifyXR, Version=" + SimplifyXRAccessManager.Instance.GetSimplifyXRVersion() + ",";

            for (int i = 0; i < originalFile.Length; i++)
            {
                if(scriptReg.IsMatch(originalFile[i])){
                    newFile[i] = TheLineConversion(originalFile[i]);
                    j++;
                }else if(originalFile[i].Contains("Impart.")){
                    Debug.Log("Found old Impart namespace");
                    newFile[i] = originalFile[i].Replace("Impart","SimplifyXR");
                }
                // Only want to repair Step Format type within the SimplifyXR namespace
                else if(originalFile[i].Contains("NameOfFormatType: SimplifyXR."))
                {
                    if (TheLineConversion == ConvertSourceLineToDLL)
                        newFile[i] = Regex.Replace(originalFile[i], assemblyReg, assemblyReplace);
                    else if (TheLineConversion == ConvertDLLLineToSource)
                        newFile[i] = Regex.Replace(originalFile[i], assemblyReg, assemblySource);
                    Debug.LogFormat("Step Format Repair: Replaced{0} ....with...... {1}", originalFile[i], newFile[i]);
                }
                else{
                    newFile[i] = originalFile[i];
                }
            }
            Debug.LogFormat("Completed Conversion of Scene: {0}. {1} lines were modified.",path,j);
            File.WriteAllLines(path,newFile);
        }

        //Convert an individual line in the Scene file
        string ConvertSourceLineToDLL(string originalLine){

            string returnValue = originalLine;

            Regex guidReg = new Regex(@"\w{32}");
            string guid = guidReg.Match(originalLine).Value;

            if(LoadedMapGUIDKey.ContainsKey(guid)){
                string newID = LoadedMapGUIDKey[guid];
                if(DLLID[guid] == "Runtime"){
                    if(convertToWindows)
                        returnValue = string.Concat("  m_Script: {fileID: ",newID,", guid: ",WindowsRuntimeDLLGuid,", type: 3}");
                    else
                        returnValue = string.Concat("  m_Script: {fileID: ",newID,", guid: ",MacRuntimeDLLGuid,", type: 3}");
                }
                else if(DLLID[guid] == "Vuforia")
                    returnValue = string.Concat("  m_Script: {fileID: ",newID,", guid: ",VuforiaDLLGuid,", type: 3}");
                else if(DLLID[guid] == "VisionLib"){
                    if(convertToWindows)
                        returnValue = string.Concat("  m_Script: {fileID: ",newID,", guid: ",WindowsVisionLibDLLGuid,", type: 3}");
                    else
                        returnValue = string.Concat("  m_Script: {fileID: ",newID,", guid: ",MacVisionLibDLLGuid,", type: 3}");
                }
                else
                    Debug.LogError("New DLL type has been added. Add another else to this method");
                
                return returnValue;
            }else{
                return returnValue;
            }
        }

        //Convert an individual line in the Scene file
        string ConvertDLLLineToSource(string originalLine){

            string returnValue = originalLine;

            string sourceFileID = "11500000";

            Regex guidReg = new Regex(@"\w{32}");
            string guid = guidReg.Match(originalLine).Value;

            if(guid.Equals(WindowsRuntimeDLLGuid) || guid.Equals(MacRuntimeDLLGuid) || guid.Equals(VuforiaDLLGuid) || guid.Equals(WindowsVisionLibDLLGuid) || guid.Equals(MacVisionLibDLLGuid)){

                Regex fileIDReg = new Regex(@"(?<=  m_Script: {fileID: )(|-)\d+");
                string fileID = fileIDReg.Match(originalLine).Value;
                if(string.IsNullOrEmpty(fileID))
                {
                    Debug.LogErrorFormat("Bad File ID find. Skiping line {0}",originalLine);
                    return returnValue;
                }

                string sourceGUID = LoadedMapFileIDKey[fileID];

                if(!string.IsNullOrEmpty(sourceGUID)){ 
                    returnValue = string.Concat("  m_Script: {fileID: ",sourceFileID,", guid: ",sourceGUID,", type: 3}");
                }else{
                    Debug.LogErrorFormat("Bad GUID find. Skiping line {0} ");
                }
                return returnValue;
            }else{
                return returnValue;
            }
        }

        bool IsCurrentVersionMap(TextAsset theMapFile){

            string currentVersion = SimplifyXRAccessManager.Instance.GetSimplifyXRVersion();

            string path = AssetDatabase.GetAssetPath(theMapFile);
            path = path.Replace("Assets","");
            path = Application.dataPath + path;

            string mapVersion = Path.GetFileName(path);
            mapVersion = mapVersion.Replace(".csv","");
            mapVersion = mapVersion.Replace("GUIDMap","");

            if(currentVersion.Equals(mapVersion)){
                return true;
            }

            return false;
        }

        //Builds the Dictionary from a map file
        Dictionary<string,string> ExtractMapDictionary(TextAsset theMapFile, bool guidKey = true){
            
            DLLID = new Dictionary<string, string>();
            Dictionary<string,string> theReturnDictionary = new Dictionary<string,string>();

            string path = AssetDatabase.GetAssetPath(theMapFile);
            path = path.Replace("Assets","");
            path = Application.dataPath + path;
            
            string[] originalFile = File.ReadAllLines(path);

            foreach(string s in originalFile){
                string[] split = s.Split(',');
                if(guidKey)
                    theReturnDictionary.Add(split[0],split[1]);
                else
                    theReturnDictionary.Add(split[1],split[0]);

                DLLID.Add(split[0],split[2]);
            }

            Debug.LogFormat("Completed map generation with {0} entries",theReturnDictionary.Count);
            return theReturnDictionary;
        }
        //Creates the map file from a passed directory
        void CreateMapFileOfDirectory(string targetDirectory){

            List<string> Guids = new List<string>();

            ProcessDirectory(targetDirectory,Guids);

            string filename = string.Concat("GUIDMap", SimplifyXRAccessManager.Instance.GetSimplifyXRVersion(),".csv");
            string saveLocation = Application.dataPath + "/SimplifyXR/SimplifyXR_Internal/Shared Internal";

            using (StreamWriter sw = new StreamWriter(saveLocation+"/"+filename))
            {
                foreach(string s in Guids){
                    sw.WriteLine(s);
                }
            }
            Debug.LogFormat("Completed File Write at {0}. Map has {1} entries",targetDirectory,Guids.Count);
            AssetDatabase.Refresh();
        }

        // Process all files in the directory passed in, recurse on any directories 
        // that are found, and process the files they contain.
        void ProcessDirectory(string targetDirectory,List<string> output) 
        {            
            // Process the list of files found in the directory.
            string [] fileEntries = Directory.GetFiles(targetDirectory,"*.cs");
            foreach(string fileName in fileEntries){
                // output.Add(ExtractGUIDAnd(fileName));
                string extraction = ExtractGUIDAndID(fileName);
                if(!string.IsNullOrEmpty(extraction)){
                    output.Add(extraction);
                }
            }

            // Recurse into subdirectories of this directory.
            string [] subdirectoryEntries = Directory.GetDirectories(targetDirectory);
            foreach(string subdirectory in subdirectoryEntries)
                ProcessDirectory(subdirectory,output);
        }
        //Pull the GUIDs and FileIDs from the file at the passed path
        string ExtractGUIDAndID(string pathToFile){
            
            string relativePath = pathToFile.Replace(Application.dataPath + "/","");

            relativePath = "Assets/"+ relativePath;

            string guid = AssetDatabase.AssetPathToGUID(relativePath);
            Type t = AssetDatabase.GetMainAssetTypeAtPath(relativePath);

            if(t == typeof(MonoScript)){
                MonoScript asset= AssetDatabase.LoadAssetAtPath(relativePath,typeof(MonoScript)) as MonoScript;
                Type theClassType = asset.GetClass();
                if(theClassType == null)
                {
                    Debug.LogWarningFormat("Type is not a MonoScript: {0}",relativePath);
                    return null;
                }
                int fileID = FileIDUtil.Compute(theClassType);
                if(relativePath.Contains("ExternalToolAdaptors") && relativePath.Contains("Vuforia"))
                    return String.Concat(guid,',',fileID,',',"Vuforia");
                else if(relativePath.Contains("ExternalToolAdaptors") && relativePath.Contains("VisionLib"))
                    return String.Concat(guid,',',fileID,',',"VisionLib");
                else
                    return String.Concat(guid,',',fileID,',',"Runtime");
            }else{
                Debug.LogError("Bad file path");
                return null;
            }
        }

        /// <summary>
        /// Draws a line that defaults to grey color and typical thickness and padding
        /// </summary>
        /// <param name="color">Optional parameter to set color</param>
        /// <param name="thickness">Optional parameter to set thickness</param>
        /// <param name="vert_padding">Optional parameter to set vertical padding</param>
        /// <param name="horz_padding">Optional parameter to set horizontal padding</param>
        void DrawUILine(Color? color = null, int thickness = 2, int vert_padding = 10, int horz_padding = -6)
        {
            Rect rect = EditorGUILayout.GetControlRect(GUILayout.Height(vert_padding + thickness));
            rect.height = thickness;
            rect.y += vert_padding / 2;
            rect.x += horz_padding / 2;
            rect.width -= horz_padding;
            if (color == null)
                color = Color.grey;
            EditorGUI.DrawRect(rect, (UnityEngine.Color)color);
        }
        #endregion
    }


 #region Hash Calculations
    public class MD4 : HashAlgorithm
    {
        private uint _a;
        private uint _b;
        private uint _c;
        private uint _d;
        private uint[] _x;
        private int _bytesProcessed;
        
        public MD4()
        {
            _x = new uint[16];
        
            Initialize();
        }
        
        public override void Initialize()
        {
            _a = 0x67452301;
            _b = 0xefcdab89;
            _c = 0x98badcfe;
            _d = 0x10325476;
        
            _bytesProcessed = 0;
        }
        
        protected override void HashCore(byte[] array, int offset, int length)
        {
            ProcessMessage(Bytes(array, offset, length));
        }
        
        protected override byte[] HashFinal()
        {
            try
            {
                ProcessMessage(Padding());
        
                return new [] {_a, _b, _c, _d}.SelectMany(word => Bytes(word)).ToArray();
            }
            finally
            {
                Initialize();
            }
        }
        
        private void ProcessMessage(IEnumerable<byte> bytes)
        {
            foreach (byte b in bytes)
            {
                int c = _bytesProcessed & 63;
                int i = c >> 2;
                int s = (c & 3) << 3;
        
                _x[i] = (_x[i] & ~((uint)255 << s)) | ((uint)b << s);
        
                if (c == 63)
                {
                    Process16WordBlock();
                }
        
                _bytesProcessed++;
            }
        }
        
        private static IEnumerable<byte> Bytes(byte[] bytes, int offset, int length)
        {
            for (int i = offset; i < length; i++)
            {
                yield return bytes[i];
            }
        }
        
        private IEnumerable<byte> Bytes(uint word)
        {
            yield return (byte)(word & 255);
            yield return (byte)((word >> 8) & 255);
            yield return (byte)((word >> 16) & 255);
            yield return (byte)((word >> 24) & 255);
        }
        
        private IEnumerable<byte> Repeat(byte value, int count)
        {
            for (int i = 0; i < count; i++)
            {
                yield return value;
            }
        }
        
        private IEnumerable<byte> Padding()
        {
            return Repeat(128, 1)
                .Concat(Repeat(0, ((_bytesProcessed + 8) & 0x7fffffc0) + 55 - _bytesProcessed))
                .Concat(Bytes((uint)_bytesProcessed << 3))
                .Concat(Repeat(0, 4));
        }
        
        private void Process16WordBlock()
        {
            uint aa = _a;
            uint bb = _b;
            uint cc = _c;
            uint dd = _d;
        
            foreach (int k in new [] { 0, 4, 8, 12 })
            {
                aa = Round1Operation(aa, bb, cc, dd, _x[k], 3);
                dd = Round1Operation(dd, aa, bb, cc, _x[k + 1], 7);
                cc = Round1Operation(cc, dd, aa, bb, _x[k + 2], 11);
                bb = Round1Operation(bb, cc, dd, aa, _x[k + 3], 19);
            }
        
            foreach (int k in new [] { 0, 1, 2, 3 })
            {
                aa = Round2Operation(aa, bb, cc, dd, _x[k], 3);
                dd = Round2Operation(dd, aa, bb, cc, _x[k + 4], 5);
                cc = Round2Operation(cc, dd, aa, bb, _x[k + 8], 9);
                bb = Round2Operation(bb, cc, dd, aa, _x[k + 12], 13);
            }
        
            foreach (int k in new [] { 0, 2, 1, 3 })
            {
                aa = Round3Operation(aa, bb, cc, dd, _x[k], 3);
                dd = Round3Operation(dd, aa, bb, cc, _x[k + 8], 9);
                cc = Round3Operation(cc, dd, aa, bb, _x[k + 4], 11);
                bb = Round3Operation(bb, cc, dd, aa, _x[k + 12], 15);
            }
        
            unchecked
            {
                _a += aa;
                _b += bb;
                _c += cc;
                _d += dd;
            }
        }
    
        private static uint ROL(uint value, int numberOfBits)
        {
            return (value << numberOfBits) | (value >> (32 - numberOfBits));
        }
        
        private static uint Round1Operation(uint a, uint b, uint c, uint d, uint xk, int s)
        {
            unchecked
            {
                return ROL(a + ((b & c) | (~b & d)) + xk, s);
            }
        }
        
        private static uint Round2Operation(uint a, uint b, uint c, uint d, uint xk, int s)
        {
            unchecked
            {
                return ROL(a + ((b & c) | (b & d) | (c & d)) + xk + 0x5a827999, s);
            }
        }
        
        private static uint Round3Operation(uint a, uint b, uint c, uint d, uint xk, int s)
        {
            unchecked
            {
                return ROL(a + (b ^ c ^ d) + xk + 0x6ed9eba1, s);
            }
        }
    }
 
    public static class FileIDUtil
    {
        public static int Compute(Type t)
        {
            string toBeHashed = "s\0\0\0" + t.Namespace + t.Name;

            using (HashAlgorithm hash = new MD4())
            {
                byte[] hashed = hash.ComputeHash(System.Text.Encoding.UTF8.GetBytes(toBeHashed));
    
                int result = 0;
    
                for(int i = 3; i >= 0; --i)
                {
                    result <<= 8;
                    result |= hashed[i];
                }
    
                return result;
            }
        }
    }
    #endregion
}
