// Copyright (c) Microsoft Corporation. 
// Licensed under the MIT license. 

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

namespace Simplygon.Unity.EditorPlugin
{
    public static class SimplygonPrefabUtils
    {
        /// <summary>
        /// Helper function to build prefab path
        /// </summary>
        /// <param name="prefabRootPath">the folder where the prefab is located</param>
        /// <param name="prefabName">the name of the prefab</param>
        /// <returns>the full path to the prefab</returns>
        private static string GetPrefabPath(string prefabRootPath, string prefabName)
        {
            return $"{prefabRootPath}/{prefabName}.prefab";
        }

        /// <summary>
        /// Checks is the object already is connected to a prefab
        /// </summary>
        /// <param name="obj">object to check</param>
        /// <returns>true if the object already is a prefab</returns>
        private static bool IsAlreadyPrefab(UnityEngine.Object obj)
        {
            UnityEngine.Object parentObject = PrefabUtility.GetCorrespondingObjectFromOriginalSource(obj);
            if (!parentObject)
                return false;
            string path = AssetDatabase.GetAssetPath(parentObject);
            return path != null;
        }

        /// <summary>
        /// Exports the incoming game object as a prefab. You can optionally embed meshes, textures and materials into the prefab.
        /// </summary>
        /// <param name="gameObject">the game object to save as a prefab</param>
        /// <param name="prefabRootPath">the root folder to put the prefab in</param>
        /// <param name="prefabName">the name of the prefab</param>
        /// <param name="embedNew">will embed materials and meshes that are created by simplygon. If materials are new, textures will also be embedded.</param>
        /// <param name="deleteTextures">if true textures that are moved to the new prefab will be deleted in the original location</param>
        public static string SaveAsPrefab(GameObject gameObject, string prefabRootPath, string prefabName, bool embedNew = false, bool deleteTextures = false)
        {
            string prefabPath = GetPrefabPath(prefabRootPath, prefabName);
            // If the prefab already exists it will be deleted.
            AssetDatabase.DeleteAsset(prefabPath);
            AssetDatabase.Refresh();
            // If we are going to embed anything into the prefab we need to create an empty prefab first.
            if (embedNew)
            {
                GameObject temp = new GameObject();
                PrefabUtility.SaveAsPrefabAssetAndConnect(temp, prefabPath, InteractionMode.UserAction);
                GameObject.DestroyImmediate(temp);
            }
            else 
            {
                string path = prefabRootPath + "/" + prefabName;
                if (!AssetDatabase.IsValidFolder(path))
                {
                    string lodGuid = AssetDatabase.CreateFolder(prefabRootPath, prefabName);
                    path = AssetDatabase.GUIDToAssetPath(lodGuid);
                }
                prefabRootPath = path;
                prefabPath = path;
            }
            SaveData(gameObject, prefabRootPath, prefabName, embedNew, deleteTextures);
            PrefabUtility.SaveAsPrefabAssetAndConnect(gameObject, GetPrefabPath(prefabRootPath, prefabName), InteractionMode.UserAction);
            AssetDatabase.Refresh();
            return prefabPath;
        }

        /// <summary>
        /// Saves the incoming object either as a separate prefab or merged into another prefab. If the object already is a prefab the null will be returned.
        /// </summary>
        /// <param name="obj">either a mesh or a material</param>
        /// <param name="prefabRootPath">the folder where the prefab is located</param>
        /// <param name="prefabName">the name of the prefab</param>
        /// <param name="fileEnding">the file ending to use for a standalone prefab</param>
        /// <param name="embed">if true, the asset will be embeddded into the existing prefab otherwise it will be a separate prefab</param>
        /// <returns>a newly created prefab object, null if the object already is a prefab</returns>
        private static UnityEngine.Object SaveObject(UnityEngine.Object obj, string prefabRootPath, string prefabName, string fileEnding, bool embed)
        {
            if (!IsAlreadyPrefab(obj))
            {
                // Better create a clone of the mesh to ensure that it's not used somewhere else.
                UnityEngine.Object objClone = GameObject.Instantiate(obj);
                objClone.name = $"{prefabName}_{ obj.name}";
                if (embed)
                {
                    AssetDatabase.AddObjectToAsset(objClone, GetPrefabPath(prefabRootPath, prefabName));
                }
                else
                {
                    string prefabPath = $"{prefabRootPath}/{obj.name}.{fileEnding}";
                    AssetDatabase.CreateAsset(objClone, prefabPath);
                }
                return objClone;
            }
            return null;
        }

