using System.Collections;
using System.Collections.ObjectModel;
using System.Text;

using anbs_cp.Classes;
using anbs_cp.Classes.Encrypt;

using FileSplitter.Shared.Enums;
using FileSplitter.Shared.Interfaces;

using Newtonsoft.Json;

namespace FileSplitter.Shared.Classes;

/// <summary>
/// Класс операций
/// </summary>
public static class FileSplitterClass
{
    /// <summary>
    /// Событие прогресса
    /// </summary>
    public static event EventHandler<FileSplitterOnProgress>? OnProgress;

    /// <summary>
    /// Получает информацию о файле, который готовится к разбиению
    /// </summary>
    /// <param name="fileName">Имя файла</param>
    /// <param name="partSize">Размер части</param>
    /// <returns>Информация о файле (<see cref="TargetFileInfo"/>)</returns>
    public static TargetFileInfo GetSplitFileInfo (string fileName, long partSize)
    {
        //Получаю размер текущего файла
        long fileSize = FileExtension.FileSize(fileName);

        //Получаю информацию о целевом файле
        return new()
        {
            //Имя файла
            FileName = Path.GetFileName(fileName),
            //Размер файла
            FileSize = fileSize,
            //md5-сумма файла (хэш)
            FileHash = new FileHash(fileName).Hash,
            //Количество кусков
            SplitCount = (long)Math.Round(fileSize / (decimal)partSize, MidpointRounding.ToPositiveInfinity)
        };
    }

    /// <summary>
    /// Разделение файла на части
    /// </summary>
    /// <param name="fileName">Имя файла для разделения</param>
    /// <param name="targetDir">Папка, в которую нужно сохранить файлы</param>
    /// <param name="partSize">Размер части в байтах</param>
    /// <param name="options">Параметры разбиения</param>
    /// <returns>Код возврата</returns>
    public static FsExitCode SplitFile (string fileName, string targetDir, long partSize, ReadOnlyCollection<FileSplitOptions> options)
    {
        //Задаю обработчик для распаковки
        EventHandler<FileSplitterOnProgress>? onProgressEvent = OnProgress;

        //Инициализирующее сообщение
        onProgressEvent?.Invoke(null,
            new(0, 0,
                string.Format(Localization.MessageStartSplit, fileName,
                    new FileSizeConverter(Localization.FileSizeStrings).Convert(partSize),
                    targetDir
                    )));

        //Создаю результат
        FileSplitInfo splitInfo = new()
        {
            FileSplitterVersion = FileSplitterConstants.CurrentSplitterVersion
        };

        //Если целевая папка не создана
        if (!Directory.Exists(targetDir))
            //- то создаём её
            Directory.CreateDirectory(targetDir);

        //Получаю размер текущего файла
        long fileSize = FileExtension.FileSize(fileName);

        //Получаю информацию о целевом файле
        splitInfo.TargetFile = GetSplitFileInfo(fileName, partSize);

        //Открываю файл
        BinaryReader fileReader = new(File.Open(fileName, FileMode.Open));

        //Разделяю файлы
        for (int i = 0; i < splitInfo.TargetFile.SplitCount; i++)
        {
            //Сообщение прогресса
            onProgressEvent?.Invoke(null,
                new(i + 1, splitInfo.TargetFile.SplitCount,
                    string.Format(Localization.MessageSplitFilePart, i + 1, splitInfo.TargetFile.SplitCount)));

            //Имя части
            string partName = $"{Path.GetFileName(fileName)}.part{i + 1}.fsf";
            //Полный путь к файлу части
            string partFileName = $"{LikeDelphi.IncludeTrailingBackslash(targetDir)}{partName}";

            //Смещение части от начала исходного файла
            int startPos = (int)(i * partSize);

            //Размер части
            long bufferSize = fileSize - startPos > 0 ? partSize : partSize + (fileSize - startPos);

            //Считываю значения
            byte[] buffer = fileReader.ReadBytes((int)bufferSize);

            //Записываю значение в файл
            //- открываю файл
            BinaryWriter fileWriter = new(File.Create(partFileName, (int)bufferSize));
            //- записываю файл
            fileWriter.Write(buffer);
            //- закрываю файл
            fileWriter.Close();

            //Создаю информацию о части
            SplitFile fileInfo = new()
            {
                Num = i + 1,
                Offset = startPos,
                Size = new FileInfo(partFileName).Length,
                Hash = new FileHash(partFileName).Hash
            };

            //Добавляю в файл информации
            splitInfo.SplitFiles.Add(fileInfo);
        }

        //Закрываю исходный файл
        fileReader.Close();
        //- и освобождаю память
        fileReader.Dispose();

        //Инициализирующее сообщение
        onProgressEvent?.Invoke(null,
            new(0, 0,
                string.Format(Localization.MessageSplitComplete, fileName)));

        //Проверка сумм
        if (!options.Contains(FileSplitOptions.fsoNoCheckHash))
            //- и если не прошли проверку
            if (!CheckHash(targetDir, Path.GetFileName(fileName), splitInfo.SplitFiles))
            {
                //- то удаляем файлы разбиения
                DeleteSplitFiles(Path.GetFileName(fileName), targetDir, splitInfo.TargetFile.SplitCount);

                //- возвращаю ошибку хэша
                return FsExitCode.ecSplitFileHashError;
            }

        //Имя файла информации о разбиении
        string reportFileName = $"{LikeDelphi.IncludeTrailingBackslash(targetDir)}{Path.GetFileName(fileName)}.fsi";

        //Записываю информацию о разбиении
        //- создаю файл
        StreamWriter writer = new(reportFileName, Encoding.UTF8, new() { Access = FileAccess.Write, Mode = FileMode.Create });
        //- создаю данные для записи файла
        string data = options.Contains(FileSplitOptions.fsoEncryptInfoFile)
            ? StringEncrypt.Encrypt(JsonConvert.SerializeObject(splitInfo))
            : JsonConvert.SerializeObject(splitInfo);
        //- записываю в файл
        writer.WriteLine(data);
        //- закрываю файл
        writer.Close();
        //- освобождаю память
        writer.Dispose();

        //Если в опциях активирована опция удаления исходного файла
        if (options.Contains(FileSplitOptions.fsoDeleteSourceFileAfterSplit))
            //- то удаляю его
            File.Delete(fileName);

        //Закрываю консоль с успехом
        return FsExitCode.ecAllDone;
    }

