using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Xml.Serialization;
using UnityEditor;
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.UI;
using Object = UnityEngine.Object;

// 暂时不考虑完全替换内置资源
// 仅仅替换内置Shader,防止Shader.Parse
// 其实是替换一些Material和Texture套组
public static class BuiltinAssetBuilder
{
    public const string replacementPath = "Assets/BuiltinAssets";
    public const string configPath = replacementPath + "/config.txt";
    private const string _texturePath = replacementPath + "/Texture";
    private const string _materialPath = replacementPath + "/Material";
    private const string _shaderPath = replacementPath + "/Shader";

    public static BuiltinFileRecord CopyMaterial(Material material)
    {
        var instance = new Material(material);
        var assetPath = _materialPath.Open(material.name);
        assetPath = ChangeExtension(assetPath, ".mat");
        BuildFilePath(assetPath);
        AssetDatabase.Refresh();
        AssetDatabase.CreateAsset(instance, assetPath);
        return new BuiltinFileRecord(material, instance);
    }

    public static BuiltinFileRecord CopySprite(Sprite sprite)
    {
        var importer = SaveTextureToPath(sprite.texture);
        importer.textureType = TextureImporterType.Sprite;
        importer.SaveAndReimport();
        return new BuiltinFileRecord(sprite, AssetDatabase.LoadAssetAtPath<Sprite>(importer.assetPath));
    }

    public static BuiltinFileRecord CopyTexture2D(Texture2D texture)
    {
        var importer = SaveTextureToPath(texture);
        importer.textureType = TextureImporterType.Default;
        importer.SaveAndReimport();
        return new BuiltinFileRecord(texture, AssetDatabase.LoadAssetAtPath<Texture2D>(importer.assetPath));
    }

    public static BuiltinFileRecord GetShaderReplace(List<Shader> replaceList, Shader shader)
    {
        BuiltinFileRecord record = null;
        for (var i = 0; i < replaceList.Count; i++)
        {
            var replace = replaceList[i];
            if (replace.name == shader.name)
            {
                record = new BuiltinFileRecord(shader, replace);
                break;
            }
        }

        return record;
    }

    private static TextureImporter SaveTextureToPath(Texture2D source)
    {
        var renderTex = RenderTexture.GetTemporary(source.width, source.height);
        Graphics.Blit(source, renderTex);
        var texture = new Texture2D(renderTex.width, renderTex.height, TextureFormat.RGBA32, false);
        RenderTexture.active = renderTex;
        texture.ReadPixels(new Rect(0, 0, renderTex.width, renderTex.height), 0, 0);
        RenderTexture.active = null;
        RenderTexture.ReleaseTemporary(renderTex);
        var assetPath = _texturePath.Open(source.name);
        assetPath = ChangeExtension(assetPath, ".png");
        var filePath = BuildFilePath(assetPath);
        var binary = texture.EncodeToPNG();
        Object.DestroyImmediate(texture);
        File.WriteAllBytes(filePath, binary);
        AssetDatabase.Refresh();
        var importer = AssetImporter.GetAtPath(assetPath) as TextureImporter;
        return importer;
    }

    public static void ExtractBuiltinAssets()
    {
        var spriteList = new List<Sprite>();
        var materialList = new List<Material>();
        var textureList = new List<Texture2D>();
        var shaderList = new List<Shader>();

        var unityAssets = AssetDatabase.LoadAllAssetsAtPath("Resources/unity_builtin_extra");
        for (var i = 0; i < unityAssets.Length; i++)
        {
            var asset = unityAssets[i];
            var sprite = asset as Sprite;
            if (sprite)
            {
                spriteList.Add(sprite);
                continue;
            }

            var material = asset as Material;
            if (material)
            {
                materialList.Add(material);
                continue;
            }

            var texture = asset as Texture2D;
            if (texture)
            {
                textureList.Add(texture);
                continue;
            }

            var shader = asset as Shader;
            if (shader)
            {
                shaderList.Add(shader);
            }
        }

        for (var i = textureList.Count - 1; i >= 0; i--)
        {
            var texture = textureList[i];
            for (var j = 0; j < spriteList.Count; j++)
                if (texture.name == spriteList[j].name)
                {
                    textureList.RemoveAt(i);
                    break;
                }
        }

        var shaderReplaceList = new List<Shader>();
        var shaderReplacePaths = from assetPath in AssetDatabase.GetAllAssetPaths()
            where assetPath.StartsWith(_shaderPath)
            where Path.GetExtension(assetPath) == ".shader"
            select assetPath;
        foreach (var assetPath in shaderReplacePaths)
        {
            var shader = AssetDatabase.LoadAssetAtPath<Shader>(assetPath);
            if (shader)
                shaderReplaceList.Add(shader);
        }

        var recordList = new List<BuiltinFileRecord>();

        for (var i = 0; i < spriteList.Count; i++)
            recordList.Add(CopySprite(spriteList[i]));

        for (var i = 0; i < materialList.Count; i++)
            recordList.Add(CopyMaterial(materialList[i]));

        for (var i = 0; i < textureList.Count; i++)
            recordList.Add(CopyTexture2D(textureList[i]));

        for (var i = 0; i < shaderList.Count; i++)
        {
            var record = GetShaderReplace(shaderReplaceList, shaderList[i]);
            if (record != null)
                recordList.Add(record);
        }

        var serializer = new XmlSerializer(typeof(List<BuiltinFileRecord>));
        var configFilePath = EditorCommonUtility.AssetToFilePath(configPath);
        using (var fs = File.Create(configFilePath))
        {
            serializer.Serialize(fs, recordList);
        }

        UpdateBuiltinMaterial(recordList);
    }