        /// <summary>
        /// Moves the texture either into a prefab or into a new location, depending on whether embed is set or not.
        /// </summary>
        /// <param name="texture">the texture to move</param>
        /// <param name="prefabRootPath">the path to put the texture in</param>
        /// <param name="prefabName">the prefab to embed the asset into (if embed set)</param>
        /// <param name="embed">if true, the texture will be embedded into the asset</param>
        /// <param name="deleteTextures">if true textures that are moved to the new prefab will be deleted in the original location</param>
        /// <returns>the new texture (or potentially the old if not embedded)</returns>
        private static Texture MoveTexture(Texture texture, string prefabRootPath, string prefabName, bool embed, bool deleteTextures)
        {
            string texturePath = AssetDatabase.GetAssetPath(texture);
            //We need to make the texture readable first
            TextureImporter importer = (TextureImporter)AssetImporter.GetAtPath(texturePath);
            importer.isReadable = true;
            AssetDatabase.ImportAsset(texturePath, ImportAssetOptions.ForceUpdate);
            Texture newTexture = UnityEngine.Object.Instantiate(texture);
            newTexture.name = $"{prefabName}_{texture.name}";
            if (embed)
            {
                AssetDatabase.AddObjectToAsset(newTexture, GetPrefabPath(prefabRootPath, prefabName));
                if(deleteTextures)
                    AssetDatabase.DeleteAsset(texturePath);
            }
            else
            {
                string textureFile = $"{prefabRootPath}/{Path.GetFileName(texturePath)}";
                AssetDatabase.MoveAsset(texturePath, textureFile);
                newTexture = texture;
            }
            string folder = Path.GetDirectoryName(texturePath);
            if (Directory.GetFiles(folder).Length == 0)
            {
                AssetDatabase.DeleteAsset(folder);
            }
            return newTexture;
        }

        /// <summary>
        /// Saves all the data in the game object and it's children into a prefab.
        /// </summary>
        /// <param name="gameObject">the game object to save</param>
        /// <param name="prefabRootPath">the path to put the texture in</param>
        /// <param name="prefabName">the prefab to embed the asset into (if embed set)</param>
        /// <param name="embed">if true, all assets will be embedded into the same prefab</param>
        /// <param name="deleteTextures">if true textures that are moved to the new prefab will be deleted in the original location</param>
        private static void SaveData(GameObject gameObject, string prefabRootPath, string prefabName, bool embed, bool deleteTextures)
        {
            MeshFilter meshFilter = gameObject.GetComponent<MeshFilter>();
            if (meshFilter && meshFilter.sharedMesh)
            {
                Mesh savedMesh = (Mesh)SaveObject(meshFilter.sharedMesh, prefabRootPath, prefabName, "mesh", embed);
                if(savedMesh)
                    meshFilter.sharedMesh = savedMesh;
            }

            MeshRenderer meshRenderer = gameObject.GetComponent<MeshRenderer>();
            if (meshRenderer)
            {
                Material[] materials = new Material[meshRenderer.sharedMaterials.Length];
                for(int i = 0; i < materials.Length; ++i)
                {
                    Material savedMaterial = (Material)SaveObject(meshRenderer.sharedMaterials[i], prefabRootPath, prefabName, "mat", embed);
                    if (savedMaterial)
                    {
                        // Since this is a new material, we can assume that the textures are too. Let's move them into the prefab as well.
                        materials[i] = savedMaterial;
                        string[] materialTextureProperties = savedMaterial.GetTexturePropertyNames();

                        foreach (var materialTextureProperty in materialTextureProperties)
                        {
                            Texture texture = savedMaterial.GetTexture(materialTextureProperty);
                            if(texture)
                            {
                                texture = MoveTexture(texture, prefabRootPath, prefabName, embed, deleteTextures);
                                savedMaterial.SetTexture(materialTextureProperty, texture);
                                meshRenderer.sharedMaterials[i].SetTexture(materialTextureProperty, texture);
                            }
                        }
                    }
                    else
                        materials[i] = meshRenderer.sharedMaterials[i];
                }
                meshRenderer.sharedMaterials = materials;
            }

            foreach (Transform child in gameObject.transform)
            {
                SaveData(child.gameObject, prefabRootPath, prefabName, embed, deleteTextures);
            }
        }

