using System.Diagnostics;
using System.Globalization;
using System.Text.RegularExpressions;

namespace RolemasterDb.ImportTool;

public sealed class PdfXmlExtractor
{
    public const int RenderScaleFactor = 4;
    public const int XmlAlignedRenderDpi = 108;
    public const int ScaledRenderDpi = XmlAlignedRenderDpi * RenderScaleFactor;
    private const string PortableMiKTeXPath = @"D:\Code\miktex-portable\texmfs\install\miktex\bin\x64";

    public static int ScaleCoordinate(int value) => checked(value * RenderScaleFactor);

    public async Task ExtractAsync(string pdfPath, string outputPath, CancellationToken cancellationToken = default)
    {
        Directory.CreateDirectory(Path.GetDirectoryName(outputPath)!);

        var startInfo = new ProcessStartInfo
        {
            FileName = ResolveExecutable("ROLEMASTERDB_PDFTOHTML_PATH", "pdftohtml.exe"),
            RedirectStandardError = true,
            RedirectStandardOutput = true,
            UseShellExecute = false,
            CreateNoWindow = true
        };

        startInfo.ArgumentList.Add("-xml");
        startInfo.ArgumentList.Add("-i");
        startInfo.ArgumentList.Add("-noframes");
        startInfo.ArgumentList.Add(pdfPath);
        startInfo.ArgumentList.Add(outputPath);

        await RunProcessAsync(startInfo, cancellationToken);
    }

    public async Task<PdfDocumentInfo> ReadDocumentInfoAsync(string pdfPath,
        CancellationToken cancellationToken = default)
    {
        var startInfo = new ProcessStartInfo
        {
            FileName = ResolveExecutable("ROLEMASTERDB_PDFINFO_PATH", "pdfinfo.exe"),
            RedirectStandardError = true,
            RedirectStandardOutput = true,
            UseShellExecute = false,
            CreateNoWindow = true
        };

        startInfo.ArgumentList.Add(pdfPath);

        var (output, error) = await RunProcessAsync(startInfo, cancellationToken);

        var pageCountMatch = Regex.Match(output, @"Pages:\s*(\d+)", RegexOptions.Multiline);
        var sizeMatch = Regex.Match(output, @"Page size:\s*([0-9.]+)\s*x\s*([0-9.]+)\s*pts", RegexOptions.Multiline);
        if (!pageCountMatch.Success || !sizeMatch.Success)
        {
            throw new InvalidOperationException($"pdfinfo output for '{pdfPath}' could not be parsed.");
        }

        return new PdfDocumentInfo(
            int.Parse(pageCountMatch.Groups[1].Value, CultureInfo.InvariantCulture),
            double.Parse(sizeMatch.Groups[1].Value, CultureInfo.InvariantCulture),
            double.Parse(sizeMatch.Groups[2].Value, CultureInfo.InvariantCulture));
    }

    public Task RenderPagePngAsync(
        string pdfPath,
        int pageNumber,
        string outputPath,
        CancellationToken cancellationToken = default) =>
        RenderPagePngAsync(pdfPath, pageNumber, outputPath, ScaledRenderDpi, cancellationToken);

    public Task RenderPagePngAsync(
        string pdfPath,
        int pageNumber,
        string outputPath,
        int renderDpi,
        CancellationToken cancellationToken = default) =>
        RenderPngAsync(pdfPath, pageNumber, outputPath, renderDpi, null, null, null, null, cancellationToken);

    public Task RenderCropPngAsync(
        string pdfPath,
        int pageNumber,
        int left,
        int top,
        int width,
        int height,
        string outputPath,
        CancellationToken cancellationToken = default) =>
        RenderCropPngAsync(pdfPath, pageNumber, left, top, width, height, outputPath, ScaledRenderDpi,
            cancellationToken);