    /// <summary>
    /// Объединение разъединенного файла
    /// </summary>
    /// <param name="infoFileName">Имя файла с информацией о разделении</param>
    /// <param name="targetFileName">Целевой файл</param>
    /// <param name="options">Параметры объединения</param>
    /// <returns>Код возврата</returns>
    public static FsExitCode JoinFile (string infoFileName, string targetFileName, ReadOnlyCollection<FileJoinOptions> options)
    {
        //Задаю обработчик для распаковки
        EventHandler<FileSplitterOnProgress>? onProgressEvent = OnProgress;

        //Инициализирующее сообщение
        onProgressEvent?.Invoke(null,
            new(0, 0,
                string.Format(Localization.MessageStartJoin, infoFileName, targetFileName)));

        //Открываю файл для чтения
        StreamReader reader = new(infoFileName, Encoding.UTF8);

        //Читаю строку
        string tLine = reader.ReadLine() ?? "{}";

        //Освобождаю reader
        reader.Close();
        reader.Dispose();

        //Содержимое файла информации о разбиении
        string jsonData = options.Contains(FileJoinOptions.fjoInfoFileIsEncrypted)
            ? StringEncrypt.Decrypt(tLine)
            : tLine;

        //Создаю переменную для информации о разбиении
        FileSplitInfo splitInfo = new();

        try
        {
            //Получаю информацию о разбиении
            splitInfo = JsonConvert.DeserializeObject<FileSplitInfo>(jsonData) ?? splitInfo;
        }
        catch (Exception)
        {
            onProgressEvent?.Invoke(null,
                new(0, 0, Localization.MessageJoinUnknownFile));
            return FsExitCode.ecUnknownInfoFile;
        }

        //Проверяю версию
        if (!options.Contains(FileJoinOptions.fjoIgnoreSupportedScriptVersion) &&
            !FileSplitterConstants.SupportSplitterVersion.Contains(splitInfo.FileSplitterVersion))
        {
            //Вывожу сообщение
            onProgressEvent?.Invoke(null,
                new(0, 0, string.Format(Localization.MessageJoinUnsupportedFile, splitInfo.FileSplitterVersion)));

            //Прерываю
            return FsExitCode.ecInfoVersionUnsupported;
        }

        //Получаю папку с разделёнными файлами
        string sourceDir = LikeDelphi.IncludeTrailingBackslash(Path.GetDirectoryName(infoFileName) ?? "");

        //Проверяю хэш
        if (!options.Contains(FileJoinOptions.fjoNoCheckHash) && !CheckHash(sourceDir,
                Path.GetFileName(splitInfo.TargetFile.FileName), splitInfo.SplitFiles))
        {
            //Вывожу сообщение
            onProgressEvent?.Invoke(null, new(0, 0, Localization.MessageJoinHashError));

            //Прерываю
            return FsExitCode.ecSplitFileHashError;
        }

        //Открываю файл
        BinaryWriter fileWriter = new(File.Create(targetFileName, (int)splitInfo.TargetFile.FileSize));

        //Объединяю файлы
        for (int i = 1; i <= splitInfo.TargetFile.SplitCount; i++)
        {
            //Вывожу сообщение
            onProgressEvent?.Invoke(null,
                new(i, splitInfo.TargetFile.SplitCount,
                    string.Format(Localization.MessageJoinFilePart, i, splitInfo.TargetFile.SplitCount)));

            //Имя файла части
            string partFileName = $"{sourceDir}{Path.GetFileName(splitInfo.TargetFile.FileName)}.part{i}.fsf";

            //Параметры части
            ISplitFile partInfo = splitInfo.SplitFiles.FirstOrDefault(file => file.Num == i) ?? new SplitFile();

            //Читаю часть
            //- открываю файл для чтения
            BinaryReader fileReader = new(File.Open(partFileName, FileMode.Open));
            //- считываю содержимое
            byte[] buffer = fileReader.ReadBytes((int)partInfo.Size);
            //- закрываю файл
            fileReader.Close();
            //- освобождаю память
            fileReader.Dispose();

            //Записываю часть в общий файл
            fileWriter.Write(buffer);
        }

        //Закрываю целевой файл
        fileWriter.Close();
        //Освобождаю память
        fileWriter.Dispose();

        //Проверяю объединённый файл
        //- получаю хэш
        IStructuralEquatable fileHashEq = new FileHash(targetFileName).Hash;

        //- проверяю
        if (
            //-- проверка не отключена опцией
            !options.Contains(FileJoinOptions.fjoSkipCheckJoinedFile) &&
            //-- совпадает ли размер
            FileExtension.FileSize(targetFileName) != splitInfo.TargetFile.FileSize &&
            //-- совпадает ли хэш
            !fileHashEq.Equals(splitInfo.TargetFile.FileHash, StructuralComparisons.StructuralEqualityComparer)
            )
        {
            //Вывожу сообщение
            onProgressEvent?.Invoke(null, new(0, 0, Localization.MessageJoinCheckError));

            //Прерываю
            return FsExitCode.ecTargetFileCheckError;
        }

        //Вывожу сообщение
        onProgressEvent?.Invoke(null, new(0, 0, Localization.MessageJoinComplete));

        //Удаляю разделённые файлы
        if (options.Contains(FileJoinOptions.fjoDeleteSourceFilesAfterJoin))
            DeleteSplitFiles(splitInfo.TargetFile.FileName, sourceDir, splitInfo.TargetFile.SplitCount);

        //Возвращаю успех
        return FsExitCode.ecAllDone;
    }

