using System.Collections;
using System.Collections.Generic;
using System.Linq;

using UnityEditor;

using UnityEngine;
using UnityEngine.Networking;

using System.IO;
using Unity.AI.Navigation;

public class LoadAssetBundle : EditorWindow
{
    private static string loadAssetName = "";
    private static string assetURL = "";
    private static List<(string name, string type)> availableAssetNames = new List<(string, string)>();

    private static string LoadProcess = "";

    private static UnityWebRequest request;

    private static bool loadingAsset = false;
    private static bool loadingData = false;
    private static bool savingAsset = false;

    private static IEnumerator loadingAssetProcess = null;
    private static IEnumerator loadingDataProcess = null;
    private static IEnumerator savingAssetProcess = null;

    [MenuItem("Asset Bundle/Load Asset Bundle", priority = 29)]
    static void Init()
    {
        LoadAssetBundle window = EditorWindow.GetWindow(typeof(LoadAssetBundle)) as LoadAssetBundle;
        window.position = new Rect(Screen.width / 2, Screen.height / 2, 400, 150);
    }

    void OnGUI()
    {
        loadAssetName = EditorGUILayout.TextField("Target Asset(Optional):", loadAssetName);
        string targetAssetURL = EditorGUILayout.TextField("Asset bundle URL*:", assetURL);

        if (targetAssetURL != assetURL)
        {
            assetURL = targetAssetURL;
            availableAssetNames.Clear();
        }

        if (!string.IsNullOrEmpty(LoadProcess))
        {
            EditorGUILayout.LabelField(LoadProcess);
        }

        GUILayout.BeginHorizontal();
        if (loadingAsset || loadingData || savingAsset)
            GUI.enabled = false;
        if (GUILayout.Button("Clear"))
        {
            loadAssetName = "";
            assetURL = "";
            availableAssetNames.Clear();
        }
        if (GUILayout.Button("Get Data"))
        {
            if (!loadingAsset && !loadingData && !savingAsset)
                loadingData = true;
        }
        if (GUILayout.Button("Load Asset"))
        {
            if (!loadingAsset && !loadingData && !savingAsset)
                loadingAsset = true;
        }
        if (GUILayout.Button("Save Asset"))
        {
            if (!loadingAsset && !loadingData && !savingAsset)
                savingAsset = true;
        }
        if (loadingAsset || loadingData || savingAsset)
            GUI.enabled = true;
        else GUI.enabled = false;
        if (GUILayout.Button("Abort"))
        {
            loadingAsset = false;
            loadingData = false;
            savingAsset = false;
            LoadProcess = "";
            if (request != null)
            {
                request.Abort();
                request.Dispose();
                request = null;
            }
        }
        GUI.enabled = true;
        GUILayout.EndHorizontal();

        if (availableAssetNames.Count > 0)
        {
            GUILayout.BeginHorizontal();
            EditorGUILayout.LabelField("Target Asset:");
            EditorGUILayout.LabelField("Asset Type:");
            GUILayout.EndHorizontal();
            for (int i = 0; i < availableAssetNames.Count; i++)
            {
                GUILayout.BeginHorizontal();
                EditorGUILayout.SelectableLabel(availableAssetNames[i].name);
                EditorGUILayout.SelectableLabel(availableAssetNames[i].type);
                GUILayout.EndHorizontal();
            }
        }
    }

    public void Update()
    {
        if (!loadingAsset)
        {
            if (loadingAssetProcess != null)
                loadingAssetProcess = null;
        }

        if (!loadingData)
        {
            if (loadingDataProcess != null)
                loadingDataProcess = null;
        }

        if (!savingAsset)
        {
            if (savingAssetProcess != null)
                savingAssetProcess = null;
        }

        if (loadingAsset)
        {
            if (loadingAssetProcess == null)
            {
                loadingAssetProcess = LoadAsset(assetURL);
            }
            if (!loadingAssetProcess.MoveNext())
                loadingAsset = false;
        }
        if (loadingData)
        {
            if (loadingDataProcess == null)
            {
                loadingDataProcess = LoadData(assetURL);
            }
            if (!loadingDataProcess.MoveNext())
                loadingData = false;
        }
        if (savingAsset)
        {
            if (savingAssetProcess == null)
            {
                savingAssetProcess = SaveAsset(assetURL);
            }
            if (!savingAssetProcess.MoveNext())
                savingAsset = false;
        }
    }

