#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;

using Docnet.Core.Models;
using Docnet.Core;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
using System.Reflection.Metadata.Ecma335;
using Docnet.Core.Readers;
using MigraDoc.DocumentObjectModel.Visitors;

namespace MayShow.ViewModels;

class MainViewModel : BaseViewModel, IFontResolver, ICanCheckShutdown
{
    private bool _isPerformingInitialLoad;
    private string _processDir;
    private bool _isCreatingPDF;
    private string _programLog;
    private string _workingFolder;

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

    private Settings _settings;

    private bool _hasUnsavedWork;

    public MainViewModel(IChangeViewModel viewModelChanger) : base(viewModelChanger)
    {
        _isPerformingInitialLoad = true;
        _processDir = Path.GetDirectoryName(Environment.ProcessPath) ?? "";
        Console.WriteLine("Internal storage directory is: {0}", Utilities.GetInternalDataPath());
        _isCreatingPDF = false;
        var quotes = Constants.GetQuotes();
        Random random = new Random();
        var quoteIndex = random.Next(0, quotes.Length);
        _programLog = "----- MayShow v" + Constants.AppVersion + " ------" + Environment.NewLine;
        _programLog += quotes[quoteIndex] + Environment.NewLine;
        _programLog += "---------------------------------------" + Environment.NewLine;
        _programLog += "Loaded and ready to create report!" + Environment.NewLine;
        _programLog += "Please copy and send this Program Log when reporting any issues with the software.";
        _workingFolder = "";
        ReportFiles = _reportFiles = new ObservableCollection<ReportFile>();
        _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);
        }
        else
        {
            LogInfo("Choose a receipt folder to begin...");
        }
        HasUnsavedWork = false;
        _isPerformingInitialLoad = false;
    }

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

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

    public bool CanAddItem
    {
        get => IsTitleBoxVisible && !IsCreatingPDF;
    }

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

    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 ProgramLog
    {
        get => _programLog;
        set { _programLog = value; NotifyPropertyChanged(); }
    }

    public bool HasUnsavedWork
    {
        get => _hasUnsavedWork;
        set
        {
            _hasUnsavedWork = value;
            NotifyPropertyChanged();
        }
    }

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

    private void LogInfo(string message, params object[]? arguments)
    {
        var timestamp = string.Format("[{0:s}]", DateTime.Now);
        Console.WriteLine(timestamp + " " + message, arguments);
        ProgramLog += 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();
                HasUnsavedWork = true;
            }
        }
    }

    private string GetReportSavedDataPath(string folderPath)
    {
        if (_settings.SaveReportJsonDataInInternalDir)
        {
            var internalPath = Utilities.GetInternalDataPath();
            if (!_settings.WorkingFolderToInternalFolderName.ContainsKey(folderPath))
            {
                var uuid = "";
                var potentialPath = "";
                var isDone = false;
                // make sure uuid not already used...just in case...because paranoia...
                do
                {
                    uuid = Guid.NewGuid().ToString();
                    potentialPath = Path.Combine(internalPath, uuid);
                    isDone = !Directory.Exists(potentialPath);
                } while (!isDone);
                // make internal dir -- using dir so we have option to copy data into dir later if needed
                // (if we ever implement a more robust report system where we keep all files)
                Directory.CreateDirectory(potentialPath);
                _settings.WorkingFolderToInternalFolderName[folderPath] = uuid;
                _settings.SaveSettingsNotAsync(); // save new key/value pair
            }
            return Path.Combine(
                internalPath, 
                _settings.WorkingFolderToInternalFolderName[folderPath], 
                Constants.ReportSavedDataFileName
            );
        }
        else
        {
            return Path.Combine(folderPath, Constants.ReportSavedDataFileName);
        }
    }

    private void ScanFolder(string path)
    {
        if (Directory.Exists(path))
        {
            WorkingFolder = path;
            NotifyPropertyChanged(nameof(IsTitleBoxVisible));
            NotifyPropertyChanged(nameof(CanAddItem));
            var reportFilePath = GetReportSavedDataPath(path);
            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)
                {
                    Console.WriteLine("Loading prior report data at {0}", reportFilePath);
                    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();
                HasUnsavedWork = true;
            }
        }
        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 async Task ShowSettings()
    {
        var updatedSettings = await DialogHost.Show(new SettingsViewModel(_settings, TopLevelGrabber));
        if (updatedSettings != null)
        {
            _settings = (Settings)updatedSettings;
            await _settings.SaveSettingsAsync();
            LogInfo("Saved updated settings!");
        }
    }

    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);
                HasUnsavedWork = true;
            }
        }
    }

    // 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;
            HasUnsavedWork = true;
        }
    }

    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 = Constants.AllowedFileExtensionPatterns,
                        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 = Constants.AllowedFileExtensionsNoStar;
            var didMatch = false;
            foreach (var fileExtension in fileExtensions)
            {
                if (filePath.ToLower().EndsWith("." + fileExtension.ToLower()))
                {
                    didMatch = true;
                    break;
                }
            }
            if (!didMatch)
            {
                if (!filePath.EndsWith(Constants.ReportSavedDataFileName))
                {
                    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,
                });
                HasUnsavedWork = true;
            }
        }
    }

    public async void RemoveAllItems()
    {
        var result = await DialogHost.Show(new ConfirmViewModel("Warning!", "Are you sure you want to remove all items from this report?", "Remove All Items", "Cancel"));
        if (result != null && (bool)result)
        {
            ReportFiles.Clear();
            HasUnsavedWork = true;
            NotifyPropertyChanged(nameof(IsCreatePDFButtonEnabled));
        }
    }

    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 = Constants.AllowedFileExtensionPatterns,
                        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;
                HasUnsavedWork = true;
            }
        }
    }

    // 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));
        HasUnsavedWork = true;
    }

    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 Task 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 = GetReportSavedDataPath(WorkingFolder);
        await File.WriteAllTextAsync(savePath, json);
        LogInfo("Saved report information to {0}", savePath);
        HasUnsavedWork = false;
    }

    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);
    }

    public async Task CopyLogToClipboard()
    {
        var clipboard = TopLevelGrabber?.GetTopLevel().Clipboard;
        if (clipboard != null)
        {
            await clipboard.SetTextAsync(ProgramLog);
            LogInfo("Program log has been copied to the clipboard!");
        }
    }

    public byte[]? GetFont(string faceName)
    {
        LogInfo(string.Format("Loading font {0}", faceName));
        if (faceName == "Noto Sans")
        {
            var path = Path.Combine(_processDir, "Assets/Fonts/Noto_Sans/static/NotoSans-Regular.ttf");
            if (!File.Exists(path))
            {
                path = Path.Combine(_processDir, "../Resources/Assets/Fonts/Noto_Sans/static/NotoSans-Regular.ttf");
            }
            return File.ReadAllBytes(path);
        }
        if (faceName == "Noto Sans Bold")
        {
            var path = Path.Combine(_processDir, "Assets/Fonts/Noto_Sans/static/NotoSans-Bold.ttf");
            if (!File.Exists(path))
            {
                path = Path.Combine(_processDir, "../Resources/Assets/Fonts/Noto_Sans/static/NotoSans-Bold.ttf");
            }
            return File.ReadAllBytes(path);
        }
        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")
        {
            if (bold)
            {
                return new FontResolverInfo(familyName + " Bold");
            }
            return new FontResolverInfo(familyName);
        }
        if (familyName == "Noto Sans JP")
        {
            if (bold)
            {
                return new FontResolverInfo(familyName + " Bold");
            }
            return new FontResolverInfo(familyName);
        }
        return null;
    }

    private Paragraph GetFooterParagraph()
    {
        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"));
        footerPar.Tag = "FooterPar";
        footerPar.Format.Font.Name = "Noto Sans";
        return footerPar;
    }

    private decimal GetExistingPageItemHeight(PdfDocumentRenderer pdfRenderer, decimal footerParagraphHeight)
    {
        pdfRenderer.DocumentRenderer.PrepareDocument();
        var currPageCount = pdfRenderer.DocumentRenderer.FormattedDocument?.PageCount;
        var heightForExistingItemsOnPage = footerParagraphHeight;
        if (currPageCount.HasValue)
        {
            var renderInfo = pdfRenderer.DocumentRenderer.GetRenderInfoFromPage(currPageCount.Value);
            if (renderInfo != null)
            {
                // Console.WriteLine("Got render info for page: {0}", currPageCount);
                foreach (var item in renderInfo)
                {
                    heightForExistingItemsOnPage += (decimal)item.LayoutInfo.ContentArea.Height.Inch;
                }
            }
        }
        return heightForExistingItemsOnPage;
    }

    // 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?
        // safety checks
        var outputDir = _settings.SaveOutputPdfInWorkingDir ? folderPath : _settings.OutputPdfDir;
        if (!Directory.Exists(outputDir))
        {
            await DialogHost.Show(new WarningViewModel("Output directory not found! Please adjust your application Settings before continuing. Output directory: " + outputDir));
            return;
        }
        // setup globals and consts...
        GlobalFontSettings.FontResolver = this;
        GlobalFontSettings.FallbackFontResolver = new FailsafeFontResolver();
        const decimal pageWidth = 8.5m;
        const decimal pageHeight = 11.0m;
        const decimal margin = 0.5m;
        const int imageResolution = 72;
        const int imageInsertMarginPixels = 30; // we calculate max available; use max - this # for max image size
        var maxItemPxWidth = ((pageWidth - (2 * margin)) * imageResolution) - imageInsertMarginPixels;
        // start making PDF!
        IsCreatingPDF = true;
        var pdfDoc = new Document();
        var outputFileName = ReportTitle + ".pdf";
        var folderName = new DirectoryInfo(folderPath).Name;
        const int maxImageWidth = 425;
        var imageLineFormat = new MigraDoc.DocumentObjectModel.Shapes.LineFormat()
        {
            Color = Colors.Black,
            Width = Unit.FromPoint(2),
        };;
        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);
            }
        }
        // setup initial section (for page characteristics)
        var section = pdfDoc.AddSection();
        section.PageSetup.PageFormat = PageFormat.Letter;
        section.PageSetup.PageWidth = pageWidth + "in";
        section.PageSetup.PageHeight = pageHeight + "in";
        section.PageSetup.TopMargin = margin + "in";
        section.PageSetup.RightMargin = margin + "in";
        section.PageSetup.BottomMargin = margin + "in";
        section.PageSetup.LeftMargin = margin + "in";
        // setup footer for page number
        var footerPar = GetFooterParagraph();
        section.Footers.Primary.Add(footerPar);
        // create a quick PDF doc renderer to measure footer paragraph height
        var footerParagraphHeight = 0.4m; // estimate
        var footerOnlyPdfDoc = new Document();
        var sectionClone = section.Clone();
        footerOnlyPdfDoc.Add(sectionClone);
        sectionClone.Add(GetFooterParagraph());
        var footerPdfRenderer = new PdfDocumentRenderer
        {
            Document = footerOnlyPdfDoc
        };
        footerPdfRenderer.DocumentRenderer.PrepareDocument();
        var footerRenderInfo = footerPdfRenderer.DocumentRenderer.GetRenderInfoFromPage(1);
        if (footerRenderInfo != null)
        {
            foreach (var item in footerRenderInfo)
            {
                if (item.DocumentObject.Tag?.ToString() == "FooterPar")
                {
                    Console.WriteLine("Got footer paragraph height!");
                    footerParagraphHeight = (decimal)item.LayoutInfo.ContentArea.Height.Inch;
                    break;
                }
            }
        }
        // continue setting up document
        // First page only: 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);
        reportTitlePar.Tag = "TitlePar";
        // get converted files directory path and create it if necessary
        var convertedDir = Path.Combine(Utilities.GetInternalDataPath(), "converted");
        if (!Directory.Exists(convertedDir))
        {
            Directory.CreateDirectory(convertedDir);
        }
        //
        var pdfRenderer = new PdfDocumentRenderer
        {
            Document = pdfDoc,
            WorkingDirectory = folderPath
        };
        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);
            imageTitlePar.Tag = "ReceiptTitlePar";
            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"));
            receiptDatePar.Tag = "ReceiptDatePar";
            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);
                imageNotesPar.Tag = "ReceiptNotesPar";
            }
            var emptyPar = section.AddParagraph(); // add empty line for spacing
            emptyPar.Tag = "EmptyParagraph";
            // 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;
            // get max pixel height remaining for items on this page
            // (For multi-page PDFs, showing page 2 and on will have more height since they have no title, 
            // but to keep things consistent we will use the same height for all PDF pages.)
            // render up to now on this page and get height remaining in inches
            var currPageCount = pdfRenderer.DocumentRenderer.FormattedDocument?.PageCount;
            var heightForExistingItemsOnPage = GetExistingPageItemHeight(pdfRenderer, footerParagraphHeight);
            var remainingHeightInches = pageHeight - (2 * margin) - heightForExistingItemsOnPage;
            var remainingHeightPixels = (remainingHeightInches * imageResolution) - imageInsertMarginPixels;
            if (!isPDF)
            {
                using var mImage = new MagickImage(info.FullName);
                loadedImageWidth = mImage.Width;
                loadedImageHeight = mImage.Height;
                var convertedOutputPath = Path.Combine(convertedDir, info.Name + ".jpg");
                var didAdjust = false;
                LogInfo("Image orientation of {0} is {1}", fileName, mImage.Orientation);
                if (mImage.Orientation != OrientationType.TopLeft)
                {
                    LogInfo("Auto-adjusted image orientation of {0}", fileName);
                    mImage.AutoOrient();
                    didAdjust = true;
                }
                // perform needed image manipulations
                if (isHEIC || isWebp || isPNG || (!isPDF && info.Length > _settings.ImageResizeThreshold * 1024 * 1024))
                {
                    // Save image as jpg
                    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);
                        LogInfo("Image {2} scaled to {0}x{1}", loadedImageWidth, loadedImageHeight, fileName);
                    }
                    didAdjust = true;
                    LogInfo("Converted image {0} to JPEG", fileName);
                }
                else
                {
                    // load height/width
                    loadedImageWidth = mImage.Width;
                    loadedImageHeight = mImage.Height;
                }
                if (didAdjust)
                {
                    await mImage.WriteAsync(convertedOutputPath);
                    filePath = convertedOutputPath;
                    LogInfo(string.Format("Saved adjusted image to JPEG; file path is now {0}", filePath));                    
                }
                // write to PDF
                var paragraph = section.AddParagraph();
                paragraph.Format.Alignment = ParagraphAlignment.Center;
                var image = paragraph.AddImage(filePath);
                image.Resolution = imageResolution; // dots per inch
                image.Tag = "ReceiptImageTag";
                paragraph.Tag = "ReceiptImageParagraphTag";
                image.LineFormat = imageLineFormat.Clone();
                // resize down until it will fit on the page
                while (loadedImageHeight > remainingHeightPixels || loadedImageWidth > maxItemPxWidth)
                {
                    // Console.WriteLine("Image height = {0}, width = {1}; decreasing size by 5% to h={2}, w={3}", loadedImageHeight, loadedImageWidth, (uint)Math.Floor(loadedImageHeight * 0.95), (uint)Math.Floor(loadedImageWidth * 0.95));
                    // keep reducing size by 5% (little by little) until it fits on the page
                    // ...might skew ever so slightly but should not be noticable...
                    loadedImageHeight = (uint)Math.Floor(loadedImageHeight * 0.95);
                    loadedImageWidth = (uint)Math.Floor(loadedImageWidth * 0.95);
                }
                image.Height = loadedImageHeight;
                image.Width = loadedImageWidth;
            }
            else // isPDF
            {
                // need to render PDF to images
                if (_settings.UseDocnetPDFImageRendering)
                {
                    // render using Docnet library (which utilizes pdfium, the chrome renderer)
                    string RenderPdfPageToImage(IDocReader docReader, int pgNum)
                    {
                        Console.WriteLine("Rendering pg " + pgNum);
                        using var pageReader = docReader.GetPageReader(pgNum);
                        Console.WriteLine("Getting image for page " + pgNum);
                        var rawBytes = pageReader.GetImage(RenderFlags.RenderAnnotations);
                        Console.WriteLine("Getting width & height for page " + pgNum);
                        var width = pageReader.GetPageWidth();
                        var height = pageReader.GetPageHeight();
                        Console.WriteLine("Loading pixel data for page " + pgNum);
                        using var img = Image.LoadPixelData<Bgra32>(rawBytes, width, height);
                        // you are likely going to want this as well otherwise you might end up with transparent parts.
                        img.Mutate(x => x.BackgroundColor(SixLabors.ImageSharp.Color.White));
                        var pdfPageImageOutputPath = Path.Combine(convertedDir, info.Name + "-Page-" 
                            + (pgNum + 1).ToString().PadLeft(3, '0') + ".jpg");
                        img.Save(pdfPageImageOutputPath);
                        Console.WriteLine("Done rendering pg " + pgNum);
                        return pdfPageImageOutputPath;
                    }
                    // render all pages to images
                    var docReader = DocLib.Instance.GetDocReader(
                        filePath,
                        new PageDimensions(1080, 1920)); // TODO: are these dims right?
                    // add to document
                    var pgCount = docReader.GetPageCount();
                    if (pgCount > 0)
                    {
                        var convertedPdfImagePath = RenderPdfPageToImage(docReader, 0);
                        imageTitlePar.AddText(string.Format(" (PDF with {0} page{1}) ", 
                            pgCount, 
                            pgCount == 1 ? "" : "s"));
                        var paragraph = section.AddParagraph();
                        paragraph.Format.Alignment = ParagraphAlignment.Center;
                        // get image height/width off of disk so we can resize down if needed
                        var image = paragraph.AddImage(convertedPdfImagePath);
                        image.LockAspectRatio = true;
                        image.LineFormat = imageLineFormat.Clone();
                        using (var firstPdfPageImage = new MagickImage(convertedPdfImagePath))
                        {
                            var pdfPageImageWidth = firstPdfPageImage.Width;
                            var pdfPageImageHeight = firstPdfPageImage.Height;
                            // resize down until it will fit on the page
                            while (pdfPageImageHeight > remainingHeightPixels || pdfPageImageWidth > maxItemPxWidth)
                            {
                                pdfPageImageHeight = (uint)Math.Floor(pdfPageImageHeight * 0.95);
                                pdfPageImageWidth = (uint)Math.Floor(pdfPageImageWidth * 0.95);
                            }
                            image.Height = pdfPageImageHeight;
                            image.Width = pdfPageImageWidth;
                        }
                        for (var j = 1; j < pgCount; j++)
                        {
                            section.AddPageBreak();
                            paragraph = section.AddParagraph();
                            paragraph.Format.Alignment = ParagraphAlignment.Center;
                            convertedPdfImagePath = RenderPdfPageToImage(docReader, j);
                            image = paragraph.AddImage(convertedPdfImagePath);
                            image.LockAspectRatio = true;
                            image.Width = maxImageWidth;
                            image.LineFormat = imageLineFormat.Clone();
                            using (var otherPdfPageImage = new MagickImage(convertedPdfImagePath))
                            {
                                var pdfPageImageWidth = otherPdfPageImage.Width;
                                var pdfPageImageHeight = otherPdfPageImage.Height;
                                // resize down until it will fit on the page
                                while (pdfPageImageHeight > remainingHeightPixels || pdfPageImageWidth > maxItemPxWidth)
                                {
                                    pdfPageImageHeight = (uint)Math.Floor(pdfPageImageHeight * 0.95);
                                    pdfPageImageWidth = (uint)Math.Floor(pdfPageImageWidth * 0.95);
                                }
                                image.Height = pdfPageImageHeight;
                                image.Width = pdfPageImageWidth;
                            }
                        }
                    }
                }
                else
                {
                    // use older, not-docnet rendering method.
                    // uses MigraDoc rendering. Does not work with annotations, and since Migradoc
                    // doesn't let us know how big the image is, we can't do the image resizing, so
                    // we just do our best.
                    // render first page (eventually need to improve code to just do everything in a loop)
                    var paragraph = section.AddParagraph();
                    paragraph.Format.Alignment = ParagraphAlignment.Center;
                    var image = paragraph.AddImage(filePath);
                    image.LockAspectRatio = true;
                    image.Width = maxImageWidth; // can't be too wide now...not sure why...maybe due to margins...
                    image.LineFormat = imageLineFormat.Clone();
                    // render other PDF pages, if any
                    // see: https://stackoverflow.com/a/65091204/3938401
                    var pdfFileToAdd = PdfReader.Open(filePath, PdfDocumentOpenMode.Import);
                    var pgCount = pdfFileToAdd.PageCount;
                    imageTitlePar.AddText(string.Format(" (PDF with {0} page{1}) ", 
                        pgCount, 
                        pgCount == 1 ? "" : "s"));
                    for (var j = 2; j <= pgCount; j++)
                    {
                        section.AddPageBreak();
                        paragraph = section.AddParagraph();
                        paragraph.Format.Alignment = ParagraphAlignment.Center;
                        image = paragraph.AddImage(filePath + "#" + j);
                        image.LockAspectRatio = true;
                        image.Width = maxImageWidth;
                        image.LineFormat = imageLineFormat.Clone();
                    }
                }
            }
            LogInfo(string.Format("Added image: {0} ({1})", file.Title, filePath));
            hasAddedData = true;
        }
        LogInfo("Rendering document to PDF file...");
        pdfRenderer.DocumentRenderer.PrepareDocument(); // needed if you make edits after first PrepareDocument() is called
        pdfRenderer.RenderDocument();
        // actually save to disk now
        string outputPDFFilePath = Path.Join(outputDir, outputFileName);
        LogInfo("Saving PDF document to disk...");
        pdfRenderer.PdfDocument.Save(outputPDFFilePath);
        LogInfo("Finished saving PDF output to: " + outputPDFFilePath);
        await CreateAndSaveReportObjectAfterReportCreation();
        // clean up data dir
        Directory.Delete(convertedDir, true);
        // show output folder to user
        OpenFolderForFileInFileViewer(outputPDFFilePath);
        IsCreatingPDF = false;
    }

    public async Task<bool> CheckIsSafeToShutdown()
    {
        if (!HasUnsavedWork || string.IsNullOrWhiteSpace(WorkingFolder))
        {
            return true;
        }
        else
        {
            var result = await DialogHost.Show(new ShutdownCheckViewModel());
            if (result != null && result is ShutdownCheckOptions opt)
            {
                if (opt == ShutdownCheckOptions.SaveAndShutdown)
                {
                    await SaveInterimReportInfo();
                    return true;
                }
                else if (opt == ShutdownCheckOptions.NoSaveShutdown)
                {
                    return true;
                }
                else if (opt == ShutdownCheckOptions.CancelShutdown)
                {
                    return false;
                }
            }
        }
        return false;
    }
}