        /// <summary>
        /// Clones the list of gameobjects and returns a game object containing the data
        /// </summary>
        /// <param name="prefabs">the list of game objects to clone</param>
        /// <returns>a game object containing the data from the objects to clone</returns>
        public static GameObject CreateCloneForGameObjects(List<GameObject> prefabs, int lodIndex)
        {
            GameObject cloneRoot = null;
            try
            {
                string rootName = string.Join("_", prefabs.Select(p => p.name).Distinct().ToArray());

                if(lodIndex >= 0)
                {
                    rootName = rootName + $"_LOD{lodIndex}";
                }

                if (prefabs.Count > 1)
                {
                    Vector3 center = Vector3.zero;
                    foreach (GameObject prefab in prefabs)
                    {
                        center += prefab.transform.localPosition;
                    }
                    center = center / prefabs.Count;
                    cloneRoot = new GameObject();
                    cloneRoot.name = rootName;
                    cloneRoot.transform.localPosition = center;

                    foreach (GameObject prefab in prefabs)
                    {
                        GameObject clonePrefab = GameObject.Instantiate(prefab, prefab.transform.localPosition, prefab.transform.localRotation) as GameObject;
                        clonePrefab.transform.parent = cloneRoot.transform;
                        clonePrefab.name = prefab.name;
                        clonePrefab.hideFlags = HideFlags.None;
                    }
                }
                else
                {
                    cloneRoot = GameObject.Instantiate(prefabs[0], Vector3.zero, prefabs[0].transform.localRotation) as GameObject;
                    cloneRoot.name = rootName;
                    cloneRoot.hideFlags = HideFlags.None;
                    DeepCopy(prefabs[0], cloneRoot, null, null);
                }
            }
            catch (System.Exception ex)
            {
                Debug.Log(string.Format("Simplygon exception: {0} {1}", ex.Message, ex.StackTrace));
            }

            return cloneRoot;
        }



