using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Input;
using Avalonia.Controls;
using Avalonia.Platform.Storage;
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using RomRepoMgr.Core.EventArgs;
using RomRepoMgr.Core.Workers;
using RomRepoMgr.Database;
using RomRepoMgr.Database.Models;
using RomRepoMgr.Models;
using RomRepoMgr.Resources;
using Serilog;
using Serilog.Extensions.Logging;
using SharpCompress.Readers;

namespace RomRepoMgr.ViewModels;

public sealed partial class ImportRomFolderViewModel : ViewModelBase
{
    readonly Context _ctx =
        Context.Create(Settings.Settings.Current.DatabasePath, new SerilogLoggerFactory(Log.Logger));
    readonly Stopwatch              _mainStopwatch = new();
    readonly ConcurrentBag<DbDisk>  _newDisks      = [];
    readonly ConcurrentBag<DbFile>  _newFiles      = [];
    readonly ConcurrentBag<DbMedia> _newMedias     = [];
    readonly Stopwatch              _stopwatch     = new();
    [ObservableProperty]
    bool _canChoose;
    [ObservableProperty]
    bool _canClose;
    [ObservableProperty]
    bool _canStart;
    [ObservableProperty]
    string _folderPath;
    [ObservableProperty]
    bool _isImporting;
    [ObservableProperty]
    bool _isReady;
    [ObservableProperty]
    bool _knownOnlyChecked;
    int _listPosition;
    [ObservableProperty]
    bool _progress2IsIndeterminate;
    [ObservableProperty]
    double _progress2Maximum;
    [ObservableProperty]
    double _progress2Minimum;
    [ObservableProperty]
    double _progress2Value;
    [ObservableProperty]
    bool _progress2Visible;
    [ObservableProperty]
    bool _progressIsIndeterminate;
    [ObservableProperty]
    double _progressMaximum;
    [ObservableProperty]
    double _progressMinimum;
    [ObservableProperty]
    double _progressValue;
    [ObservableProperty]
    bool _progressVisible;
    bool _recurseArchivesChecked;
    [ObservableProperty]
    bool _removeFilesChecked;
    [ObservableProperty]
    bool _removeFilesEnabled;
    FileImporter _rootImporter;
    [ObservableProperty]
    string _statusMessage;
    [ObservableProperty]
    string _statusMessage2;
    [ObservableProperty]
    bool _statusMessage2Visible;

    public ImportRomFolderViewModel()
    {
        SelectFolderCommand    = new AsyncRelayCommand(SelectFolderAsync);
        CloseCommand           = new RelayCommand(Close);
        StartCommand           = new RelayCommand(Start);
        CanClose               = true;
        RemoveFilesChecked     = false;
        KnownOnlyChecked       = true;
        RecurseArchivesChecked = Settings.Settings.CanDecompress;
        RemoveFilesEnabled     = false;
        CanChoose              = true;
    }

    public ICommand SelectFolderCommand { get; }
    public ICommand CloseCommand        { get; }
    public ICommand StartCommand        { get; }
    public Window   View                { get; init; }

    public bool RecurseArchivesEnabled => Settings.Settings.CanDecompress;

    public bool RecurseArchivesChecked
    {
        get => _recurseArchivesChecked;
        set
        {
            if(value) RemoveFilesChecked = false;

            RemoveFilesEnabled = !value;
            SetProperty(ref _recurseArchivesChecked, value);
        }
    }

    public ObservableCollection<RomImporter> Importers { get; } = [];

    void Start()
    {
        _rootImporter = new FileImporter(_ctx, _newFiles, _newDisks, _newMedias, KnownOnlyChecked, RemoveFilesChecked);
        _rootImporter.SetMessage += SetMessage;
        _rootImporter.SetIndeterminateProgress += SetIndeterminateProgress;
        _rootImporter.SetProgress += SetProgress;
        _rootImporter.SetProgressBounds += SetProgressBounds;
        _rootImporter.Finished += EnumeratingFilesFinished;
        ProgressIsIndeterminate = true;
        ProgressVisible = true;
        CanClose = false;
        CanStart = false;
        IsImporting = true;
        IsReady = false;
        CanChoose = false;
        _mainStopwatch.Start();

        _ = Task.Run(() => _rootImporter.FindFiles(FolderPath));
    }

