using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Xml.Serialization;
namespace OpenCVStudio_v0
{
    public sealed class ProjectManager
    {
        private const string ProjectFileName = "project.xml";

        private const string ImagesFolderName = "Images";
        private const string ResultsFolderName = "Results";
        private const string BlocksFolderName = "Blocks";
        private const string SettingsFolderName = "Settings";
        public OpenCvStudioProject? CurrentProject { get; private set; }
        public string? CurrentProjectXmlPath { get; private set; }
        public string? CurrentProjectDirectory { get { if (string.IsNullOrWhiteSpace(CurrentProjectXmlPath)) return null; return Path.GetDirectoryName(CurrentProjectXmlPath); } }
        public bool HasProject => CurrentProject != null;
        public bool IsModified { get; private set; }
        public event EventHandler? ProjectChanged; public event EventHandler? ModifiedChanged;
        public void CreateNewProject(string parentDirectory, string projectName)
        {
            if (string.IsNullOrWhiteSpace(parentDirectory))
                throw new ArgumentException("Не указана папка хранения проекта.", nameof(parentDirectory));
            if (string.IsNullOrWhiteSpace(projectName))
                throw new ArgumentException("Не указано имя проекта.", nameof(projectName));
            string safeProjectName = MakeSafeFileName(projectName);
            if (string.IsNullOrWhiteSpace(safeProjectName))
            {
                throw new ArgumentException("Имя проекта содержит недопустимые символы.", nameof(projectName));
            }
            string projectDirectory = Path.Combine(parentDirectory, safeProjectName);
            if (Directory.Exists(projectDirectory))
            {
                throw new IOException($"Папка проекта уже существует:\n{projectDirectory}");
            }
            Directory.CreateDirectory(projectDirectory);
            Directory.CreateDirectory(Path.Combine(projectDirectory, ImagesFolderName));
            Directory.CreateDirectory(Path.Combine(projectDirectory, ResultsFolderName));
            Directory.CreateDirectory(Path.Combine(projectDirectory, BlocksFolderName));
            Directory.CreateDirectory(Path.Combine(projectDirectory, SettingsFolderName));
            CurrentProjectXmlPath = Path.Combine(projectDirectory, ProjectFileName);
            CurrentProject = new OpenCvStudioProject
            {
                FormatVersion = 1,
                ProjectName = projectName.Trim(),
                ProjectDirectoryName = safeProjectName,
                CreatedAt = DateTime.Now,
                UpdatedAt = DateTime.Now,
                InputImageRelativePath = string.Empty,
                Blocks = new List<ProjectBlock>()
            };
            SetModified(true);
            OnProjectChanged();
            SaveProject();
        }
        public void OpenProject(string projectXmlPath)
        {
            if (string.IsNullOrWhiteSpace(projectXmlPath)) throw new ArgumentException("Не указан путь к XML проекта.", nameof(projectXmlPath));
            if (!File.Exists(projectXmlPath)) throw new FileNotFoundException("Файл проекта не найден.", projectXmlPath);
            XmlSerializer serializer = new XmlSerializer(typeof(OpenCvStudioProject));
            using FileStream stream = new FileStream(projectXmlPath, FileMode.Open, FileAccess.Read, FileShare.Read);
            object? result = serializer.Deserialize(stream);
            if (result is not OpenCvStudioProject project) throw new InvalidDataException("Не удалось прочитать проект.");
            project.Blocks ??= new List<ProjectBlock>();
            foreach (ProjectBlock block in project.Blocks) block.Parameters ??= new List<BlockParameter>();
            CurrentProject = project; CurrentProjectXmlPath = Path.GetFullPath(projectXmlPath);
            NormalizeBlockOrder();
            SetModified(false); OnProjectChanged();
        }
        public void SaveProject()
        {
            EnsureProjectIsOpen();
            if (string.IsNullOrWhiteSpace(CurrentProjectXmlPath)) throw new InvalidOperationException("Не задан путь сохранения проекта.");
            string? projectDirectory = Path.GetDirectoryName(CurrentProjectXmlPath);
            if (string.IsNullOrWhiteSpace(projectDirectory)) throw new InvalidOperationException("Не удалось определить папку проекта.");
            Directory.CreateDirectory(projectDirectory);
            Directory.CreateDirectory(Path.Combine(projectDirectory, ImagesFolderName));
            Directory.CreateDirectory(  Path.Combine(projectDirectory, ResultsFolderName));
            Directory.CreateDirectory( Path.Combine(projectDirectory, BlocksFolderName));
            Directory.CreateDirectory(  Path.Combine(projectDirectory, SettingsFolderName));
            NormalizeBlockOrder(); CurrentProject!.UpdatedAt = DateTime.Now;
            XmlSerializer serializer = new XmlSerializer(typeof(OpenCvStudioProject));
            string temporaryPath = CurrentProjectXmlPath + ".tmp";
            using (FileStream stream = new FileStream(temporaryPath, FileMode.Create, FileAccess.Write, FileShare.None)) serializer.Serialize(stream, CurrentProject);
            File.Move(temporaryPath, CurrentProjectXmlPath, overwrite: true);
            SetModified(false); OnProjectChanged();
        }
        public string AddInputImage(string sourceImagePath)
        {
            EnsureProjectIsOpen();
            if (string.IsNullOrWhiteSpace(sourceImagePath)) throw new ArgumentException("Не указан путь к изображению.", nameof(sourceImagePath));
            if (!File.Exists(sourceImagePath)) throw new FileNotFoundException("Изображение не найдено.", sourceImagePath);
            string extension = Path.GetExtension(sourceImagePath).ToLowerInvariant();
            string[] allowedExtensions = { ".png", ".jpg", ".jpeg", ".bmp", ".tif", ".tiff" };
            if (!allowedExtensions.Contains(extension)) throw new NotSupportedException($"Формат изображения {extension} пока не поддерживается.");
            string projectDirectory = CurrentProjectDirectory ?? throw new InvalidOperationException("Не определена папка проекта.");
            string imagesDirectory = Path.Combine(projectDirectory, ImagesFolderName);
            Directory.CreateDirectory(imagesDirectory);
            string destinationFileName = "input" + extension;
            string destinationPath = Path.Combine(imagesDirectory, destinationFileName);
            string fullSourcePath = Path.GetFullPath(sourceImagePath);
            string fullDestinationPath = Path.GetFullPath(destinationPath);
            if (!string.Equals(fullSourcePath, fullDestinationPath, StringComparison.OrdinalIgnoreCase)) File.Copy(fullSourcePath, fullDestinationPath, overwrite: true);
            CurrentProject!.InputImageRelativePath = Path.Combine(ImagesFolderName, destinationFileName);
            SetModified(true); OnProjectChanged();
            return fullDestinationPath;
        }