    private IEnumerator LoadAsset(string url)
    {
        Debug.Log("Loading " + url + " ...");

        while (Caching.ready != true)
            yield return null;

        request = UnityWebRequestAssetBundle.GetAssetBundle(url);
        request.SendWebRequest();
        while (!request.isDone)
        {
            LoadProcess = "Downloading at " + request.downloadProgress * 100 + "%";
            yield return null;
        }
        if (request.error != null)
            Debug.LogError(request.error);

        AssetBundle assetBundle = DownloadHandlerAssetBundle.GetContent(request);
        string[] assetContentNames = assetBundle.GetAllAssetNames();
        GameObject loadAsset;
        if (string.IsNullOrEmpty(loadAssetName.Trim()))
            loadAsset = (GameObject)assetBundle.LoadAsset(assetContentNames[0]);
        else
            loadAsset = (GameObject)assetBundle.LoadAsset(loadAssetName);

        if (loadAsset != null)
        {
            GameObject go = GameObject.Instantiate(loadAsset);
            ReApplyShaders.Start(go);
        }

        request.Dispose();
        request = null;

        yield return Resources.UnloadUnusedAssets();
        yield return assetBundle.UnloadAsync(false);
        assetBundle = null;
        LoadProcess = "";
    }

    private IEnumerator LoadData(string url)
    {
        Debug.Log("Loading " + url + " ...");
        availableAssetNames.Clear();

        while (Caching.ready != true)
            yield return null;

        request = UnityWebRequestAssetBundle.GetAssetBundle(url);
        request.SendWebRequest();
        while (!request.isDone)
        {
            LoadProcess = "Downloading at " + request.downloadProgress * 100 + "%";
            yield return null;
        }
        if (request.error != null)
            Debug.LogError(request.error);

        AssetBundle assetBundle = DownloadHandlerAssetBundle.GetContent(request);
        Object[] assets = assetBundle.LoadAllAssets();

        foreach (Object asset in assets)
        {
            availableAssetNames.Add((asset.name, asset.GetType().Name));
        }

        request.Dispose();
        request = null;

        yield return Resources.UnloadUnusedAssets();
        yield return assetBundle.UnloadAsync(false);
        assetBundle = null;
        LoadProcess = "";
    }

    private IEnumerator SaveAsset(string url)
    {
        Debug.Log("Loading " + url + " ...");

        while (Caching.ready != true)
            yield return null;

        string bundleDir = Path.Combine(Application.dataPath, "SavedBundles");
        Directory.CreateDirectory(bundleDir);

        string bundlePath = Path.Combine(bundleDir, Path.GetFileName(url));

        request = UnityWebRequestAssetBundle.GetAssetBundle(url);
        request.SendWebRequest();
        while (!request.isDone)
        {
            LoadProcess = "Downloading at " + request.downloadProgress * 100 + "%";
            yield return null;
        }
        if (request.error != null)
            Debug.LogError(request.error);

        AssetBundle assetBundle = DownloadHandlerAssetBundle.GetContent(request);

        ExtractAllAssets(assetBundle, Path.GetFileNameWithoutExtension(bundlePath));

        request.Dispose();
        request = null;

        yield return Resources.UnloadUnusedAssets();
        yield return assetBundle.UnloadAsync(true);
        assetBundle = null;
        LoadProcess = "";
    }

    private void ExtractAllAssets(AssetBundle bundle, string bundleName)
    {
        string root = $"Assets/Extracted/{bundleName}";
        Directory.CreateDirectory(root);

        Object[] assets = bundle.LoadAllAssets();

        foreach (Object asset in assets)
        {
            SaveExtractedAsset(asset, root);
        }

        AssetDatabase.SaveAssets();
        AssetDatabase.Refresh();

        Debug.Log("Extracted assets to " + root);
    }

    private void SaveExtractedAsset(Object asset, string root)
    {
        string safeName = asset.name.Replace("/", "_");

        if (asset is Texture2D tex)
        {
            SaveTexture(tex, $"{root}/Textures/{safeName}.png");
        }
        else if (asset is Mesh)
        {
            SaveAssetCopy(asset, $"{root}/Meshes/{safeName}.asset");
        }
        else if (asset is Material)
        {
            SaveAssetCopy(Object.Instantiate(asset), $"{root}/Materials/{safeName}.mat");
        }
        else if (asset is AnimationClip)
        {
            SaveAssetCopy(Object.Instantiate(asset), $"{root}/Animations/{safeName}.anim");
        }
        else if (asset is GameObject go)
        {
            SavePrefab(go, $"{root}/Prefabs", $"{safeName}.prefab");
        }
        else
        {
            SaveAssetCopy(Object.Instantiate(asset), $"{root}/Misc/{safeName}.asset");
        }
    }

    private void SaveAssetCopy(Object asset, string path)
    {
        Directory.CreateDirectory(Path.GetDirectoryName(path));
        AssetDatabase.CreateAsset(asset, path);
    }

    private void SavePrefab(GameObject go, string path, string prefabName)
    {
        Directory.CreateDirectory(Path.GetDirectoryName(path));

        GameObject instance = Instantiate(go);

        ExtractEmbeddedAssets(go, path);

        PrefabUtility.SaveAsPrefabAsset(instance, path + "/" + prefabName);

        DestroyImmediate(instance);
    }

