﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using RkokoExcel2Json.Free;
using UnityEditor;
using UnityEngine;
using Debug = UnityEngine.Debug;

namespace RKOKO.Config.Free.Editor
{
    public class Exporter
    {
        private readonly List<string> _exportInfoLogs = new List<string>();
        private readonly List<string> _exportErrorLogs = new List<string>();

        public class ExporterOption
        {
            public string JsonExportDir { get; set; }
            public string CsharpExportDir { get; set; }
            public string ExcelDir { get; set; }
            public string OutputDir { get; set; }
            public bool EncryptJson { get; set; } = false;
            public string JsonEncryptKey { get; set; } = "123456";
            public bool BackUp { get; set; } = false;
            public int MaxKeepBackUpCount { get; set; } = 100;
            public bool IsLoadResourcesJson { get; set; } = false;
        }

        public void Export()
        {
            ResetExportLogs();

            //判断路径是否合法
            var jsonExportDir = Preferences.Default.CustomJsonExportPath;
            if (Preferences.Default.JsonLoadMode == JsonLoadMode.Resources)
            {
                jsonExportDir = Path.GetDirectoryName(Paths.RESOURCES_JSON_FILE_PATH);
            }

            var csharpExportDir = Preferences.Default.CustomCsharpExportPath;
            if (Preferences.Default.CSharpExportMode == CSharpExportMode.Default)
            {
                csharpExportDir = Paths.DEFAULT_CSHARP_EXPORT_PATH;
            }

            if (!IsPathInsideAssets(jsonExportDir))
            {
                ToolEditor.ShowMessageBox(EditorLocalize.Get(1017), EditorLocalize.Get(1020),
                    PreferencesWindow.Open);
                return;
            }

            if (!IsPathInsideAssets(csharpExportDir))
            {
                ToolEditor.ShowMessageBox(EditorLocalize.Get(1015), EditorLocalize.Get(1020),
                    PreferencesWindow.Open);
                return;
            }

            //构建导出配置
            var config = new
            {
                tableDir = Path.GetFullPath(Paths.TABLE_DIR),
                outputDir = Path.GetFullPath(Paths.OUTPUT_DIR),
                lan = (int)EditorLocalize.GetLanguageType(),
            };
            var tableDirString = config.tableDir;
            if (tableDirString == null)
            {
                tableDirString = string.Empty;
            }

            tableDirString = tableDirString.Replace("\\", "/");
            var outputDirString = config.outputDir;
            if (outputDirString == null)
            {
                outputDirString = string.Empty;
            }

            outputDirString = outputDirString.Replace("\\", "/");
            var configJson =
                "{\n" +
                "  \"tableDir\":\"" + EscapeJsonString(tableDirString) + "\",\n" +
                "  \"outputDir\":\"" + EscapeJsonString(outputDirString) + "\",\n" +
                "  \"lan\":" + config.lan + "\n" +
                "}";
            var option = new ExporterOption()
            {
                JsonExportDir = jsonExportDir,
                CsharpExportDir = csharpExportDir,
                ExcelDir = config.tableDir,
                OutputDir = config.outputDir,
                IsLoadResourcesJson = Preferences.Default.JsonLoadMode == JsonLoadMode.Resources,
                EncryptJson = Preferences.Default.EncryptJson,
                JsonEncryptKey = Preferences.Default.JsonEncryptKey,
                BackUp = Preferences.Default.BackUp,
                MaxKeepBackUpCount = Env.MAX_BACK_UP_COUNT,
            };
            var configPath = Path.GetFullPath(Paths.EXPORT_CONFIG_JSON);
            ToolEditor.CreateDirIfNotExists(Path.GetDirectoryName(configPath));
            File.WriteAllText(configPath, configJson);
            if (!CheckAesKey(option))
            {
                ToolEditor.ShowMessageBox(EditorLocalize.Get(1031), EditorLocalize.Get(1020),
                    PreferencesWindow.Open);
                return;
            }

            var req = new ExportRequest()
            {
                ExcelDirectory = tableDirString,
                OutputDirectory = outputDirString,
                Language = (RkokoExcel2Json.Free.LanguageType)EditorLocalize.GetLanguageType()
            };
            ExportResult result = null;
            string errorMessage = null;
            string errorDetail = null;
            LogProxy.InfoHandler -= OnLogInfo;
            LogProxy.ErrorHandler -= OnLogError;
            LogProxy.InfoHandler += OnLogInfo;
            LogProxy.ErrorHandler += OnLogError;
            try
            {
                result = RkokoExcel2JsonApi.Export(req);
            }
            catch (Exception e)
            {
                errorMessage = e.Message;
                errorDetail = e.ToString();
                AddLogError(errorDetail);
            }
            finally
            {
                LogProxy.InfoHandler -= OnLogInfo;
                LogProxy.ErrorHandler -= OnLogError;
            }

            if (result == null || !result.Success)
            {
                if (result != null)
                {
                    errorMessage = string.IsNullOrWhiteSpace(result.ErrorMessage) ? errorMessage : result.ErrorMessage;
                    errorDetail = string.IsNullOrWhiteSpace(result.ErrorDetail) ? errorDetail : result.ErrorDetail;
                }

                ShowExportReport(false, option, result, errorMessage, errorDetail);
                Debug.LogError(EditorLocalize.Get(1055));
                return;
            }

            try
            {
                if (!CheckCSharpFilesNames(option))
                {
                    errorMessage = EditorLocalize.Get(1024);
                    AddLogError(errorMessage);
                    ShowExportReport(false, option, result, errorMessage, errorMessage);
                    Debug.LogError(EditorLocalize.Get(1055));
                    return;
                }

                BackUpOldFiles(option);
                CleanOldExportFiles(option);
                CopyJsonFile(option);
                CopyCSharpFiles(option);
                GenerateConfigTablesCSharp(option);
                WriteHistory(option);
                AssetDatabase.Refresh();
                ToolEditor.Ping(Path.Combine(option.JsonExportDir, "all_in_one.json"));
                AddLogInfo(EditorLocalize.Get(1026));
                ShowExportReport(true, option, result, null, null);
            }
            catch (Exception e)
            {
                errorMessage = e.Message;
                errorDetail = e.ToString();
                AddLogError(errorDetail);
                ShowExportReport(false, option, result, errorMessage, errorDetail);
                Debug.LogError(EditorLocalize.Get(1055));
            }
        }


