using System.Globalization;
using System.Text;

using RolemasterDb.ImportTool.Parsing;

namespace RolemasterDb.ImportTool;

public sealed class OcrCriticalSourceExtractor(PdfXmlExtractor pdfXmlExtractor) : ICriticalSourceExtractor
{
    private const int OcrRenderDpi = PdfXmlExtractor.ScaledRenderDpi;
    private const string TesseractExeDefaultPath = @"C:\Program Files\Sejda PDF Desktop\resources\vendor\tesseract-windows-x64\tesseract.exe";
    private const string TessdataDefaultPath = @"C:\Program Files\Sejda PDF Desktop\resources\vendor\tessdata";

    public async Task ExtractAsync(string pdfPath, ImportArtifactPaths artifactPaths, CancellationToken cancellationToken = default)
    {
        Directory.CreateDirectory(artifactPaths.DirectoryPath);
        Directory.CreateDirectory(artifactPaths.OcrPagesDirectoryPath);

        var info = await pdfXmlExtractor.ReadDocumentInfoAsync(pdfPath, cancellationToken);
        if (info.PageCount != 1)
        {
            throw new InvalidOperationException("The OCR extractor currently supports only single-page critical tables.");
        }

        var pageImagePath = artifactPaths.GetOcrPageImagePath(1);
        await pdfXmlExtractor.RenderPagePngAsync(pdfPath, 1, pageImagePath, OcrRenderDpi, cancellationToken);

        var tsvContent = await RunTesseractAsync(pageImagePath, cancellationToken);
        await File.WriteAllTextAsync(artifactPaths.OcrTsvPath, tsvContent, cancellationToken);
    }

    public async Task<ExtractedCriticalSource> LoadAsync(
        string pdfPath,
        ImportArtifactPaths artifactPaths,
        CancellationToken cancellationToken = default)
    {
        if (!File.Exists(artifactPaths.OcrTsvPath))
        {
            throw new FileNotFoundException($"Missing OCR artifact: {artifactPaths.OcrTsvPath}", artifactPaths.OcrTsvPath);
        }

        var pageImagePath = artifactPaths.GetOcrPageImagePath(1);
        if (!File.Exists(pageImagePath))
        {
            throw new FileNotFoundException($"Missing OCR page image artifact: {pageImagePath}", pageImagePath);
        }

        var tsvContent = await File.ReadAllTextAsync(artifactPaths.OcrTsvPath, cancellationToken);
        var (pageWidth, pageHeight) = ReadPngDimensions(pageImagePath);

        return new ExtractedCriticalSource(
            "ocr",
            "Imported from PDF OCR extraction.",
            SourceRenderProfile.OcrPixels(OcrRenderDpi),
            [new ParsedPdfPageGeometry(1, pageWidth, pageHeight)],
            ParseTsv(tsvContent));
    }

    internal static IReadOnlyList<PositionedTextFragment> ParseTsv(string tsvContent)
    {
        var lines = tsvContent
            .Split(["\r\n", "\n"], StringSplitOptions.RemoveEmptyEntries)
            .ToList();
        if (lines.Count == 0)
        {
            return [];
        }

        var fragments = new List<PositionedTextFragment>();
        foreach (var line in lines.Skip(1))
        {
            var columns = line.Split('\t');
            if (columns.Length < 12 || columns[0] != "5")
            {
                continue;
            }

            var text = CriticalTableParserSupport.NormalizeText(string.Join('\t', columns.Skip(11)));
            if (string.IsNullOrWhiteSpace(text))
            {
                continue;
            }

            fragments.Add(new PositionedTextFragment(
                int.Parse(columns[1], CultureInfo.InvariantCulture),
                int.Parse(columns[7], CultureInfo.InvariantCulture),
                int.Parse(columns[6], CultureInfo.InvariantCulture),
                int.Parse(columns[8], CultureInfo.InvariantCulture),
                int.Parse(columns[9], CultureInfo.InvariantCulture),
                text,
                ParseConfidence(columns[10])));
        }

        return fragments;
    }

    private static int? ParseConfidence(string value) =>
        int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var confidence) && confidence >= 0
            ? confidence
            : null;

    private static (int Width, int Height) ReadPngDimensions(string path)
    {
        using var stream = File.OpenRead(path);
        using var reader = new BinaryReader(stream, Encoding.UTF8, leaveOpen: false);
        var signature = reader.ReadBytes(8);
        var expectedSignature = new byte[] { 137, 80, 78, 71, 13, 10, 26, 10 };
        if (!signature.SequenceEqual(expectedSignature))
        {
            throw new InvalidOperationException($"'{path}' is not a PNG file.");
        }

        _ = ReadBigEndianInt32(reader);
        var chunkType = Encoding.ASCII.GetString(reader.ReadBytes(4));
        if (!string.Equals(chunkType, "IHDR", StringComparison.Ordinal))
        {
            throw new InvalidOperationException($"'{path}' is missing a PNG IHDR header.");
        }

        var width = ReadBigEndianInt32(reader);
        var height = ReadBigEndianInt32(reader);
        return (width, height);
    }

    private static int ReadBigEndianInt32(BinaryReader reader)
    {
        var bytes = reader.ReadBytes(4);
        if (bytes.Length != 4)
        {
            throw new EndOfStreamException("Unexpected end of stream.");
        }

        if (BitConverter.IsLittleEndian)
        {
            Array.Reverse(bytes);
        }

        return BitConverter.ToInt32(bytes, 0);
    }

    private static async Task<string> RunTesseractAsync(string imagePath, CancellationToken cancellationToken)
    {
        var startInfo = new System.Diagnostics.ProcessStartInfo
        {
            FileName = ResolveTesseractExecutable(),
            RedirectStandardError = true,
            RedirectStandardOutput = true,
            UseShellExecute = false,
            CreateNoWindow = true
        };

        startInfo.Environment["TESSDATA_PREFIX"] = ResolveTessdataPath();
        startInfo.ArgumentList.Add(imagePath);
        startInfo.ArgumentList.Add("stdout");
        startInfo.ArgumentList.Add("--psm");
        startInfo.ArgumentList.Add("11");
        startInfo.ArgumentList.Add("tsv");

        using var process = new System.Diagnostics.Process { StartInfo = startInfo };
        process.Start();
        var output = await process.StandardOutput.ReadToEndAsync(cancellationToken);
        await process.WaitForExitAsync(cancellationToken);

        if (process.ExitCode != 0)
        {
            var error = await process.StandardError.ReadToEndAsync(cancellationToken);
            throw new InvalidOperationException($"tesseract failed for '{imagePath}': {error}");
        }

        return output;
    }

    private static string ResolveTesseractExecutable()
    {
        var configuredPath = Environment.GetEnvironmentVariable("ROLEMASTERDB_TESSERACT_PATH");
        if (!string.IsNullOrWhiteSpace(configuredPath) && File.Exists(configuredPath))
        {
            return configuredPath;
        }

        if (File.Exists(TesseractExeDefaultPath))
        {
            return TesseractExeDefaultPath;
        }

        return "tesseract";
    }

    private static string ResolveTessdataPath()
    {
        var configuredPath = Environment.GetEnvironmentVariable("ROLEMASTERDB_TESSDATA_PREFIX");
        if (!string.IsNullOrWhiteSpace(configuredPath) && Directory.Exists(configuredPath))
        {
            return configuredPath;
        }

        if (Directory.Exists(TessdataDefaultPath))
        {
            return TessdataDefaultPath;
        }

        return string.Empty;
    }
}