        public string AddBlockImage(Guid blockId, string sourceImagePath, string destinationBaseName)
        {
            EnsureProjectIsOpen();
            if (string.IsNullOrWhiteSpace(sourceImagePath))
            {
                throw new ArgumentException("Не указан путь к изображению.", nameof(sourceImagePath));
            }
            if (!File.Exists(sourceImagePath))
            {
                throw new FileNotFoundException("Выбранное изображение не найдено.", sourceImagePath);
            }
            if (string.IsNullOrWhiteSpace(destinationBaseName))
            {
                throw new ArgumentException("Не указано имя файла изображения.", nameof(destinationBaseName));
            }
            ProjectBlock block = GetBlock(blockId) ?? throw new KeyNotFoundException($"Блок {blockId} не найден.");
            string extension = Path.GetExtension(sourceImagePath).ToLowerInvariant();
            string[] allowedExtensions = { ".png", ".jpg", ".jpeg", ".bmp", ".tif", ".tiff" };
            if (!allowedExtensions.Contains(extension))
            {
                throw new NotSupportedException($"Формат изображения {extension} пока не поддерживается.");
            }
            string projectDirectory = CurrentProjectDirectory ?? throw new InvalidOperationException("Не определена папка проекта.");
            string imagesDirectory = Path.Combine(projectDirectory, ImagesFolderName);
            Directory.CreateDirectory(imagesDirectory);
            string destinationFileName = destinationBaseName + extension;
            string destinationPath = Path.Combine(imagesDirectory, destinationFileName);
            /*
             * Получаем ранее сохранённый файл этого блока.
             * Если расширение изменилось, старый файл удалим.
             */
            string oldRelativePath = GetBlockParameter(block, "ImageRelativePath");
            string? oldFullPath = null;
            if (!string.IsNullOrWhiteSpace(oldRelativePath))
            {
                oldFullPath = Path.GetFullPath(Path.Combine(projectDirectory, oldRelativePath));
            }
            string fullSourcePath = Path.GetFullPath(sourceImagePath);
            string fullDestinationPath = Path.GetFullPath(destinationPath);
            /*
             * Если пользователь выбрал файл, который уже
             * находится в нужном месте, копировать его
             * поверх самого себя нельзя.
             */
            if (!string.Equals(fullSourcePath, fullDestinationPath, StringComparison.OrdinalIgnoreCase))
            {
                File.Copy(fullSourcePath, fullDestinationPath, overwrite: true);
            }
            /*
             * Например, раньше был main_source.png,
             * а теперь выбран main_source.jpg.
             */
            if (!string.IsNullOrWhiteSpace(oldFullPath) &&
                !string.Equals(oldFullPath, fullDestinationPath, StringComparison.OrdinalIgnoreCase) &&
                File.Exists(oldFullPath) &&
                IsFileInsideDirectory(oldFullPath, imagesDirectory))
            {
                File.Delete(oldFullPath);
            }
            string relativePath = Path.GetRelativePath(projectDirectory, fullDestinationPath);
            SetBlockParameter(block.Id, "ImageRelativePath", relativePath);
            return fullDestinationPath;
        }