        private void OnLogInfo(string msg)
        {
            AddLogInfo(msg);
        }

        private void OnLogError(string err)
        {
            AddLogError(err);
        }

        private void ResetExportLogs()
        {
            _exportInfoLogs.Clear();
            _exportErrorLogs.Clear();
        }

        private void AddLogInfo(string msg)
        {
            if (!string.IsNullOrWhiteSpace(msg))
                _exportInfoLogs.Add(msg);
        }

        private void AddLogError(string err)
        {
            if (!string.IsNullOrWhiteSpace(err))
                _exportErrorLogs.Add(err);
        }

        private void ShowExportReport(bool success, ExporterOption option, ExportResult result, string errorMessage,
            string errorDetail)
        {
            var data = new RKOKOExportReportWindow.ReportData()
            {
                Success = success,
                ExcelDir = option?.ExcelDir ?? result?.ExcelDirectory,
                OutputDir = option?.OutputDir ?? result?.OutputDirectory,
                JsonExportDir = option?.JsonExportDir,
                CsharpExportDir = option?.CsharpExportDir,
                ErrorMessage = errorMessage,
                ErrorDetail = errorDetail,
                SheetCount = result?.SheetCount ?? 0,
                GeneratedFileCount = result?.GeneratedFiles?.Count ?? 0,
            };
            data.InfoLogs.AddRange(_exportInfoLogs);
            data.ErrorLogs.AddRange(_exportErrorLogs);
            LogExportReportToConsole(data);
            if (success)
            {
                LogSuccessSummary(data);
                return;
            }

            RKOKOExportReportWindow.Show(data);
        }


        private void LogExportReportToConsole(RKOKOExportReportWindow.ReportData data)
        {
            var text = RKOKOExportReportWindow.BuildReportText(data);
            if (string.IsNullOrWhiteSpace(text))
                return;
            var logText = "[RKOKO CONFIG FREE] Export Report\n" + text;
            Debug.Log(logText);
        }

        public bool CheckAesKey(ExporterOption option)
        {
            if (option.EncryptJson)
            {
                return !string.IsNullOrEmpty(option.JsonEncryptKey);
            }

            return true;
        }