    /// <summary>
    /// Удаление разделённых файлов
    /// </summary>
    /// <param name="fileName">Исходное имя файла (без пути!)</param>
    /// <param name="targetDir">Целевая директория</param>
    /// <param name="splitCount">Число разбиения</param>
    private static void DeleteSplitFiles (string fileName, string targetDir, long splitCount)
    {
        //Перебираю части
        for (int i = 1; i < splitCount; i++)
        {
            //Имя части
            string partName = $"{fileName}.part{i}.fsf";
            //Полный путь к файлу части
            string partFileName = $"{LikeDelphi.IncludeTrailingBackslash(targetDir)}{partName}";

            //Удаляю файл
            File.Delete(partFileName);
        }

        //Имя файла информации о разбиении
        string reportFileName = $"{LikeDelphi.IncludeTrailingBackslash(targetDir)}{Path.GetFileName(fileName)}.fsi";

        //Удаляю файл информации
        File.Delete(reportFileName);
    }

    /// <summary>
    /// Проверяет соответствие хэша файлов
    /// </summary>
    /// <param name="baseDir">Базовая папка с файлами</param>
    /// <param name="baseName">Базовое имя файлов</param>
    /// <param name="fileList">Список параметров файлов разбиения</param>
    /// <returns>Прошли ли файлы проверку</returns>
    private static bool CheckHash (string baseDir, string baseName, List<SplitFile> fileList)
    {
        //Задаю обработчик для распаковки
        EventHandler<FileSplitterOnProgress>? onProgressEvent = OnProgress;

        //Инициализирующее сообщение
        onProgressEvent?.Invoke(null,
            new(0, 0,
                Localization.MessageHashCheckStart));

        //Перебираю все части
        foreach (SplitFile file in fileList)
        {
            //Полный путь к файлу части
            string partFileName = $"{LikeDelphi.IncludeTrailingBackslash(baseDir)}{baseName}.part{file.Num}.fsf";

            //Получаю хэш
            IStructuralEquatable fileHashEq = new FileHash(partFileName).Hash;

            //Если не равен
            if (!fileHashEq.Equals(file.Hash, StructuralComparisons.StructuralEqualityComparer))
            {
                //- то сообщаю
                onProgressEvent?.Invoke(null,
                    new(0, 0,
                       string.Format(Localization.MessageHashCheckFail, partFileName)));

                //- и прерываю
                return false;
            }
        }

        //Везде хэш был равен
        //- выдаю сообщение
        onProgressEvent?.Invoke(null,
            new(0, 0,
                Localization.MessageHashCheckComplete));

        //- и возвращаю результат
        return true;
    }
}