    void SetProgressBounds(object sender, ProgressBoundsEventArgs e) => Dispatcher.UIThread.Post(() =>
    {
        ProgressIsIndeterminate = false;
        ProgressMaximum         = e.Maximum;
        ProgressMinimum         = e.Minimum;
    });

    void SetProgress(object sender, ProgressEventArgs e)
    {
        Dispatcher.UIThread.Post(() => ProgressValue = e.Value);
    }

    void SetIndeterminateProgress(object sender, EventArgs e)
    {
        Dispatcher.UIThread.Post(() => ProgressIsIndeterminate = true);
    }

    void SetProgress2Bounds(object sender, ProgressBoundsEventArgs e) => Dispatcher.UIThread.Post(() =>
    {
        Progress2IsIndeterminate = false;
        Progress2Maximum         = e.Maximum;
        Progress2Minimum         = e.Minimum;
    });

    void SetProgress2(object sender, ProgressEventArgs e)
    {
        Dispatcher.UIThread.Post(() => Progress2Value = e.Value);
    }

    void SetIndeterminateProgress2(object sender, EventArgs e)
    {
        Dispatcher.UIThread.Post(() => Progress2IsIndeterminate = true);
    }

    void EnumeratingFilesFinished(object sender, EventArgs e)
    {
        _rootImporter.Finished -= EnumeratingFilesFinished;

        if(RecurseArchivesChecked)
        {
            Progress2Visible                        =  true;
            StatusMessage2Visible                   =  true;
            _rootImporter.SetMessage2               += SetMessage2;
            _rootImporter.SetIndeterminateProgress2 += SetIndeterminateProgress2;
            _rootImporter.SetProgress2              += SetProgress2;
            _rootImporter.SetProgressBounds2        += SetProgress2Bounds;

            _rootImporter.Finished += CheckArchivesFinished;

            _ = Task.Run(() =>
            {
                _stopwatch.Restart();

                if(Settings.Settings.Current.UseInternalDecompressor)
                    _rootImporter.SeparateFilesAndArchivesManaged();
                else
                    _rootImporter.SeparateFilesAndArchives();
            });
        }
        else
            ProcessFiles();
    }

    void ProcessFiles()
    {
        _listPosition           = 0;
        ProgressMinimum         = 0;
        ProgressMaximum         = _rootImporter.Files.Count;
        ProgressValue           = 0;
        ProgressIsIndeterminate = false;
        ProgressVisible         = true;
        CanClose                = false;
        CanStart                = false;
        IsReady                 = false;
        IsImporting             = true;
        _stopwatch.Restart();

        Parallel.ForEach(_rootImporter.Files,
                         new ParallelOptions
                         {
                             MaxDegreeOfParallelism = Environment.ProcessorCount
                         },
                         file =>
                         {
                             Dispatcher.UIThread.Post(() =>
                             {
                                 StatusMessage = string.Format(Localization.ImportingItem, Path.GetFileName(file));
                                 ProgressValue = _listPosition;
                             });

                             var model = new RomImporter
                             {
                                 Filename      = Path.GetFileName(file),
                                 Indeterminate = true
                             };

                             var worker = new FileImporter(_ctx,
                                                           _newFiles,
                                                           _newDisks,
                                                           _newMedias,
                                                           KnownOnlyChecked,
                                                           RemoveFilesChecked);

                             worker.SetIndeterminateProgress2 += model.OnSetIndeterminateProgress;
                             worker.SetMessage2               += model.OnSetMessage;
                             worker.SetProgress2              += model.OnSetProgress;
                             worker.SetProgressBounds2        += model.OnSetProgressBounds;
                             worker.ImportedRom               += model.OnImportedRom;
                             worker.WorkFinished              += model.OnWorkFinished;

                             Dispatcher.UIThread.Post(() => Importers.Add(model));

                             worker.ImportFile(file);

                             Interlocked.Increment(ref _listPosition);
                         });

        _stopwatch.Stop();
        Log.Debug("Took {TotalSeconds} seconds to process files", _stopwatch.Elapsed.TotalSeconds);

        _rootImporter.SaveChanges();

        _rootImporter.UpdateRomStats();

        _listPosition           = 0;
        ProgressMinimum         = 0;
        ProgressMaximum         = 1;
        ProgressValue           = 0;
        ProgressIsIndeterminate = false;
        ProgressVisible         = false;
        CanClose                = true;
        CanStart                = false;
        IsReady                 = false;
        IsImporting             = false;
        StatusMessage           = Localization.Finished;
        _mainStopwatch.Stop();

        Log.Debug("Took {TotalSeconds} seconds to import ROMs", _mainStopwatch.Elapsed.TotalSeconds);
    }