    public static void ReplaceForAssetPaths(IList<string> assetPaths)
    {
        var config = LoadConfig();
        if (config != null)
        {
            for (var i = 0; i < assetPaths.Count; i++)
            {
                EditorUtility.DisplayProgressBar("替换内置资源中", string.Format("{0} / {1}", i, assetPaths.Count), (float)i / assetPaths.Count);
                try
                {

                    var assetPath = assetPaths[i];
                    var assetType = AssetDatabase.GetMainAssetTypeAtPath(assetPath);
                    // Scene在执行分解的流程中进行替换
                    // if (assetType == typeof(SceneAsset))
                    // {
                    //     var scene = EditorSceneManager.OpenScene(assetPath);
                    //     ReplaceForOneScene(scene, config);
                    // }
                    // else
                    if (assetType == typeof(GameObject))
                    {
                        var extension = Path.GetExtension(assetPath);
                        if (extension == ".prefab")
                            ReplaceForOneGameObject(AssetDatabase.LoadAssetAtPath<GameObject>(assetPath), config);
                    }
                    else if (assetType == typeof(Material))
                        ReplaceForOneMaterial(AssetDatabase.LoadAssetAtPath<Material>(assetPath), config);
                }
                catch (Exception e)
                {
                    Debug.LogError(e.ToString());
                }
            }
            Debug.LogWarning("Replace Builtin Assets Finish!");
            EditorUtility.ClearProgressBar();
            AssetDatabase.SaveAssets();
            AssetDatabase.Refresh();
        }
    }

    public static void ReplaceForPath(string pathHeader)
    {
        var assetPaths = (from assetPath in AssetDatabase.GetAllAssetPaths()
            where assetPath.StartsWith(pathHeader)
            where !assetPath.StartsWith("Assets/StreamingAssets")
            select assetPath).ToArray();
        ReplaceForAssetPaths(assetPaths);
    }

    private static string BuildFilePath(string assetPath)
    {
        var filePath = EditorCommonUtility.AssetToFilePath(assetPath);
        var directory = Path.GetDirectoryName(filePath);
        if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
            Directory.CreateDirectory(directory);
        return filePath;
    }

    private static string ChangeExtension(string assetPath, string extension)
    {
        var oldExtension = Path.GetExtension(assetPath);
        if (!string.IsNullOrEmpty(oldExtension))
            assetPath = assetPath.Remove(assetPath.Length - oldExtension.Length);
        return assetPath + extension;
    }

