/* Copyright
2024 Reto Spoerri
rspoerri@nouser.org
*/

using System.Collections.Generic;
using System.IO;
using System;
using UnityEditor;
using UnityEngine;
// using System.Diagnostics;
using System.Text;
using UnityEditor.AssetImporters;
using NUnit.Framework.Constraints;
using System.Linq;

[System.Serializable]

public class MaterialTextureData {
    public List<MaterialTextureInfo> materials; // Changed to a list
}

[System.Serializable]
public class MaterialTextureInfo {
    public string materialName; // Material name
    public List<TextureInfo> textureInfos; // List of texture info
    public float roughness; // Store roughness if necessary
}

[System.Serializable]
public class TextureInfo {
    public string image_name; // Relative path to the texture from the model's path
    public string channel; // Store channels if necessary
    public string comments; // Store comments if necessary
}



public class GPUInstancing : AssetPostprocessor
{
    private const string targetModelPath = "Assets/YourModelPath/YourModelName.fbx"; // Update this path to your specific model's path

    public Material OnAssignMaterialModel(Material material, Renderer renderer)
    {
        // Check if the current model being processed is the one you want to target
        if (assetPath.Equals(targetModelPath, System.StringComparison.OrdinalIgnoreCase))
        {
            ModelImporter importer = (ModelImporter)assetImporter;
            importer.AddRemap(new AssetImporter.SourceAssetIdentifier(material), 
                (Material)AssetDatabase.LoadAssetAtPath("Assets/ProfilingData/Materials/material.2.mat", typeof(Material)));
            return null; // Returning null means the material is replaced
        }

        // Return the original material if the model does not match
        return material;
    }
}

public class MaterialTextureUpdaterEditor : EditorWindow {
    private string jsonFilePath; // Path to the JSON file
    private GameObject selectedModel; // Reference to the selected model

    [MenuItem("Tools/Material Texture Updater")]
    public static void ShowWindow() {
        GetWindow<MaterialTextureUpdaterEditor>("Material Texture Updater");
    }