        public bool CheckCSharpFilesNames(ExporterOption option)
        {
            var source = Path.Combine(option.OutputDir, "csharp");
            var files = Directory.GetFiles(source, "*.cs", SearchOption.AllDirectories);
            var fileNamesWithoutExtension = files.Select(Path.GetFileNameWithoutExtension).ToList();
            return fileNamesWithoutExtension.All(e => e != "ConfigTables");
        }

        public void CopyJsonFile(ExporterOption option)
        {
            var source = Path.Combine(option.OutputDir, "json/all_in_one.json");
            var dest = Path.Combine(option.JsonExportDir, Path.GetFileName(source));
            ToolEditor.CreateDirIfNotExists(Path.GetDirectoryName(dest));
            if (option.EncryptJson)
            {
                var encryptString = RkokoAesUtility.EncryptString(File.ReadAllText(source), option.JsonEncryptKey);
                File.WriteAllText(dest, encryptString);
            }
            else
            {
                File.Copy(source, dest, true);
            }

            AddLogInfo(EditorLocalize.GetFormat(1023, dest));
        }

        public void CopyCSharpFiles(ExporterOption option)
        {
            var source = Path.Combine(option.OutputDir, "csharp");
            var dest = option.CsharpExportDir;
            CopyDirectory(source, dest);
            var readme = Path.Combine(dest, "__RKOKO_GENERATED_README.txt");
            File.WriteAllText(readme, EditorLocalize.Get(2010));
            AddLogInfo(EditorLocalize.GetFormat(1023, dest));
        }


        public void GenerateConfigTablesCSharp(ExporterOption option)
        {
            var jsonAssetsPath = ToolEditor.ToUnityPath(Path.Combine(option.JsonExportDir, "all_in_one.json"));
            var configTablesPath = Path.Combine(option.CsharpExportDir, "ConfigTables.cs");
            Directory.CreateDirectory(Path.GetDirectoryName(configTablesPath));
            var code = File.ReadAllText(Paths.CONFIG_TABLES_TEMPLATE_FILE);
            code = code.Replace("$JSON_ASSET_PATH$", jsonAssetsPath);
            code = code.Replace("$JSON_LOAD_PATH$", Paths.RESOURCES_LOAD_JSON_PATH);
            code = code.Replace("$JSON_AES_KEY$", option.JsonEncryptKey);
            code = code.Replace("$ENABLE_RUNTIME_RESOURCES_LOAD$", option.IsLoadResourcesJson ? "true" : "false");
            code = code.Replace("$ERROR_RUNTIME_LOAD_DISABLED$", EditorLocalize.Get(2011));
            File.WriteAllText(configTablesPath, code);
            AddLogInfo(EditorLocalize.GetFormat(1023, configTablesPath));
        }

        public void BackUpOldFiles(ExporterOption option)
        {
            if (!option.BackUp)
            {
                return;
            }

            var backUpFolder = Path.Combine(Paths.BACK_UP_FOLDER, DateTime.Now.ToString("yyyyMMddHHmmss"));
            TryBackUp(Paths.DEFAULT_CSHARP_EXPORT_PATH, backUpFolder);
            TryBackUp(Path.GetDirectoryName(Paths.RESOURCES_JSON_FILE_PATH), backUpFolder);
            TryBackUp(option.JsonExportDir, backUpFolder);
            TryBackUp(option.CsharpExportDir, backUpFolder);
            if (File.Exists(Paths.EXPORT_HISTORY_MANIFEST))
            {
                var lines = File.ReadAllLines(Paths.EXPORT_HISTORY_MANIFEST);
                var folders = lines.Where(e => !string.IsNullOrEmpty(e)).ToList();
                foreach (var folder in folders)
                {
                    TryBackUp(folder, backUpFolder);
                }
            }

            TrimBackUpFolders(option.MaxKeepBackUpCount);
        }

        public void TryBackUp(string sourceFolder, string backUpFolder)
        {
            if (string.IsNullOrWhiteSpace(sourceFolder))
            {
                return;
            }

            sourceFolder = Path.GetFullPath(sourceFolder);
            if (!Directory.Exists(sourceFolder))
            {
                return;
            }

            var jsonFile = Path.Combine(sourceFolder, "all_in_one.json");
            if (File.Exists(jsonFile))
            {
                BackUpFileKeepRelativePath(jsonFile, backUpFolder);
            }

            if (IsCSharpFolder(sourceFolder))
            {
                BackUpDirectoryKeepRelativePath(sourceFolder, backUpFolder);
            }
        }