        private static string GetBlockParameter(ProjectBlock block, string parameterName)
        {
            if (block.Parameters == null)
            {
                return string.Empty;
            }
            BlockParameter? parameter = block.Parameters.FirstOrDefault(item => string.Equals(item.Name, parameterName, StringComparison.OrdinalIgnoreCase));
            return parameter?.Value ?? string.Empty;
        }

        private static bool IsFileInsideDirectory(string filePath, string directoryPath)
        {
            string fullFilePath = Path.GetFullPath(filePath);
            string fullDirectoryPath = Path.GetFullPath(directoryPath).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar) + Path.DirectorySeparatorChar;
            return fullFilePath.StartsWith(fullDirectoryPath, StringComparison.OrdinalIgnoreCase);
        }
        public string? GetInputImageFullPath()
        {
            if (CurrentProject == null || string.IsNullOrWhiteSpace(CurrentProject.InputImageRelativePath)) return null;
            string? projectDirectory = CurrentProjectDirectory;
            if (string.IsNullOrWhiteSpace(projectDirectory)) return null;
            return Path.GetFullPath(Path.Combine(projectDirectory, CurrentProject.InputImageRelativePath));
        }
        /// <summary>
        /// Возвращает полный путь к изображению,
        /// выбранному для конкретного блока-источника.
        /// </summary>
        public string? GetBlockImageFullPath(Guid blockId)
        {
            if (CurrentProject == null)
            {
                return null;
            }
            ProjectBlock? block = GetBlock(blockId);
            if (block == null)
            {
                return null;
            }
            string relativePath = GetBlockParameter(blockId, "ImageRelativePath", string.Empty);
            if (string.IsNullOrWhiteSpace(relativePath))
            {
                return null;
            }
            string? projectDirectory = CurrentProjectDirectory;
            if (string.IsNullOrWhiteSpace(projectDirectory))
            {
                return null;
            }
            string fullPath = Path.GetFullPath(Path.Combine(projectDirectory, relativePath));
            /*
             * Защита от ошибочного или вручную
             * изменённого пути в XML.
             */
            string fullProjectDirectory = Path.GetFullPath(projectDirectory).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar) + Path.DirectorySeparatorChar;
            if (!fullPath.StartsWith(fullProjectDirectory, StringComparison.OrdinalIgnoreCase))
            {
                throw new InvalidDataException("Путь к изображению выходит за пределы папки проекта.");
            }
            return fullPath;
        }


        public ProjectBlock AddBlock(string blockType, string blockName, int? insertIndex = null)
        {
            EnsureProjectIsOpen();
            if (string.IsNullOrWhiteSpace(blockType))
            {
                throw new ArgumentException("Не указан тип блока.", nameof(blockType));
            }
            /*
             * Перед вставкой приводим список к фактическому
             * порядку, записанному в свойствах Order.
             */
            NormalizeBlockOrder();
            ProjectBlock block = new ProjectBlock
            {
                Id = Guid.NewGuid(),
                BlockType = blockType.Trim(),
                Name = string.IsNullOrWhiteSpace(blockName) ? blockType.Trim() : blockName.Trim(),
                IsEnabled = true,
                Order = 0,
                Parameters = new List<BlockParameter>()
            };
            if (insertIndex.HasValue)
            {
                int safeIndex = Math.Clamp(insertIndex.Value, 0, CurrentProject!.Blocks.Count);
                CurrentProject.Blocks.Insert(safeIndex, block);
            }
            else
            {
                CurrentProject!.Blocks.Add(block);
            }
            /*
             * После Insert назначаем последовательные Order
             * согласно фактическому порядку элементов списка.
             */
            ReindexBlocks();
            SetModified(true);
            OnProjectChanged();
            return block;
        }

        private void ReindexBlocks()
        {
            if (CurrentProject == null)
            {
                return;
            }

            for (int index = 0;
                 index < CurrentProject.Blocks.Count;
                 index++)
            {
                CurrentProject.Blocks[index].Order = index;
            }
        }
        public bool RemoveBlock(Guid blockId)
        {
            EnsureProjectIsOpen();
            ProjectBlock? block = CurrentProject!.Blocks.FirstOrDefault(item => item.Id == blockId);
            if (block == null) return false;
            CurrentProject.Blocks.Remove(block);
            NormalizeBlockOrder();
            SetModified(true); OnProjectChanged();
            return true;
        }
        public ProjectBlock? GetBlock(Guid blockId)
        {
            if (CurrentProject == null) return null;
            return CurrentProject.Blocks.FirstOrDefault(block => block.Id == blockId);
        }
        public void SetBlockParameter(Guid blockId, string parameterName, object? value)
        {
            EnsureProjectIsOpen();
            ProjectBlock block = GetBlock(blockId) ?? throw new KeyNotFoundException($"Блок {blockId} не найден.");
            BlockParameter? parameter = block.Parameters.FirstOrDefault(item => string.Equals(item.Name, parameterName, StringComparison.OrdinalIgnoreCase));
            string valueText = Convert.ToString(value, System.Globalization.CultureInfo.InvariantCulture) ?? string.Empty;
            string valueType = value?.GetType().FullName ?? typeof(string).FullName!;
            if (parameter == null) { parameter = new BlockParameter { Name = parameterName, Value = valueText, ValueType = valueType }; block.Parameters.Add(parameter); }
            else { parameter.Value = valueText; parameter.ValueType = valueType; }
            SetModified(true); OnProjectChanged();
        }
        public T GetBlockParameter<T>(Guid blockId, string parameterName, T defaultValue)
        {
            ProjectBlock? block = GetBlock(blockId);
            if (block == null) return defaultValue;
            BlockParameter? parameter = block.Parameters.FirstOrDefault(item => string.Equals(item.Name, parameterName, StringComparison.OrdinalIgnoreCase));
            if (parameter == null || string.IsNullOrWhiteSpace(parameter.Value)) return defaultValue;
            try
            {
                Type targetType = Nullable.GetUnderlyingType(typeof(T)) ?? typeof(T);
                if (targetType.IsEnum) { object enumValue = Enum.Parse(targetType, parameter.Value, ignoreCase: true); return (T)enumValue; }
                object? convertedValue = Convert.ChangeType(parameter.Value, targetType, System.Globalization.CultureInfo.InvariantCulture);
                return convertedValue is T typedValue ? typedValue : defaultValue;
            }
            catch { return defaultValue; }
        }


        public bool MoveBlockUp(Guid blockId)
        {
            EnsureProjectIsOpen();
            NormalizeBlockOrder();
            int index = CurrentProject!.Blocks.FindIndex(block => block.Id == blockId);
            if (index <= 0)
            {
                return false;
            }
            ProjectBlock block = CurrentProject.Blocks[index];
            CurrentProject.Blocks.RemoveAt(index);
            CurrentProject.Blocks.Insert(index - 1, block);
            ReindexBlocks();
            SetModified(true);
            OnProjectChanged();
            return true;
        }
        public bool MoveBlockDown(Guid blockId)
        {
            EnsureProjectIsOpen();
            NormalizeBlockOrder();
            int index = CurrentProject!.Blocks.FindIndex(block => block.Id == blockId);
            if (index < 0 || index >= CurrentProject.Blocks.Count - 1)
            {
                return false;
            }
            ProjectBlock block = CurrentProject.Blocks[index];
            CurrentProject.Blocks.RemoveAt(index);
            CurrentProject.Blocks.Insert(index + 1, block);

            ReindexBlocks();
            SetModified(true);
            OnProjectChanged();
            return true;
        }
        public void CloseProject()
        {
            CurrentProject = null; CurrentProjectXmlPath = null;
            SetModified(false); OnProjectChanged();
        }
        private void NormalizeBlockOrder()
        {
            if (CurrentProject == null)
            {
                return;
            }
            CurrentProject.Blocks = CurrentProject.Blocks.OrderBy(block => block.Order).ToList();
            ReindexBlocks();
        }
        private void EnsureProjectIsOpen()
        {
            if (CurrentProject == null) throw new InvalidOperationException("Сначала создайте или откройте проект.");
        }
        private void SetModified(bool value)
        {
            if (IsModified == value) return;
            IsModified = value; ModifiedChanged?.Invoke(this, EventArgs.Empty);
        }
        private void OnProjectChanged()
        {
            ProjectChanged?.Invoke(this, EventArgs.Empty);
        }
        private static string MakeSafeFileName(string value)
        {
            char[] invalidCharacters = Path.GetInvalidFileNameChars();
            string safeName = new string(value.Trim().Select(character => invalidCharacters.Contains(character) ? '_' : character).ToArray());
            return safeName.Trim('.', ' ');
        }
    }
    [Serializable]
    [XmlRoot("OpenCvStudioProject")]
    public sealed class OpenCvStudioProject
    {
        [XmlAttribute] public int FormatVersion { get; set; } = 1;
        public string ProjectName { get; set; } = string.Empty;

        public string ProjectDirectoryName { get; set; } = string.Empty;
        public DateTime CreatedAt { get; set; }
        public DateTime UpdatedAt { get; set; }
        public string InputImageRelativePath { get; set; } = string.Empty;
        [XmlArray("Blocks")][XmlArrayItem("Block")] public List<ProjectBlock> Blocks { get; set; } = new();
    }
    [Serializable]
    public sealed class ProjectBlock
    {
        [XmlAttribute] public Guid Id { get; set; } = Guid.NewGuid();
        [XmlAttribute] public int Order { get; set; }
        [XmlAttribute] public bool IsEnabled { get; set; } = true;
        public string BlockType { get; set; } = string.Empty;
        public string Name { get; set; } = string.Empty;
        [XmlArray("Parameters")][XmlArrayItem("Parameter")] public List<BlockParameter> Parameters { get; set; } = new();
        public override string ToString() { return IsEnabled ? $"{Order + 1}. {Name}" : $"{Order + 1}. {Name} (выключен)"; }
    }
    [Serializable]
    public sealed class BlockParameter
    {
        [XmlAttribute] public string Name { get; set; } = string.Empty;
        [XmlAttribute] public string ValueType { get; set; } = typeof(string).FullName!;
        [XmlText] public string Value { get; set; } = string.Empty;
        public override string ToString() { return $"{Name} = {Value}"; }
    }
}