    GameObject _previousModel = null;
    private int materialCount = 0;
    private void OnGUI() {
        GUILayout.Label("Material Texture Updater", EditorStyles.boldLabel);

        // Field to select a model from the project
        selectedModel = (GameObject)EditorGUILayout.ObjectField("Selected Model", selectedModel, typeof(GameObject), false);
        if (selectedModel != _previousModel) {
            _previousModel = selectedModel;
            materialCount = -1;
            jsonFilePath = "";
        }

        if (selectedModel != null) {


            if (GUILayout.Button("Export JSON from Blender")) {
                string assetPath = AssetDatabase.GetAssetPath(selectedModel);
                string fullModelPath = Path.GetFullPath(Path.Combine(Application.dataPath, assetPath.Substring("Assets/".Length)));
                Debug.Log($"Run Blender on {fullModelPath}");
                LaunchBlender(fullModelPath);
                materialCount = -1;
            }

            if (String.IsNullOrEmpty(jsonFilePath)) {
                string expectedJsonFile = GetExpectedJsonFilePath(selectedModel);

                if (File.Exists(expectedJsonFile)) {
                    jsonFilePath = expectedJsonFile;
                }
                // else {
                //     EditorGUILayout.LabelField("JSON File Not Found: " + expectedJsonFile);
                // }
            }

            EditorGUILayout.BeginHorizontal();
            EditorGUILayout.LabelField($"JSON File: {jsonFilePath}");

            if (GUILayout.Button("Manually select"))  {
                jsonFilePath = EditorUtility.OpenFilePanel("Select JSON File", Path.GetDirectoryName(AssetDatabase.GetAssetPath(selectedModel)), "json");
                jsonFilePath = FileUtil.GetProjectRelativePath(jsonFilePath);
                materialCount = -1;
            }
            if (GUILayout.Button("Reset"))  {
                jsonFilePath = "";
                materialCount = -1;
            }
            EditorGUILayout.EndHorizontal();

            if ((!String.IsNullOrEmpty(jsonFilePath)) && (jsonFilePath!="ERROR")) {

                if (materialCount < 0) {
                    string jsonData = File.ReadAllText(jsonFilePath);
                    MaterialTextureData materialData = JsonUtility.FromJson<MaterialTextureData>(jsonData);
                    if (materialData == null) {
                        Debug.LogError("Failed to deserialize JSON data.");
                        jsonFilePath = "ERROR";
                    } else {
                        materialCount = materialData.materials.Count;
                    }
                }
                EditorGUILayout.LabelField($"JSON File with {materialCount} Materials");

                GUILayout.Space(10);

                if (GUILayout.Button("Create new Materials from JSON")) {
                    ApplyTexturesToSelectedModel();
                }

                EditorGUILayout.BeginHorizontal();
                if (GUILayout.Button("Apply Material Remap")) {
                    ApplyMaterialRemapSettings();
                }

                if (GUILayout.Button("Clear Material Remapping for Model")) {
                    string selectedModelPath = AssetDatabase.GetAssetPath(selectedModel);
                    AssetImporter assetImporter = AssetImporter.GetAtPath(selectedModelPath);
                    Dictionary<AssetImporter.SourceAssetIdentifier, UnityEngine.Object> externalObjectMap = assetImporter.GetExternalObjectMap();
                    foreach (KeyValuePair<AssetImporter.SourceAssetIdentifier, UnityEngine.Object> kvp in externalObjectMap) {
                        Debug.Log(kvp.Key + " -> " + kvp.Value);
                        assetImporter.RemoveRemap(kvp.Key);
                    }
                    AssetDatabase.WriteImportSettingsIfDirty(selectedModelPath);
                    AssetDatabase.ImportAsset(selectedModelPath, ImportAssetOptions.ForceUpdate);
                }
                EditorGUILayout.EndHorizontal();
            }
        }
    }


    private void LaunchBlender(string blenderModelFile) {
        string blenderPath = GetBlenderPath();
        if (string.IsNullOrEmpty(blenderPath)) {
            UnityEngine.Debug.LogError("Blender not found.");
            return;
        }

        string blendFile = blenderModelFile; // Adjust your blend file path if needed

        // Construct the path to the Python script
        string pythonScript = Path.Combine(Application.dataPath, "Editor", "mat-export.py");
        // string pythonScript = "mat-export.py"; // Adjust your Python script path if needed

        // Prepare the process start info
        System.Diagnostics.ProcessStartInfo startInfo = new System.Diagnostics.ProcessStartInfo {
            FileName = blenderPath,
            Arguments = $"--background \"{blendFile}\" --python \"{pythonScript}\"",
            RedirectStandardOutput = true,
            RedirectStandardError = true,
            UseShellExecute = false,
            CreateNoWindow = true,
            WorkingDirectory = Path.GetDirectoryName(blenderModelFile) // Set the working directory
        };

        using (var process = new System.Diagnostics.Process { StartInfo = startInfo }) {
            StringBuilder output = new StringBuilder();
            StringBuilder errorOutput = new StringBuilder();

            // Hook up output and error streams
            process.OutputDataReceived += (sender, args) => {
                if (!string.IsNullOrEmpty(args.Data)) {
                    output.AppendLine(args.Data);
                }
            };

            process.ErrorDataReceived += (sender, args) => {
                if (!string.IsNullOrEmpty(args.Data)) {
                    errorOutput.AppendLine(args.Data);
                }
            };

            process.Start();

            // Begin reading the output
            process.BeginOutputReadLine();
            process.BeginErrorReadLine();

            // Wait for the process to exit
            process.WaitForExit();

            // Get the output and error messages
            string outputString = output.ToString();
            string errorString = errorOutput.ToString();

            Debug.Log("Output: " + outputString);
            if (!string.IsNullOrEmpty(errorString)) {
                Debug.LogError("Error Output: " + errorString);
            }
        }

        AssetDatabase.Refresh();
    }