    void ProcessArchives()
    {
        // For each archive
        ProgressMaximum         = _rootImporter.Archives.Count;
        ProgressMinimum         = 0;
        ProgressValue           = 0;
        ProgressIsIndeterminate = false;
        Progress2Visible        = false;
        StatusMessage2Visible   = false;
        _listPosition           = 0;
        _stopwatch.Restart();

        Parallel.ForEach(_rootImporter.Archives,
                         new ParallelOptions
                         {
                             MaxDegreeOfParallelism = Environment.ProcessorCount
                         },
                         archive =>
                         {
                             Dispatcher.UIThread.Post(() =>
                             {
                                 StatusMessage = "Processing archive: " + Path.GetFileName(archive);
                                 ProgressValue = _listPosition;
                             });

                             // Create FileImporter
                             var archiveImporter = new FileImporter(_ctx,
                                                                    _newFiles,
                                                                    _newDisks,
                                                                    _newMedias,
                                                                    KnownOnlyChecked,
                                                                    RemoveFilesChecked);

                             // Extract archive
                             bool ret = Settings.Settings.Current.UseInternalDecompressor
                                            ? archiveImporter.ExtractArchiveManaged(archive)
                                            : archiveImporter.ExtractArchive(archive);

                             if(!ret) return;

                             // Process files in archive
                             foreach(string file in archiveImporter.Files)
                             {
                                 var model = new RomImporter
                                 {
                                     Filename      = Path.GetFileName(file),
                                     Indeterminate = true
                                 };

                                 var worker = new FileImporter(_ctx,
                                                               _newFiles,
                                                               _newDisks,
                                                               _newMedias,
                                                               KnownOnlyChecked,
                                                               RemoveFilesChecked);

                                 worker.SetIndeterminateProgress2 += model.OnSetIndeterminateProgress;
                                 worker.SetMessage2               += model.OnSetMessage;
                                 worker.SetProgress2              += model.OnSetProgress;
                                 worker.SetProgressBounds2        += model.OnSetProgressBounds;
                                 worker.ImportedRom               += model.OnImportedRom;
                                 worker.WorkFinished              += model.OnWorkFinished;

                                 Dispatcher.UIThread.Post(() => Importers.Add(model));

                                 worker.ImportFile(file);

                                 worker.Files.Clear();
                             }

                             // Remove temporary files
                             archiveImporter.CleanupExtractedArchive();

                             Interlocked.Increment(ref _listPosition);
                         });

        _stopwatch.Stop();
        Log.Debug("Took {TotalSeconds} seconds to process archives", _stopwatch.Elapsed.TotalSeconds);

        Progress2Visible      = false;
        StatusMessage2Visible = false;

        ProcessFiles();
    }