    public Task RenderCropPngAsync(
        string pdfPath,
        int pageNumber,
        int left,
        int top,
        int width,
        int height,
        string outputPath,
        int renderDpi,
        CancellationToken cancellationToken = default) =>
        RenderPngAsync(pdfPath, pageNumber, outputPath, renderDpi, left, top, width, height, cancellationToken);

    private static async Task RenderPngAsync(
        string pdfPath,
        int pageNumber,
        string outputPath,
        int renderDpi,
        int? left,
        int? top,
        int? width,
        int? height,
        CancellationToken cancellationToken)
    {
        Directory.CreateDirectory(Path.GetDirectoryName(outputPath)!);

        var startInfo = new ProcessStartInfo
        {
            FileName = ResolveExecutable("ROLEMASTERDB_PDFTOPPM_PATH", "pdftoppm.exe"),
            RedirectStandardError = true,
            RedirectStandardOutput = true,
            UseShellExecute = false,
            CreateNoWindow = true
        };

        startInfo.ArgumentList.Add("-png");
        startInfo.ArgumentList.Add("-r");
        startInfo.ArgumentList.Add(renderDpi.ToString(CultureInfo.InvariantCulture));
        startInfo.ArgumentList.Add("-f");
        startInfo.ArgumentList.Add(pageNumber.ToString());
        startInfo.ArgumentList.Add("-l");
        startInfo.ArgumentList.Add(pageNumber.ToString());
        startInfo.ArgumentList.Add("-singlefile");

        if (left.HasValue && top.HasValue && width.HasValue && height.HasValue)
        {
            startInfo.ArgumentList.Add("-x");
            startInfo.ArgumentList.Add(left.Value.ToString());
            startInfo.ArgumentList.Add("-y");
            startInfo.ArgumentList.Add(top.Value.ToString());
            startInfo.ArgumentList.Add("-W");
            startInfo.ArgumentList.Add(width.Value.ToString());
            startInfo.ArgumentList.Add("-H");
            startInfo.ArgumentList.Add(height.Value.ToString());
        }

        startInfo.ArgumentList.Add(pdfPath);
        startInfo.ArgumentList.Add(Path.Combine(Path.GetDirectoryName(outputPath)!,
            Path.GetFileNameWithoutExtension(outputPath)));

        await RunProcessAsync(startInfo, cancellationToken);

        if (!File.Exists(outputPath))
        {
            throw new InvalidOperationException($"pdftoppm completed but did not create '{outputPath}'.");
        }
    }

    private static async Task<(string Output, string Error)> RunProcessAsync(
        ProcessStartInfo startInfo,
        CancellationToken cancellationToken)
    {
        using var process = new Process { StartInfo = startInfo };
        process.Start();

        var outputTask = startInfo.RedirectStandardOutput
            ? process.StandardOutput.ReadToEndAsync(cancellationToken)
            : Task.FromResult(string.Empty);
        var errorTask = startInfo.RedirectStandardError
            ? process.StandardError.ReadToEndAsync(cancellationToken)
            : Task.FromResult(string.Empty);

        await process.WaitForExitAsync(cancellationToken);
        var output = await outputTask;
        var error = await errorTask;

        if (process.ExitCode != 0)
        {
            throw new InvalidOperationException(string.IsNullOrWhiteSpace(error)
                ? $"{Path.GetFileNameWithoutExtension(startInfo.FileName)} failed with exit code {process.ExitCode}."
                : error);
        }

        return (output, error);
    }

    private static string ResolveExecutable(string environmentVariableName, string executableName)
    {
        var configuredPath = Environment.GetEnvironmentVariable(environmentVariableName);
        if (!string.IsNullOrWhiteSpace(configuredPath) && File.Exists(configuredPath))
        {
            return configuredPath;
        }

        var portablePath = Path.Combine(PortableMiKTeXPath, executableName);
        if (File.Exists(portablePath))
        {
            return portablePath;
        }

        return Path.GetFileNameWithoutExtension(executableName);
    }
}