    private string GetBlenderPath() {
        #if UNITY_STANDALONE_OSX
            return "/Applications/Blender.app/Contents/MacOS/Blender";
        #elif UNITY_STANDALONE_WIN
            // Adjust the path as needed for Windows
            return @"C:\Program Files\Blender Foundation\Blender\blender.exe";
        #elif UNITY_STANDALONE_LINUX
            // Adjust the path as needed for Linux
            return "/usr/bin/blender";
        #else
            //return null; // Unsupported platform
            return "/Applications/Blender.app/Contents/MacOS/Blender";
        #endif
    }


    private string GetExpectedJsonFilePath(GameObject model) {
        string modelPath = AssetDatabase.GetAssetPath(model);
        string modelName = Path.GetFileNameWithoutExtension(modelPath);
        string jsonFileName = $"{modelName}_materials_data.json";
        string jsonFilePath = Path.Combine(Path.GetDirectoryName(modelPath), jsonFileName);

        return jsonFilePath;
    }

    // Method to apply material remapping settings
    private void ApplyMaterialRemapSettings() {
        // Get the path to the model's asset
        string assetPath = AssetDatabase.GetAssetPath(selectedModel);
        if (string.IsNullOrEmpty(assetPath)) {
            Debug.LogError("No valid model selected!");
            return;
        }

        // Get the ModelImporter for the selected model
        ModelImporter modelImporter = AssetImporter.GetAtPath(assetPath) as ModelImporter;
        if (modelImporter == null) {
            Debug.LogError("Selected object is not a valid 3D model!");
            return;
        }

        // Apply the material remap settings
        // modelImporter.materialImportMode = ModelImporterMaterialImportMode.ImportStandard; // On Demand Remap
        // modelImporter.materialSearch = ModelImporterMaterialSearch.Local; // Search and Remap
        // modelImporter.materialLocation = ModelImporterMaterialLocation.InPrefab; // From Model's Material, Local Folder

        AssetDatabase.Refresh();

        modelImporter.SearchAndRemapMaterials(ModelImporterMaterialName.BasedOnMaterialName, ModelImporterMaterialSearch.Local);

        // Save the changes and reimport the model to apply the settings
        AssetDatabase.WriteImportSettingsIfDirty(assetPath);
        AssetDatabase.ImportAsset(assetPath, ImportAssetOptions.ForceUpdate);

        Debug.Log($"Material remap settings applied to {selectedModel.name}.");
    }

    // to incorporate
    // https://discussions.unity.com/t/access-models-remapped-materials-through-code/217739/2
    // https://docs.unity3d.com/ScriptReference/AssetImporter.AddRemap.html