        private void BackUpFileKeepRelativePath(string sourceFilePath, string backUpFolder)
        {
            if (string.IsNullOrWhiteSpace(sourceFilePath) || !File.Exists(sourceFilePath))
            {
                return;
            }

            var relativePath = ToBackupRelativePath(sourceFilePath);
            var targetFilePath = Path.Combine(backUpFolder, relativePath);
            var targetDir = Path.GetDirectoryName(targetFilePath);
            if (!string.IsNullOrEmpty(targetDir) && !Directory.Exists(targetDir))
            {
                Directory.CreateDirectory(targetDir);
            }

            File.Copy(sourceFilePath, targetFilePath, true);
        }


        private string ToBackupRelativePath(string fullPath)
        {
            fullPath = Path.GetFullPath(fullPath).Replace("\\", "/");
            var projectRoot = Path.GetFullPath(Path.Combine(Application.dataPath, "..")).Replace("\\", "/")
                .TrimEnd('/');
            var assetsPath = Path.GetFullPath(Application.dataPath).Replace("\\", "/").TrimEnd('/');
            if (fullPath.StartsWith(assetsPath + "/", StringComparison.Ordinal))
            {
                return "Assets" + fullPath.Substring(assetsPath.Length);
            }

            if (fullPath.Equals(assetsPath, StringComparison.Ordinal))
            {
                return "Assets";
            }

            if (fullPath.StartsWith(projectRoot + "/", StringComparison.Ordinal))
            {
                var relative = fullPath.Substring(projectRoot.Length).TrimStart('/');
                return relative;
            }

            var root = Path.GetPathRoot(fullPath)?.Replace("\\", "/");
            if (!string.IsNullOrEmpty(root) && fullPath.StartsWith(root, StringComparison.Ordinal))
            {
                fullPath = fullPath.Substring(root.Length);
            }

            return fullPath.TrimStart('/');
        }

        private void BackUpDirectoryKeepRelativePath(string sourceFolder, string backUpFolder)
        {
            if (string.IsNullOrWhiteSpace(sourceFolder) || !Directory.Exists(sourceFolder))
            {
                return;
            }

            var files = Directory.GetFiles(sourceFolder, "*", SearchOption.AllDirectories);
            foreach (var file in files)
            {
                BackUpFileKeepRelativePath(file, backUpFolder);
            }
        }


        private void TrimBackUpFolders(int keepCount)
        {
            if (keepCount <= 0)
            {
                keepCount = 1;
            }

            if (!Directory.Exists(Paths.BACK_UP_FOLDER))
            {
                return;
            }

            var backUpFolders = Directory.GetDirectories(Paths.BACK_UP_FOLDER)
                .Where(Directory.Exists)
                .OrderByDescending(Path.GetFileName)
                .ToList();
            if (backUpFolders.Count <= keepCount)
            {
                return;
            }

            var removeFolders = backUpFolders.Skip(keepCount).ToList();
            foreach (var folder in removeFolders)
            {
                try
                {
                    Directory.Delete(folder, true);
                }
                catch (Exception e)
                {
                    AddLogError($"删除旧备份失败：{folder}\n{e}");
                }
            }
        }

        public void WriteHistory(ExporterOption option)
        {
            ToolEditor.CreateDirIfNotExists(Path.GetDirectoryName(Paths.EXPORT_HISTORY_MANIFEST));
            var lines = new List<string>();
            if (File.Exists(Paths.EXPORT_HISTORY_MANIFEST))
            {
                lines = File.ReadAllLines(Paths.EXPORT_HISTORY_MANIFEST).Where(e => !string.IsNullOrEmpty(e))
                    .ToList();
            }

            if (!lines.Contains(option.JsonExportDir))
            {
                lines.Add(option.JsonExportDir);
            }

            if (!lines.Contains(option.CsharpExportDir))
            {
                lines.Add(option.CsharpExportDir);
            }

            File.WriteAllLines(Paths.EXPORT_HISTORY_MANIFEST, lines);
            AddLogInfo(EditorLocalize.Get(1025));
        }