    private void ExtractEmbeddedAssets(GameObject root, string rootPath)
    {
        ExtractMeshes(root, rootPath);
        ExtractMaterials(root, rootPath);
        ExtractTextures(root, rootPath);
        ExtractAnimations(root, rootPath);
        ExtractNavMesh(root, rootPath);
    }

    private void ExtractMeshes(GameObject root, string path)
    {
        foreach (var mf in root.GetComponentsInChildren<MeshFilter>(true))
        {
            if (mf.sharedMesh != null &&
                AssetDatabase.GetAssetPath(mf.sharedMesh) == "")
            {
                string p = $"{path}/Meshes/{mf.sharedMesh.name}.asset";
                SaveAssetCopy(Object.Instantiate(mf.sharedMesh), p);
                mf.sharedMesh = AssetDatabase.LoadAssetAtPath<Mesh>(p);
            }
        }

        foreach (var smr in root.GetComponentsInChildren<SkinnedMeshRenderer>(true))
        {
            if (smr.sharedMesh != null &&
                AssetDatabase.GetAssetPath(smr.sharedMesh) == "")
            {
                string p = $"{path}/Meshes/{smr.sharedMesh.name}.asset";
                SaveAssetCopy(Object.Instantiate(smr.sharedMesh), p);
                smr.sharedMesh = AssetDatabase.LoadAssetAtPath<Mesh>(p);
            }
        }
    }

    private void ExtractMaterials(GameObject root, string path)
    {
        foreach (var r in root.GetComponentsInChildren<Renderer>(true))
        {
            foreach (var mat in r.sharedMaterials)
            {
                if (mat != null && AssetDatabase.GetAssetPath(mat) == "")
                {
                    string p = $"{path}/Materials/{mat.name}.mat";
                    SaveAssetCopy(Object.Instantiate(mat), p);
                }
            }
        }
    }

    private void ExtractTextures(GameObject root, string path)
    {
        foreach (var r in root.GetComponentsInChildren<Renderer>(true))
        {
            foreach (var mat in r.sharedMaterials)
            {
                if (mat == null) continue;

                foreach (string prop in mat.GetTexturePropertyNames())
                {
                    Texture tex = mat.GetTexture(prop);
                    if (tex is Texture2D t && AssetDatabase.GetAssetPath(t) == "")
                    {
                        string p = $"{path}/Textures/{t.name}.png";
                        SaveTexture(t, p);
                        mat.SetTexture(prop, AssetDatabase.LoadAssetAtPath<Texture2D>(p));
                    }
                }
            }
        }
    }

    private void ExtractAnimations(GameObject root, string path)
    {
        foreach (var animator in root.GetComponentsInChildren<Animator>(true))
        {
            if (animator.runtimeAnimatorController == null) continue;

            foreach (var clip in animator.runtimeAnimatorController.animationClips)
            {
                if (AssetDatabase.GetAssetPath(clip) == "")
                {
                    string p = $"{path}/Animations/{clip.name}.anim";
                    SaveAssetCopy(Object.Instantiate(clip), p);
                }
            }
        }
    }

    private void ExtractNavMesh(GameObject root, string path)
    {
        NavMeshSurface[] navMeshSurfaces = root.GetComponentsInChildren<NavMeshSurface>(true);
        for (int i = 0; i < navMeshSurfaces.Length;i++)
        {
            if (navMeshSurfaces[i].navMeshData != null)
            {
                if (AssetDatabase.GetAssetPath(navMeshSurfaces[i].navMeshData) == "")
                {
                    string p = $"{path}/NavMeshes/{navMeshSurfaces[i].navMeshData.name}.asset";
                    SaveAssetCopy(Object.Instantiate(navMeshSurfaces[i].navMeshData), p);
                }
            }
        }
    }

    private void SaveTexture(Texture2D tex, string path)
    {
        Directory.CreateDirectory(Path.GetDirectoryName(path));

        Texture2D readable = MakeReadable(tex);
        File.WriteAllBytes(path, readable.EncodeToPNG());

        AssetDatabase.ImportAsset(path);
    }

    private Texture2D MakeReadable(Texture2D tex)
    {
        RenderTexture rt = RenderTexture.GetTemporary(tex.width, tex.height, 0);
        Graphics.Blit(tex, rt);

        RenderTexture prev = RenderTexture.active;
        RenderTexture.active = rt;

        Texture2D readable = new Texture2D(tex.width, tex.height, TextureFormat.RGBA32, false);
        readable.ReadPixels(new Rect(0, 0, tex.width, tex.height), 0, 0);
        readable.Apply();

        RenderTexture.active = prev;
        RenderTexture.ReleaseTemporary(rt);

        return readable;
    }
}