    void ProcessArchivesManaged()
    {
        // For each archive
        ProgressMaximum         = _rootImporter.Archives.Count;
        ProgressMinimum         = 0;
        ProgressValue           = 0;
        ProgressIsIndeterminate = false;
        Progress2Visible        = false;
        StatusMessage2Visible   = false;
        _listPosition           = 0;
        _stopwatch.Restart();

        Parallel.ForEach(_rootImporter.Archives,
                         new ParallelOptions
                         {
                             MaxDegreeOfParallelism = Environment.ProcessorCount
                         },
                         archive =>
                         {
                             Dispatcher.UIThread.Post(() =>
                             {
                                 StatusMessage =
                                     string.Format(Localization.ProcessingArchive, Path.GetFileName(archive));

                                 ProgressValue = _listPosition;
                             });

                             // Create FileImporter
                             var archiveImporter = new FileImporter(_ctx,
                                                                    _newFiles,
                                                                    _newDisks,
                                                                    _newMedias,
                                                                    KnownOnlyChecked,
                                                                    RemoveFilesChecked);

                             // Open archive
                             try
                             {
                                 using var     fs     = new FileStream(archive, FileMode.Open, FileAccess.Read);
                                 using IReader reader = ReaderFactory.Open(fs);

                                 // Process files in archive
                                 while(reader.MoveToNextEntry())
                                 {
                                     if(reader.Entry.IsDirectory) continue;

                                     if(reader.Entry.Crc == 0 && KnownOnlyChecked) continue;

                                     if(!archiveImporter.IsCrcInDb(reader.Entry.Crc) && KnownOnlyChecked) continue;

                                     // Do not import files that are already in the repository
                                     if(archiveImporter.IsInRepo(reader.Entry.Crc)) continue;

                                     var model = new RomImporter
                                     {
                                         Filename      = Path.GetFileName(reader.Entry.Key),
                                         Indeterminate = true
                                     };

                                     var worker = new FileImporter(_ctx,
                                                                   _newFiles,
                                                                   _newDisks,
                                                                   _newMedias,
                                                                   KnownOnlyChecked,
                                                                   RemoveFilesChecked);

                                     worker.SetIndeterminateProgress2 += model.OnSetIndeterminateProgress;
                                     worker.SetMessage2               += model.OnSetMessage;
                                     worker.SetProgress2              += model.OnSetProgress;
                                     worker.SetProgressBounds2        += model.OnSetProgressBounds;
                                     worker.ImportedRom               += model.OnImportedRom;
                                     worker.WorkFinished              += model.OnWorkFinished;

                                     Dispatcher.UIThread.Post(() => Importers.Add(model));

                                     string tmpFile = Path.Combine(Settings.Settings.Current.RepositoryPath,
                                                                   Path.GetRandomFileName());

                                     worker.ImportAndHashRom(reader.OpenEntryStream(),
                                                             reader.Entry.Key,
                                                             tmpFile,
                                                             reader.Entry.Size);

                                     try
                                     {
                                         if(File.Exists(tmpFile)) File.Delete(tmpFile);
                                     }
                                     catch(IOException)
#pragma warning disable PH2098
                                     {
                                         // Ignore IO exceptions when deleting temporary files
                                     }
#pragma warning restore PH2098
                                 }
                             }
                             catch(Exception)
                             {
                                 Dispatcher.UIThread.Post(() => Importers.Add(new RomImporter
                                 {
                                     Filename      = Path.GetFileName(archive),
                                     Indeterminate = false,
                                     Progress      = 1,
                                     Maximum       = 1,
                                     Minimum       = 0,
                                     StatusMessage = "Error processing archive."
                                 }));
#pragma warning disable ERP022
                             }
#pragma warning restore ERP022
                             finally
                             {
                                 Interlocked.Increment(ref _listPosition);
                             }
                         });

        _stopwatch.Stop();
        Log.Debug("Took {TotalSeconds} seconds to process archives", _stopwatch.Elapsed.TotalSeconds);

        Progress2Visible      = false;
        StatusMessage2Visible = false;

        ProcessFiles();
    }

    void CheckArchivesFinished(object sender, EventArgs e)
    {
        _stopwatch.Stop();
        Log.Debug("Took {TotalSeconds} seconds to check archives", _stopwatch.Elapsed.TotalSeconds);

        Progress2Visible      = false;
        StatusMessage2Visible = false;

        _rootImporter.Finished -= CheckArchivesFinished;

        if(Settings.Settings.Current.UseInternalDecompressor)
            ProcessArchivesManaged();
        else
            ProcessArchives();
    }

    void SetMessage(object sender, MessageEventArgs e)
    {
        Dispatcher.UIThread.Post(() => StatusMessage = e.Message);
    }

    void SetMessage2(object sender, MessageEventArgs e)
    {
        Dispatcher.UIThread.Post(() => StatusMessage2 = e.Message);
    }

    void Close() => View.Close();

    async Task SelectFolderAsync()
    {
        IReadOnlyList<IStorageFolder> result =
            await View.StorageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions
            {
                Title = Localization.ImportRomsFolderDialogTitle
            });

        if(result.Count < 1) return;

        FolderPath = result[0].TryGetLocalPath() ?? string.Empty;

        IsReady  = true;
        CanStart = true;
        CanClose = true;
    }
}