#nullable enable

using System;
using System.Collections.ObjectModel;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
using Avalonia.Platform.Storage;
using Avalonia.Themes.Fluent;
using DialogHostAvalonia;
using ImageMagick;
using MigraDoc.DocumentObjectModel;
using MigraDoc.Rendering;
using PdfSharp.Fonts;
using PdfSharp.Pdf.IO;
using PdfSharp.Snippets.Font;
using MayShow.Helpers;
using MayShow.Interfaces;
using MayShow.Models;
using MayShows.Helpers;

namespace MayShow.ViewModels;

class MainViewModel : BaseViewModel, IFontResolver
{
    private string _processDir;
    private bool _isCreatingPDF;
    private string _createPDFLog;
    private string _workingFolder;

    private string _reportTitle;
    private ObservableCollection<ReportFile> _reportFiles;
    private DateTime? _lastGeneratedTime;

    private Settings _settings;

    public MainViewModel(IChangeViewModel viewModelChanger) : base(viewModelChanger)
    {
        _processDir = Path.GetDirectoryName(Environment.ProcessPath) ?? "";
        Console.WriteLine("Process is running from: {0}", _processDir);
        _isCreatingPDF = false;
        var quotes = Constants.GetQuotes();
        Random random = new Random();
        var quoteIndex = random.Next(0, quotes.Length);
        _createPDFLog = "----- MayShow v" + Constants.AppVersion + " ------" + Environment.NewLine;
        _createPDFLog += quotes[quoteIndex] + Environment.NewLine;
        _createPDFLog += "---------------------------------------" + Environment.NewLine;
        _createPDFLog += "Ready to create PDF!";
        _workingFolder = "";
        _reportFiles = new ObservableCollection<ReportFile>();
        _reportFiles.CollectionChanged += ( sender, e ) => { NotifyPropertyChanged(nameof(IsCreatePDFButtonEnabled)); };
        _reportTitle = "";
        _lastGeneratedTime = null;
        _settings = Settings.LoadSettings();
        if (!string.IsNullOrWhiteSpace(_settings.LastUsedPath))
        {
            LogInfo("Loading data at last used path of {0}", _settings.LastUsedPath);
            ScanFolder(_settings.LastUsedPath);
        }
    }

    public string ReportTitle
    {
        get => _reportTitle;
        set { _reportTitle = value; NotifyPropertyChanged(); NotifyPropertyChanged(nameof(IsTitleBoxVisible)); }
    }

    public bool IsTitleBoxVisible
    {
        get => !string.IsNullOrWhiteSpace(_workingFolder);
    }

    public bool IsCreatingPDF
    {
        get => _isCreatingPDF;
        set 
        { 
            _isCreatingPDF = value; 
            NotifyPropertyChanged();
            NotifyPropertyChanged(nameof(IsCreatePDFButtonEnabled)); 
            NotifyPropertyChanged(nameof(HasWorkingFolderAndNotMakingPDF));
        }
    }

    public bool IsCreatePDFButtonEnabled
    {
        get => !_isCreatingPDF && _reportFiles.Count > 0;
    }

    public bool HasWorkingFolder
    {
        get => !string.IsNullOrWhiteSpace(_workingFolder) && Directory.Exists(_workingFolder);
    }

    public bool HasWorkingFolderAndNotMakingPDF
    {
        get => !string.IsNullOrWhiteSpace(_workingFolder) && Directory.Exists(_workingFolder) && !_isCreatingPDF;
    }

    public string WorkingFolder
    {
        get => _workingFolder;
        set
        {
            _workingFolder = value;
            NotifyPropertyChanged();
            NotifyPropertyChanged(nameof(HasWorkingFolder));
            NotifyPropertyChanged(nameof(HasWorkingFolderAndNotMakingPDF));
        }
    }

    public string CreatePDFLog
    {
        get => _createPDFLog;
        set { _createPDFLog = value; NotifyPropertyChanged(); }
    }

    public ObservableCollection<ReportFile> ReportFiles
    {
        get => _reportFiles;
        set 
        { 
            _reportFiles = value;
            NotifyPropertyChanged(); 
            _reportFiles.CollectionChanged += ( sender, e ) => 
            { 
                NotifyPropertyChanged(nameof(IsCreatePDFButtonEnabled)); 
            };
        }
    }