    private void ApplyTexturesToSelectedModel() {
        if (selectedModel == null) {
            Debug.LogError("No model selected.");
            return;
        }

        if (string.IsNullOrEmpty(jsonFilePath) || !File.Exists(jsonFilePath)) {
            Debug.LogError("Invalid JSON file path.");
            return;
        }

        string jsonData = File.ReadAllText(jsonFilePath);
        MaterialTextureData materialData = JsonUtility.FromJson<MaterialTextureData>(jsonData);
        if (materialData == null) {
            Debug.LogError("Failed to deserialize JSON data.");
            return;
        }

        string modelPath = Path.GetDirectoryName(AssetDatabase.GetAssetPath(selectedModel));
        string jsonPath = Path.GetDirectoryName(jsonFilePath);

        string materialsPath = Path.Combine(modelPath, "Materials");
        if (!AssetDatabase.IsValidFolder(materialsPath)) {
            Debug.Log($"Create 'Materials' folder in '{modelPath}' ('{selectedModel}' '{modelPath}')");
            AssetDatabase.CreateFolder(modelPath, "Materials");
        }

        Debug.Log("Material count: " + materialData.materials.Count);
        Renderer[] renderers = selectedModel.GetComponentsInChildren<Renderer>();

        Dictionary<string, string> texture_channel_mapping = new Dictionary<string, string>() {
            {"COLOR", "_BaseMap"}, // _MainTex, _BaseColor, _BaseColorMap, _Color
            {"METALNESS", "_MetallicGlossMap"},
            {"SPECULAR", "_SpecGlossMap"},
            {"NORMAL", "_BumpMap"},
            {"ROUGHNESS", "_MetallicGlossMap"}, // it's the same in URP?
            {"GLOSS", "_MetallicGlossMap"},
            {"OPACITY", "_OpacityMap"}, // dont overwrite _BaseMap
            {"OCCLUSION", "_OcclusionMap"},
            {"EMISSION", "_EmissionMap"},
            {"DISPLACEMENT", "_Height"},
            {"AMBIENT_OCCLUSION", "_Occlusion"},
        };

        for (int i = 0; i < materialData.materials.Count; i++) {
            MaterialTextureInfo materialInfo = materialData.materials[i];
            if (materialInfo != null) {
                // Create a copy of the material to apply changes
                string materialAssetPath = Path.Combine(materialsPath, materialInfo.materialName);
                Debug.Log($"Create {materialAssetPath}.mat ({materialsPath})");
                Material instanceMaterial = new Material(Shader.Find("Universal Render Pipeline/Lit"));
                AssetDatabase.CreateAsset(instanceMaterial, materialAssetPath+".mat");

                Debug.Log($"Created {AssetDatabase.GetAssetPath(instanceMaterial)}");

                AssetDatabase.WriteImportSettingsIfDirty(materialAssetPath);
                EditorUtility.SetDirty(instanceMaterial);
                AssetDatabase.SaveAssetIfDirty(instanceMaterial);
                AssetDatabase.Refresh();

                instanceMaterial.SetFloat("_Smoothness", materialInfo.roughness);

                if (true) {
                    foreach (var textureInfo in materialInfo.textureInfos) {
                        string textureAssetPath = Path.Combine(jsonPath, textureInfo.image_name);
                        Texture2D texture = AssetDatabase.LoadAssetAtPath<Texture2D>(textureAssetPath);

                        Debug.Log($"Setting {textureInfo.image_name} to {textureInfo.channel} w={texture.width}");

                        if (texture != null) {
                            string texture_channel = "";
                            texture_channel_mapping.TryGetValue(textureInfo.channel, out texture_channel);

                            if (!String.IsNullOrEmpty(texture_channel)) {
                                Debug.Log($"{materialInfo.materialName} {textureInfo.image_name} applying to {texture_channel} ({textureInfo.channel})");
                                instanceMaterial.SetTexture(texture_channel, texture);
                                
                                // set additional parameters
                                switch (textureInfo.channel) {
                                    case "OPACITY":
                                        instanceMaterial.SetFloat("_Mode", 2);
                                        break;
                                }
                            } else {
                                Debug.LogError($"{materialInfo.materialName} {textureInfo.image_name} Unknown channel type: {textureInfo.channel}");
                            }
                        }

                        else {
                            Debug.LogError($"Texture not found at path: {textureAssetPath}");
                        }
                    }

                    EditorUtility.SetDirty(instanceMaterial);
                    AssetDatabase.SaveAssetIfDirty(instanceMaterial);
                }

                EditorUtility.SetDirty(instanceMaterial);
                AssetDatabase.SaveAssetIfDirty(instanceMaterial);

                Debug.Log($"Reading='{instanceMaterial.name}' {texture_channel_mapping["COLOR"]}='{instanceMaterial.GetTexture(texture_channel_mapping["COLOR"])}'");
            }
        }

        Debug.Log("Textures applied to the selected model.");
    }
}