        public void CleanOldExportFiles(ExporterOption option)
        {
            TryDeleteJsonFileOrCSharpFolder(Paths.DEFAULT_CSHARP_EXPORT_PATH);
            TryDeleteJsonFileOrCSharpFolder(Path.GetDirectoryName(Paths.RESOURCES_JSON_FILE_PATH));
            TryDeleteJsonFileOrCSharpFolder(option.JsonExportDir);
            TryDeleteJsonFileOrCSharpFolder(option.CsharpExportDir);
            if (File.Exists(Paths.EXPORT_HISTORY_MANIFEST))
            {
                var lines = File.ReadAllLines(Paths.EXPORT_HISTORY_MANIFEST);
                var folders = lines.Where(e => !string.IsNullOrEmpty(e)).ToList();
                foreach (var folder in folders)
                {
                    TryDeleteJsonFileOrCSharpFolder(folder);
                }
            }

            AddLogInfo(EditorLocalize.Get(1027));
        }

        public void TryDeleteJsonFileOrCSharpFolder(string folder)
        {
            if (string.IsNullOrWhiteSpace(folder))
                return;
            var jsonFile = Path.Combine(folder, "all_in_one.json");
            if (File.Exists(jsonFile))
            {
                File.Delete(jsonFile);
                AddLogInfo(EditorLocalize.GetFormat(1021, jsonFile));
            }

            if (IsCSharpFolder(folder))
            {
                Directory.Delete(folder, true);
                AddLogInfo(EditorLocalize.GetFormat(1021, folder));
            }
        }

        public bool IsCSharpFolder(string folder)
        {
            if (string.IsNullOrWhiteSpace(folder))
                return false;
            var allTablesCs = Path.Combine(folder, "AllTables.cs");
            return File.Exists(allTablesCs);
        }

        public bool IsPathInsideAssets(string path)
        {
            if (string.IsNullOrEmpty(path))
            {
                return false;
            }

            var unityPath = ToolEditor.ToUnityPath(path);
            return unityPath == "Assets" || unityPath.StartsWith("Assets/");
        }


        public static void CopyDirectory(string sourceDir, string targetDir, bool overwrite = true)
        {
            if (string.IsNullOrWhiteSpace(sourceDir))
                throw new ArgumentException("sourceDir is null or empty", nameof(sourceDir));
            if (string.IsNullOrWhiteSpace(targetDir))
                throw new ArgumentException("targetDir is null or empty", nameof(targetDir));
            if (!Directory.Exists(sourceDir))
                throw new DirectoryNotFoundException($"source dir is not exits: {sourceDir}");
            Directory.CreateDirectory(targetDir);

            // 先复制当前目录下所有文件
            string[] files = Directory.GetFiles(sourceDir);
            foreach (string file in files)
            {
                string fileName = Path.GetFileName(file);
                string destFile = Path.Combine(targetDir, fileName);
                File.Copy(file, destFile, overwrite);
            }

            // 再递归复制所有子目录
            string[] directories = Directory.GetDirectories(sourceDir);
            foreach (string directory in directories)
            {
                string dirName = Path.GetFileName(directory);
                string destDirectory = Path.Combine(targetDir, dirName);
                CopyDirectory(directory, destDirectory, overwrite);
            }
        }

        private void LogSuccessSummary(RKOKOExportReportWindow.ReportData data)
        {
            Debug.Log(
                $"<color=#2FBF71><b>[RKOKO CONFIG FREE] {EditorLocalize.Get(1038)}</b></color>\n" +
                $"{EditorLocalize.Get(1056)}：{data.SheetCount}\n" +
                $"{EditorLocalize.Get(1057)}：{data.GeneratedFileCount}");
        }


        private static string EscapeJsonString(string value)
        {
            if (string.IsNullOrEmpty(value))
                return string.Empty;
            var sb = new System.Text.StringBuilder(value.Length + 16);
            foreach (var c in value)
            {
                switch (c)
                {
                    case '\"':
                        sb.Append("\\\"");
                        break;
                    case '\\':
                        sb.Append("\\\\");
                        break;
                    case '\b':
                        sb.Append("\\b");
                        break;
                    case '\f':
                        sb.Append("\\f");
                        break;
                    case '\n':
                        sb.Append("\\n");
                        break;
                    case '\r':
                        sb.Append("\\r");
                        break;
                    case '\t':
                        sb.Append("\\t");
                        break;
                    default:
                        if (c < 32)
                            sb.Append("\\u").Append(((int)c).ToString("x4"));
                        else
                            sb.Append(c);
                        break;
                }
            }

            return sb.ToString();
        }
    }
}