    private void LogInfo(string message, params object[]? arguments)
    {
        var timestamp = string.Format("[{0:s}]", DateTime.Now);
        Console.WriteLine(timestamp + " " + message, arguments);
        CreatePDFLog += Environment.NewLine + string.Format(message, arguments ?? []);
    }

    public async void ChooseFolder()
    {
        var topLevel = TopLevelGrabber?.GetTopLevel();
        if (topLevel is not null)
        {
            var folders = await topLevel.StorageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions()
            {
                Title = "Pick a folder of files...",
                AllowMultiple = false,
            });
            if (folders.Count == 1)
            {
                var folder = folders[0];
                LogInfo("Clearing existing list and loading items in folder: " + folder.Path.LocalPath);
                ReportFiles.Clear();
                ScanFolder(folder.Path.LocalPath);
                _settings.LastUsedPath = folder.Path.LocalPath;
                await _settings.SaveSettingsAsync();
                ResortPDFItemsByDate();
            }
        }
    }

    private void ScanFolder(string path)
    {
        if (Directory.Exists(path))
        {
            WorkingFolder = path;
            NotifyPropertyChanged(nameof(IsTitleBoxVisible));
            var reportFilePath = Path.Combine(path, GetReportSavedDataFileName());
            var successfullyLoadedPriorReport = false;
            if (File.Exists(reportFilePath))
            {
                // load prior report
                var json = File.ReadAllText(reportFilePath);
                var jsonContext = new SourceGenerationContext(Utilities.GetSerializerOptions());
                var report = JsonSerializer.Deserialize<PDFReport>(json, jsonContext.PDFReport);
                if (report != null && report.Files.Count > 0)
                {
                    ReportFiles = new ObservableCollection<ReportFile>(report.Files);
                    ReportTitle = report.Title;
                    WorkingFolder = report.BaseFolder;
                    _lastGeneratedTime = report.LastGenerated ?? null;
                    LogInfo("Reloaded report last saved at {0}", report.LastSaved);
                    successfullyLoadedPriorReport = true;
                }
            }
            if (!successfullyLoadedPriorReport)
            {
                // Scan folder for files and display in DataGrid
                ReportFiles.Clear();
                ReportTitle = "";
                var filePaths = Directory.GetFiles(_workingFolder);
                foreach (var filePath in filePaths)
                {
                    AddFileBasedOnPath(filePath);
                }
                ResortPDFItemsByDate();
            }
        }
        else
        {
            LogInfo("Error: The directory {0} does not exist. Please select another folder.", path);
        }
        NotifyPropertyChanged(nameof(IsCreatePDFButtonEnabled));
    }
    
    public void ShowAbout()
    {
        DialogHost.Show(new AboutViewModel());
    }

    public void RemoveFile(object f) => RemoveFileImpl((ReportFile)f);

    public async void RemoveFileImpl(ReportFile file)
    {
        var result = await DialogHost.Show(new WarningDeleteItemViewModel(file));
        if (result != null && (bool)result)
        {
            var idx = ReportFiles.IndexOf(file);
            if (idx != -1)
            {
                ReportFiles.RemoveAt(idx);
            }
        }
    }

    // https://github.com/AvaloniaUI/Avalonia/issues/10075
    public void EditFileProperties(object f) => EditFilePropertiesImpl((ReportFile)f);

    public async void EditFilePropertiesImpl(ReportFile file)
    {
        var result = await DialogHost.Show(new EditFileViewModel(file, ViewModelChanger));
        if (result != null && result is ReportFile updatedData)
        {
            file.Title = updatedData.Title;
            file.ReceiptDateTime = updatedData.ReceiptDateTime;
            file.Notes = updatedData.Notes;
        }
    }

    private string[] GetAllowedFileExtensionPatterns()
    {
        return [ "*.png", "*.jpg", "*.jpeg", "*.gif", "*.bmp", "*.webp", "*.pdf", "*.heic", ];
    }

    private string[] GetAllowedFileExtensionPatternsWithoutStar()
    {
        var list = GetAllowedFileExtensionPatterns();
        return list.Select(x => x.Replace("*.", "")).ToArray();
    }

    public async void AddItem()
    {
        var topLevel = TopLevelGrabber?.GetTopLevel();
        if (topLevel is not null)
        {
            var files = await topLevel.StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions()
            {
                Title = "Choose image or PDF files...",
                AllowMultiple = true,
                FileTypeFilter = [
                    new FilePickerFileType("All Types")
                    {
                        Patterns = GetAllowedFileExtensionPatterns(),
                        AppleUniformTypeIdentifiers = [ "public.image", "com.adobe.pdf", "public.heic" ],
                        MimeTypes = [ "image/*", "application/pdf", "image/heic" ]
                    },
                    FilePickerFileTypes.ImageAll, 
                    new FilePickerFileType("HEIC Images")
                    {
                        Patterns = [ "*.heic" ],
                        AppleUniformTypeIdentifiers = [ "public.heic" ],
                        MimeTypes = [ "image/heic" ]
                    },
                    FilePickerFileTypes.Pdf,
                ],
            });
            if (files.Count > 0)
            {
                foreach (var file in files)
                {
                    var filePath = file.TryGetLocalPath();
                    AddFileBasedOnPath(filePath);
                }
            }
        }
    }

    private void AddFileBasedOnPath(string? filePath)
    {
        if (!string.IsNullOrWhiteSpace(filePath) && File.Exists(filePath) && !filePath.EndsWith(".DS_Store"))
        {
            // make sure extensions are OK
            var fileExtensions = GetAllowedFileExtensionPatternsWithoutStar();
            var didMatch = false;
            foreach (var fileExtension in fileExtensions)
            {
                if (filePath.ToLower().EndsWith("." + fileExtension.ToLower()))
                {
                    didMatch = true;
                    break;
                }
            }
            if (!didMatch)
            {
                LogInfo("File {0} did not match allowed file extension types, so it was not added.", filePath);
            }
            else
            {
                var date = Utilities.CheckValidDateInString(filePath);
                ReportFiles.Add(new ReportFile()
                {
                    Title = Path.GetFileName(filePath),
                    ReceiptDateTime = date.HasValue ? date.Value.ToDateTime(TimeOnly.MinValue) : File.GetCreationTime(filePath),
                    Notes = "",
                    FilePath = filePath,
                });
            }
        }
    }

    public void LocateFile(object f) => LocateFileImpl((ReportFile) f);
    public async void LocateFileImpl(ReportFile reportFile)
    {
        var topLevel = TopLevelGrabber?.GetTopLevel();
        if (topLevel is not null)
        {
            var files = await topLevel.StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions()
            {
                Title = "Choose image or PDF file...",
                AllowMultiple = false,
                FileTypeFilter = [
                    new FilePickerFileType("All Types")
                    {
                        Patterns = GetAllowedFileExtensionPatterns(),
                        AppleUniformTypeIdentifiers = [ "public.image", "com.adobe.pdf", "public.heic" ],
                        MimeTypes = [ "image/*", "application/pdf", "image/heic" ]
                    },
                    FilePickerFileTypes.ImageAll, 
                    new FilePickerFileType("HEIC Images")
                    {
                        Patterns = [ "*.heic" ],
                        AppleUniformTypeIdentifiers = [ "public.heic" ],
                        MimeTypes = [ "image/heic" ]
                    },
                    FilePickerFileTypes.Pdf,
                ],
            });
            if (files.Count > 0)
            {
                var file = files[0];
                reportFile.FilePath = file.Path.LocalPath;
            }
        }
    }

    // https://github.com/AvaloniaUI/Avalonia/issues/10075
    public void OpenFile(object f) => OpenFileImpl((ReportFile)f);
    public void OpenFileImpl(ReportFile file)
    {
        var topLevel = TopLevelGrabber?.GetTopLevel();
        if (topLevel is not null)
        {
            var launcher = topLevel.Launcher;
            launcher.LaunchUriAsync(new Uri(file.FilePath));
        }
    }

    public void OpenFileLocation(object f) => OpenFileLocationImpl((ReportFile)f);

    private void OpenFileLocationImpl(ReportFile file)
    {
        OpenFolderForFileInFileViewer(file.FilePath);
    }

    private void OpenFolderForFileInFileViewer(string fullPathToFile)
    {
        var topLevel = TopLevelGrabber?.GetTopLevel();
        var dirName = Path.GetDirectoryName(fullPathToFile);
        if (topLevel is not null && dirName != null)
        {
            var launcher = topLevel.Launcher;
            launcher.LaunchUriAsync(new Uri(dirName));
        }
    }

    public void ResortPDFItemsByDate()
    {
        LogInfo("Sorting report files list...");
        ReportFiles = new ObservableCollection<ReportFile>(ReportFiles.OrderBy(x => x.ReceiptDateTime));
    }

    public async void BuildPDF()
    {
        if (string.IsNullOrWhiteSpace(ReportTitle))
        {
            await DialogHost.Show(new WarningViewModel("You must provide a report title!"));
        }
        else
        {
            try
            {
                await Task.Run(() => CreatePDF(_workingFolder));
            } catch (Exception e)
            {
                LogInfo("PDF process failed! Reason: " + e.Message);
                if (e.StackTrace != null)
                {
                    LogInfo(e.StackTrace);
                }
                if (e.InnerException != null)
                {
                    LogInfo("Inner exception: " + e.InnerException.Message);
                    if (e.InnerException.StackTrace != null)
                    {
                        LogInfo(e.InnerException.StackTrace);
                    }
                }
                LogInfo("Please report this error to a programmer or fix the issue listed above.");
                IsCreatingPDF = false;
            }
        }
    }

    public async void SaveInterimReportInfo()
    {
        var report = new PDFReport()
        {
            Title = ReportTitle,
            Files = ReportFiles.ToList(),
            BaseFolder = _workingFolder,
            LastSaved = DateTime.Now,
            LastGenerated = _lastGeneratedTime,
        };
        await SavePDFReportDataToDisk(report);
    }

    private async Task SavePDFReportDataToDisk(PDFReport report)
    {
        var jsonContext = new SourceGenerationContext(Utilities.GetSerializerOptions());
        using var memoryStream = new MemoryStream();
        await JsonSerializer.SerializeAsync(memoryStream, report, jsonContext.PDFReport);
        memoryStream.Position = 0;
        using var reader = new StreamReader(memoryStream);
        var json = await reader.ReadToEndAsync();
        var savePath = Path.Combine(_workingFolder, GetReportSavedDataFileName());
        await File.WriteAllTextAsync(savePath, json);
        LogInfo("Saved report information to {0}", savePath);
    }

    private async Task CreateAndSaveReportObjectAfterReportCreation()
    {
        var report = new PDFReport()
        {
            Title = ReportTitle,
            Files = ReportFiles.ToList(),
            BaseFolder = _workingFolder,
            LastSaved = DateTime.Now,
            LastGenerated = DateTime.Now,
        };
        _lastGeneratedTime = DateTime.Now;
        await SavePDFReportDataToDisk(report);
    }

    private string GetReportSavedDataFileName()
    {
        return "report_data.json";
    }

    public byte[]? GetFont(string faceName)
    {
        LogInfo(string.Format("Loading font {0}", faceName));
        if (faceName == "Noto Sans JP")
        {
            var path = Path.Combine(_processDir, "Assets/Fonts/Noto_Sans_JP/static/NotoSansJP-Regular.ttf");
            if (!File.Exists(path))
            {
                path = Path.Combine(_processDir, "../Resources/Assets/Fonts/Noto_Sans_JP/static/NotoSansJP-Regular.ttf");
            }
            return File.ReadAllBytes(path);
        }
        if (faceName == "Noto Sans JP Bold")
        {
            var path = Path.Combine(_processDir, "Assets/Fonts/Noto_Sans_JP/static/NotoSansJP-SemiBold.ttf");
            if (!File.Exists(path))
            {
                path = Path.Combine(_processDir, "../Resources/Assets/Fonts/Noto_Sans_JP/static/NotoSansJP-SemiBold.ttf");
            }
            return File.ReadAllBytes(path);
        }
        return null;
    }

    public FontResolverInfo? ResolveTypeface(string familyName, bool bold, bool italic)
    {
        // LogInfo(string.Format("Resolving font name {0}", familyName));
        if (familyName == "Noto Sans JP")
        {
            if (bold)
            {
                return new FontResolverInfo(familyName + " Bold");
            }
            return new FontResolverInfo(familyName);
        }
        return null;
    }

    // https://forum.pdfsharp.net/viewtopic.php?f=2&t=1025
    private async Task CreatePDF(string folderPath)
    {
        // TODO: calculate needed width for images based on page width and margins and all that?
        // TODO: resize (non-HEIC) images for smaller size...?
        IsCreatingPDF = true;
        var pdfDoc = new Document();
        var outputFileName = ReportTitle + ".pdf";
        var folderName = new DirectoryInfo(folderPath).Name;
        const int imageWidth = 425;
        if (folderName.Contains('-'))
        {
            // see if year/month format
            var parts = folderName.Split('-');
            if (parts[0].Length == 4 &&
                parts[1].Length <= 2 &&
                int.TryParse(parts[0], out int year) && int.TryParse(parts[1], out int month))
            {
                outputFileName = string.Format("{0} {1} Receipts.pdf", 
                    CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(month), 
                    year);
                LogInfo("Auto-changed output file name to " + outputFileName);
            }
        }
        var section = pdfDoc.AddSection();
        section.PageSetup.PageFormat = PageFormat.Letter;
        section.PageSetup.PageWidth = "8.5in";
        section.PageSetup.PageHeight = "11in";
        section.PageSetup.TopMargin = "0.5in";
        section.PageSetup.RightMargin = "0.5in";
        section.PageSetup.BottomMargin = "0.5in";
        section.PageSetup.LeftMargin = "0.5in";
        // setup footer for page number
        var footerPar = new Paragraph();
        footerPar.Format.Alignment = ParagraphAlignment.Center;
        footerPar.Format.Font.Size = 10;
        footerPar.AddText("--Page ");
        footerPar.AddPageField();
        footerPar.AddText(" of ");
        footerPar.AddNumPagesField();
        footerPar.AddText("--");
        footerPar.AddLineBreak();
        footerPar.AddText("Report generated on " + DateTime.Now.ToString("f"));
        section.Footers.Primary.Add(footerPar);
        // add report title
        var reportTitlePar = section.AddParagraph();
        reportTitlePar.Format.Alignment = ParagraphAlignment.Center;
        reportTitlePar.Format.Font.Size = 16;
        reportTitlePar.Format.Font.Bold = true;
        reportTitlePar.Format.Font.Name = "Noto Sans JP"; // has english letters in it, too
        reportTitlePar.AddText(ReportTitle);
        //
        GlobalFontSettings.FontResolver = this;
        GlobalFontSettings.FallbackFontResolver = new FailsafeFontResolver();
        var hasAddedData = false;
        for (var i = 0; i < ReportFiles.Count; i++)
        {
            var file = ReportFiles[i];
            var fileName = file.FileName;
            var filePath = file.FilePath;
            if (!File.Exists(filePath))
            {
                LogInfo("ERROR: File \"{0}\" does not exist at path \"{1}\". Please remove it from the report or re-add it using the Add Item button if you still want it to be in this report.", file.Title, file.FilePath);
                IsCreatingPDF = false;
                return;
            }
            if (fileName == ".DS_Store" || fileName == outputFileName)
            {
                continue;
            }
            if (i > 0 && hasAddedData)
            {
                section.AddPageBreak();
            }
            var imageTitlePar = section.AddParagraph();
            imageTitlePar.Format.Alignment = ParagraphAlignment.Center;
            imageTitlePar.Format.Font.Size = 12;
            imageTitlePar.Format.Font.Bold = true;
            imageTitlePar.Format.Font.Name = "Noto Sans JP"; // has english letters in it, too
            imageTitlePar.AddText(string.IsNullOrWhiteSpace(file.Title) ? file.FileName : file.Title);
            var receiptDatePar = section.AddParagraph();
            receiptDatePar.Format.Alignment = ParagraphAlignment.Center;
            receiptDatePar.Format.Font.Size = 12;
            receiptDatePar.Format.Font.Bold = true;
            receiptDatePar.Format.Font.Name = "Noto Sans JP"; // has english letters in it, too
            receiptDatePar.AddText(file.ReceiptDate.ToString("yyyy-MM-dd"));
            if (!string.IsNullOrWhiteSpace(file.Notes))
            {
                var imageNotesPar = section.AddParagraph();
                imageNotesPar.Format.Alignment = ParagraphAlignment.Center;
                imageNotesPar.Format.Font.Size = 10;
                imageNotesPar.Format.Font.Bold = false;
                imageNotesPar.Format.Font.Name = "Noto Sans JP";
                imageNotesPar.AddText(file.Notes);
            }
            section.AddParagraph(); // add empty line for spacing
            // now add the image
            var lowerName = fileName.ToLower();
            var isPDF = lowerName.EndsWith(".pdf");
            // convert heic, webp, or png to JPEG for size and ease of use
            // (and probably compat reasons too, though I haven't tested that...)
            var isHEIC = lowerName.EndsWith(".heic");
            var isWebp = lowerName.EndsWith(".webp");
            var isPNG = lowerName.EndsWith(".png");
            var info = new FileInfo(file.FilePath);
            uint loadedImageWidth = 0;
            uint loadedImageHeight = 0;
            if (isHEIC || isWebp || isPNG)
            {
                // Save image as jpg
                var convertedDir = Path.Combine(folderPath, "converted");
                if (!Directory.Exists(convertedDir))
                {
                    Directory.CreateDirectory(convertedDir);
                }
                var outputPath = Path.Combine(convertedDir, info.Name + ".jpg");
                using var mImage = new MagickImage(info.FullName);
                loadedImageWidth = mImage.Width;
                loadedImageHeight = mImage.Height;
                mImage.Quality = 80;
                if (mImage.Width >= 400 || mImage.Height >= 400)
                {
                    loadedImageWidth = (uint)Math.Floor(mImage.Width * 0.5);
                    loadedImageHeight = (uint)Math.Floor(mImage.Height * 0.5);
                    mImage.Scale(loadedImageWidth, loadedImageHeight);
                }
                await mImage.WriteAsync(outputPath);
                filePath = Path.Combine("Converted", info.Name + ".jpg");
                LogInfo(string.Format("Converted image to JPEG; fileName is now {0}", file.FilePath));
            }
            else if (!isPDF)
            {
                // load height/width
                using var mImage = new MagickImage(info.FullName);
                loadedImageWidth = mImage.Width;
                loadedImageHeight = mImage.Height;
            }
            var paragraph = section.AddParagraph();
            paragraph.Format.Alignment = ParagraphAlignment.Center;
            var image = paragraph.AddImage(filePath);
            image.LockAspectRatio = true;
            if (!isPDF && loadedImageHeight > 600)
            {
                image.Height = 550; // make sure it will fit on one page
            }
            else
            {
                image.Width = imageWidth; // can't be too wide now...not sure why...maybe due to margins...
            }
            LogInfo(string.Format("Added image: {0} ({1})", file.Title, filePath));
            if (isPDF)
            {
                // add other PDF pages
                // see: https://stackoverflow.com/a/65091204/3938401
                var pdfFileToAdd = PdfReader.Open(filePath);
                imageTitlePar.AddText(string.Format(" (PDF with {0} page{1}) ", 
                    pdfFileToAdd.PageCount, 
                    pdfFileToAdd.PageCount == 1 ? "" : "s"));
                for (var j = 2; j <= pdfFileToAdd.PageCount; j++)
                {
                    section.AddPageBreak();
                    paragraph = section.AddParagraph();
                    paragraph.Format.Alignment = ParagraphAlignment.Center;
                    image = paragraph.AddImage(filePath + "#" + j);
                    image.LockAspectRatio = true;
                    image.Width = imageWidth;
                }
            }
            hasAddedData = true;
        }
        var pdfRenderer = new PdfDocumentRenderer
        {
            Document = pdfDoc,
            WorkingDirectory = folderPath
        };
        LogInfo("Rendering document...");
        pdfRenderer.RenderDocument();
        string outputPDFFileName = Path.Join(folderPath, outputFileName);
        LogInfo("Saving document to disk...");
        pdfRenderer.PdfDocument.Save(outputPDFFileName);
        LogInfo("Saved PDF output to: " + outputPDFFileName);
        await CreateAndSaveReportObjectAfterReportCreation();
        OpenFolderForFileInFileViewer(outputPDFFileName);
        IsCreatingPDF = false;
    }
}