    private static void UpdateBuiltinMaterial(IList<BuiltinFileRecord> config)
    {
        var materialPaths = from assetPath in AssetDatabase.GetAllAssetPaths()
            where assetPath.StartsWith(_materialPath)
            where Path.GetExtension(assetPath) == ".mat"
            select assetPath;
        foreach (var materialPath in materialPaths)
        {
            var material = AssetDatabase.LoadAssetAtPath<Material>(materialPath);
            if (material != null)
                ReplaceForOneMaterial(material, config);
        }

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

    private static bool ReplaceOneRenderer(Renderer component, IList<BuiltinFileRecord> config)
    {
        var dirty = false;
        var materials = component.sharedMaterials;
        for (var i = 0; i < materials.Length; i++)
        {
            if (materials[i] != null)
            {
                var replace = GetReplaceAsset(materials[i], config);
                if (replace != null)
                {
                    dirty = true;
                    materials[i] = replace;
                }
            }
        }

        if (dirty)
            component.sharedMaterials = materials;
        return dirty;
    }

    private static bool ReplaceOneImage(Image image, IList<BuiltinFileRecord> config)
    {
        var dirty = ReplaceOneGraphic(image, config);
        if (image.sprite != null)
        {
            var replace = GetReplaceAsset(image.sprite, config);
            if (replace != null)
            {
                dirty = true;
                image.sprite = replace;
            }
        }

        return dirty;
    }

    private static bool ReplaceOneRawImage(RawImage rawImage, IList<BuiltinFileRecord> config)
    {
        var dirty = ReplaceOneGraphic(rawImage, config);
        if (rawImage.texture != null)
        {
            var replace = GetReplaceAsset(rawImage.texture, config);
            if (replace != null)
            {
                dirty = true;
                rawImage.texture = replace;
            }
        }

        return dirty;
    }

    private static bool ReplaceOneGraphic(Graphic graphic, IList<BuiltinFileRecord> config)
    {
        var dirty = false;
        if (graphic.material != null)
        {
            var replace = GetReplaceAsset(graphic.material, config);
            if (replace != null)
            {
                dirty = true;
                graphic.material = replace;
            }
        }

        return dirty;
    }

    private static void ReplaceForOneMaterial(Material source, IList<BuiltinFileRecord> config)
    {
        var dirty = false;
        var shader = source.shader;
        var newShader = Shader.Find(shader.name);
        if (shader != newShader)
        {
            dirty = true;
            source.shader = newShader;
        }
        var count = ShaderUtil.GetPropertyCount(source.shader);
        for (var i = 0; i < count; i++)
            if (ShaderUtil.GetPropertyType(source.shader, i) == ShaderUtil.ShaderPropertyType.TexEnv)
            {
                var propertyName = ShaderUtil.GetPropertyName(source.shader, i);
                var texture = source.GetTexture(propertyName);
                if (texture != null)
                {
                    var texturePath = GetReplacePath(texture, config);
                    if (!string.IsNullOrEmpty(texturePath))
                    {
                        var replace = AssetDatabase.LoadAssetAtPath<Texture2D>(texturePath);
                        if (replace != null)
                        {
                            dirty = true;
                            source.SetTexture(propertyName, replace);
                        }
                    }
                }
            }

        if (dirty)
            EditorUtility.SetDirty(source);
    }

    private static void ReplaceForOneGameObject(GameObject prefab, IList<BuiltinFileRecord> config)
    {
        var dirty = false;
        var renderers = prefab.GetComponentsInChildren<Renderer>(true);
        for (var i = 0; i < renderers.Length; i++)
            if (ReplaceOneRenderer(renderers[i], config))
                dirty = true;

        var graphics = prefab.GetComponentsInChildren<Graphic>(true);
        for (var i = 0; i < graphics.Length; i++)
        {
            var image = graphics[i] as Image;
            if (image)
            {
                if (ReplaceOneImage(image, config))
                    dirty = true;
                continue;
            }

            var rawImage = graphics[i] as RawImage;
            if (rawImage)
            {
                if (ReplaceOneRawImage(rawImage, config))
                    dirty = true;
                continue;
            }

            if (ReplaceOneGraphic(graphics[i], config))
                dirty = true;
        }

        if (dirty)
            EditorUtility.SetDirty(prefab);
    }

    public static void ReplaceForOneScene(Scene scene)
    {
        var config = LoadConfig();
        if (config != null)
        {
            var rootObjs = scene.GetRootGameObjects();
            for (var i = 0; i < rootObjs.Length; i++)
                ReplaceForOneGameObject(rootObjs[i], config);
        }
    }

    private static T GetReplaceAsset<T>(T source, IList<BuiltinFileRecord> config) where T : Object
    {
        T result = null;
        var replacePath = GetReplacePath(source, config);
        if (!string.IsNullOrEmpty(replacePath))
            result = AssetDatabase.LoadAssetAtPath<T>(replacePath);
        return result;
    }

    private static string GetReplacePath(Object source, IList<BuiltinFileRecord> config)
    {
        var typeString = source.GetType().ToString();
        var fileId = BuiltinFileRecord.GetFileId(source);
        BuiltinFileRecord record = null;
        for (var i = 0; i < config.Count; i++)
            if (config[i].assetType == typeString && config[i].sourceId == fileId)
            {
                record = config[i];
                break;
            }

        return record == null ? string.Empty : record.replacePath;
    }

    private static List<BuiltinFileRecord> LoadConfig()
    {
        List<BuiltinFileRecord> result = null;
        var filePath = EditorCommonUtility.AssetToFilePath(configPath);
        if (File.Exists(filePath))
        {
            var serializer = new XmlSerializer(typeof(List<BuiltinFileRecord>));
            using (var fs = File.OpenRead(filePath))
            {
                result = serializer.Deserialize(fs) as List<BuiltinFileRecord>;
            }

            if (result == null)
                Debug.LogError("Builtin Assets Config 反序列化失败!");
        }
        else
            Debug.LogError("Builtin Assets Config 文件未建立!");

        return result;
    }
}

[Serializable]
public class BuiltinFileRecord
{
    private static readonly PropertyInfo _inspectorMode =
        typeof(SerializedObject).GetProperty("inspectorMode", BindingFlags.NonPublic | BindingFlags.Instance);

    public string assetType;

    public string name;
    public string replacePath;

    public long sourceId;

    // 反序列化接口
    public BuiltinFileRecord()
    {
    }

    public BuiltinFileRecord(Object oldAsset, Object replaceAsset)
    {
        name = oldAsset.name;
        assetType = oldAsset.GetType().ToString();
        sourceId = GetFileId(oldAsset);
        replacePath = AssetDatabase.GetAssetPath(replaceAsset);
    }

    public static long GetFileId(Object asset)
    {
        var serializedObject = new SerializedObject(asset);
        _inspectorMode.SetValue(serializedObject, InspectorMode.Debug, null);
        var localIdProp = serializedObject.FindProperty("m_LocalIdentfierInFile");
        return localIdProp.longValue;
    }
}