        private static void DeepCopy(GameObject srcGameObject, GameObject dstGameObject, List<Mesh> createdMeshes, List<Avatar> createdAvatars)
        {
            SkinnedMeshRenderer srcSkinnedMeshRenderer = srcGameObject.GetComponent(typeof(SkinnedMeshRenderer)) as SkinnedMeshRenderer;
            SkinnedMeshRenderer dstSkinnedMeshRenderer = dstGameObject.GetComponent(typeof(SkinnedMeshRenderer)) as SkinnedMeshRenderer;

            if (srcSkinnedMeshRenderer != null && dstSkinnedMeshRenderer != null)
            {
                if (srcSkinnedMeshRenderer.sharedMesh != null)
                {
                    Mesh srcMesh = srcSkinnedMeshRenderer.sharedMesh;
                    Mesh dstMesh = new Mesh();

                    DeepCopy(srcMesh, dstMesh);

                    dstSkinnedMeshRenderer.sharedMesh = dstMesh;

                    if (createdMeshes != null)
                    {
                        createdMeshes.Add(dstMesh);
                    }
                }
            }

            Animator srcAnimator = srcGameObject.GetComponent(typeof(Animator)) as Animator;
            Animator dstAnimator = dstGameObject.GetComponent(typeof(Animator)) as Animator;

            if (srcAnimator != null && dstAnimator != null)
            {
                if (srcAnimator.avatar != null)
                {
                    Avatar srcAvatar = srcAnimator.avatar;
                    Avatar dstAvatar = (Avatar)Avatar.Instantiate(srcAvatar);

                    dstAvatar.name = srcAvatar.name;
                    dstAnimator.avatar = dstAvatar;

                    if (createdAvatars != null)
                    {
                        createdAvatars.Add(dstAvatar);
                    }
                }
            }

            MeshFilter srcMeshFilter = srcGameObject.GetComponent(typeof(MeshFilter)) as MeshFilter;
            MeshFilter dstMeshFilter = dstGameObject.GetComponent(typeof(MeshFilter)) as MeshFilter;

            if (srcMeshFilter != null && dstMeshFilter != null)
            {
                if (srcMeshFilter.sharedMesh != null)
                {
                    Mesh srcMesh = srcMeshFilter.sharedMesh;
                    Mesh dstMesh = new Mesh();

                    DeepCopy(srcMesh, dstMesh);

                    dstMeshFilter.sharedMesh = dstMesh;

                    if (createdMeshes != null)
                    {
                        createdMeshes.Add(dstMesh);
                    }
                }
            }

            for (int i = 0; i < srcGameObject.transform.childCount; ++i)
            {
                GameObject srcChild = srcGameObject.transform.GetChild(i).gameObject;
                GameObject dstChild = dstGameObject.transform.GetChild(i).gameObject;

                DeepCopy(srcChild, dstChild, createdMeshes, createdAvatars);
            }
        }

        public static void DeepCopy(Mesh srcMesh, Mesh dstMesh)
        {
            dstMesh.Clear();

            if (srcMesh.vertices != null) { dstMesh.vertices = (Vector3[])srcMesh.vertices.Clone(); }
            if (srcMesh.normals != null) { dstMesh.normals = (Vector3[])srcMesh.normals.Clone(); }
            if (srcMesh.tangents != null) { dstMesh.tangents = (Vector4[])srcMesh.tangents.Clone(); }
            if (srcMesh.uv != null) { dstMesh.uv = (Vector2[])srcMesh.uv.Clone(); }
            if (srcMesh.uv2 != null) { dstMesh.uv2 = (Vector2[])srcMesh.uv2.Clone(); }
            if (srcMesh.uv3 != null) { dstMesh.uv3 = (Vector2[])srcMesh.uv3.Clone(); }
            if (srcMesh.uv4 != null) { dstMesh.uv4 = (Vector2[])srcMesh.uv4.Clone(); }
            if (srcMesh.colors != null) { dstMesh.colors = (Color[])srcMesh.colors.Clone(); }
            if (srcMesh.colors32 != null) { dstMesh.colors32 = (Color32[])srcMesh.colors32.Clone(); }
            if (srcMesh.boneWeights != null) { dstMesh.boneWeights = (BoneWeight[])srcMesh.boneWeights.Clone(); }
            if (srcMesh.bindposes != null) { dstMesh.bindposes = (Matrix4x4[])srcMesh.bindposes.Clone(); }

            dstMesh.subMeshCount = srcMesh.subMeshCount;
            for (int i = 0; i < srcMesh.subMeshCount; ++i)
            {
                int[] subMeshTriangles = srcMesh.GetTriangles(i);
                dstMesh.SetTriangles(subMeshTriangles, i);
            }

            dstMesh.bounds = srcMesh.bounds;
            dstMesh.name = srcMesh.name;
        }
    }
}
