using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;

namespace Inspectron.Epson;

/// <summary>
/// Helper class for converting images to ESC/POS raster format
/// </summary>
public static class EpsonImageConverter
{
    /// <summary>
    /// Convert an image to ESC/POS raster format
    /// </summary>
    /// <param name="image">Source image</param>
    /// <param name="maxWidth">Maximum width in pixels (will maintain aspect ratio)</param>
    /// <param name="finalWidth">Output: final width after processing (byte-aligned)</param>
    /// <param name="finalHeight">Output: final height after processing</param>
    /// <returns>Byte array containing monochrome raster data</returns>
    public static byte[] ConvertToRasterData(
        Image<Rgba32> image,
        int maxWidth,
        out int finalWidth,
        out int finalHeight)
    {
        // Clone the image to avoid modifying the original
        using var processedImage = image.Clone();

        // Step 1: Resize if needed
        ResizeImage(processedImage, maxWidth);

        // Step 2: Convert to grayscale
        using var grayscaleImage = ConvertToGrayscale(processedImage);

        // Step 3: Apply dithering for better quality 1-bit conversion
        ApplyFloydSteinbergDithering(grayscaleImage);

        // Step 4: Calculate padded width (must be divisible by 8)
        int originalWidth = grayscaleImage.Width;
        int paddedWidth = ((originalWidth + 7) / 8) * 8;

        finalWidth = paddedWidth;
        finalHeight = grayscaleImage.Height;

        // Step 5: Pack pixels into bytes
        return PackPixelsToBytes(grayscaleImage, paddedWidth);
    }

    /// <summary>
    /// Resize image to fit within max width while maintaining aspect ratio
    /// </summary>
    public static void ResizeImage(Image<Rgba32> image, int maxWidth)
    {

        int newWidth = maxWidth;
        int newHeight = (int)((float)image.Height / image.Width * maxWidth);

        image.Mutate(x => x.Resize(new ResizeOptions
        {
            Size = new Size(newWidth, newHeight),
            Mode = ResizeMode.Max,
            Sampler = KnownResamplers.Lanczos3
        }));
    }

    /// <summary>
    /// Convert RGBA image to grayscale
    /// </summary>
    public static Image<L8> ConvertToGrayscale(Image<Rgba32> image)
    {
        var grayscaleImage = new Image<L8>(image.Width, image.Height);

        image.ProcessPixelRows(grayscaleImage, (sourceAccessor, targetAccessor) =>
        {
            for (int y = 0; y < sourceAccessor.Height; y++)
            {
                var sourceRow = sourceAccessor.GetRowSpan(y);
                var targetRow = targetAccessor.GetRowSpan(y);

                for (int x = 0; x < sourceAccessor.Width; x++)
                {
                    var pixel = sourceRow[x];

                    // Standard luminance calculation (ITU-R BT.709)
                    byte luminance = (byte)(
                        0.2126 * pixel.R +
                        0.7152 * pixel.G +
                        0.0722 * pixel.B
                    );

                    targetRow[x] = new L8(luminance);
                }
            }
        });

        return grayscaleImage;
    }

    /// <summary>
    /// Apply Floyd-Steinberg dithering algorithm for smooth 1-bit conversion
    /// </summary>
    public static void ApplyFloydSteinbergDithering(Image<L8> image)
    {
        int width = image.Width;
        int height = image.Height;

        // Create a copy of pixel data to work with
        var pixels = new float[height, width];

        image.ProcessPixelRows(accessor =>
        {
            for (int y = 0; y < height; y++)
            {
                var row = accessor.GetRowSpan(y);
                for (int x = 0; x < width; x++)
                {
                    pixels[y, x] = row[x].PackedValue;
                }
            }
        });

        // Apply Floyd-Steinberg dithering
        for (int y = 0; y < height; y++)
        {
            for (int x = 0; x < width; x++)
            {
                float oldPixel = pixels[y, x];
                float newPixel = oldPixel < 128 ? 0 : 255;
                pixels[y, x] = newPixel;

                float error = oldPixel - newPixel;

                // Distribute error to neighboring pixels
                if (x + 1 < width)
                    pixels[y, x + 1] += error * 7 / 16;

                if (y + 1 < height)
                {
                    if (x > 0)
                        pixels[y + 1, x - 1] += error * 3 / 16;

                    pixels[y + 1, x] += error * 5 / 16;

                    if (x + 1 < width)
                        pixels[y + 1, x + 1] += error * 1 / 16;
                }
            }
        }

        // Write dithered pixels back to image
        image.ProcessPixelRows(accessor =>
        {
            for (int y = 0; y < height; y++)
            {
                var row = accessor.GetRowSpan(y);
                for (int x = 0; x < width; x++)
                {
                    byte value = (byte)Math.Clamp(pixels[y, x], 0, 255);
                    row[x] = new L8(value);
                }
            }
        });
    }

    /// <summary>
    /// Convert image to column format bit image data for ESC * command
    /// </summary>
    /// <param name="image">Source image</param>
    /// <param name="maxWidth">Maximum width in pixels</param>
    /// <param name="mode">Bit image mode (8-dot or 24-dot)</param>
    /// <param name="finalWidth">Output: final width in dots</param>
    /// <param name="finalHeight">Output: final height in dots</param>
    /// <returns>Column format bit image data</returns>
    public static byte[] ConvertToColumnFormat(
        Image<Rgba32> image,
        int maxWidth,
        EpsonCommands.BitImageMode mode,
        out int finalWidth,
        out int finalHeight)
    {
        // Clone the image to avoid modifying the original
        using var processedImage = image.Clone();

        // Resize if needed
        ResizeImage(processedImage, maxWidth);

        // Convert to grayscale
        using var grayscaleImage = ConvertToGrayscale(processedImage);

        // Apply dithering
        ApplyFloydSteinbergDithering(grayscaleImage);

        int bitsPerColumn = mode == EpsonCommands.BitImageMode.SingleDensity8Dot ||
                            mode == EpsonCommands.BitImageMode.DoubleDensity8Dot ? 8 : 24;

        // Calculate padded height (must be divisible by column height)
        int originalHeight = grayscaleImage.Height;
        int paddedHeight = ((originalHeight + bitsPerColumn - 1) / bitsPerColumn) * bitsPerColumn;

        finalWidth = grayscaleImage.Width;
        finalHeight = paddedHeight;

        // Pack pixels into column format
        return PackPixelsToColumnFormat(grayscaleImage, bitsPerColumn, paddedHeight);
    }

    /// <summary>
    /// Pack pixels into column format (vertical bytes, left to right)
    /// </summary>
    private static byte[] PackPixelsToColumnFormat(Image<L8> image, int bitsPerColumn, int paddedHeight)
    {
        int width = image.Width;
        int height = image.Height;
        int bytesPerColumn = bitsPerColumn / 8;
        int totalColumns = width;
        int totalSlices = paddedHeight / bitsPerColumn;

        var columnData = new byte[totalColumns * totalSlices * bytesPerColumn];
        int dataIndex = 0;

        // Process image slice by slice (for 24-dot mode, we process 24 rows at a time)
        for (int slice = 0; slice < totalSlices; slice++)
        {
            int sliceStartY = slice * bitsPerColumn;

            // Process each column (x position) in this slice
            for (int x = 0; x < width; x++)
            {
                // For each byte in the column (1 byte for 8-dot, 3 bytes for 24-dot)
                for (int byteInColumn = 0; byteInColumn < bytesPerColumn; byteInColumn++)
                {
                    byte columnByte = 0;

                    // Pack 8 vertical pixels into one byte
                    for (int bit = 0; bit < 8; bit++)
                    {
                        int y = sliceStartY + (byteInColumn * 8) + bit;

                        // Get pixel value (white if outside image bounds)
                        byte pixelValue = 255; // Default to white
                        if (y < height)
                        {
                            image.ProcessPixelRows(accessor =>
                            {
                                var row = accessor.GetRowSpan(y);
                                pixelValue = row[x].PackedValue;
                            });
                        }

                        // Threshold: < 128 = black (1), >= 128 = white (0)
                        bool isBlack = pixelValue < 128;

                        if (isBlack)
                        {
                            columnByte |= (byte)(1 << (7 - bit)); // MSB = top pixel
                        }
                    }

                    columnData[dataIndex++] = columnByte;
                }
            }
        }

        return columnData;
    }

    /// <summary>
    /// Pack grayscale pixels into monochrome bytes (8 pixels per byte)
    /// </summary>
    private static byte[] PackPixelsToBytes(Image<L8> image, int paddedWidth)
    {
        int bytesPerRow = paddedWidth / 8;
        int height = image.Height;
        var rasterData = new byte[bytesPerRow * height];

        image.ProcessPixelRows(accessor =>
        {
            for (int y = 0; y < height; y++)
            {
                var row = accessor.GetRowSpan(y);
                int rowOffset = y * bytesPerRow;

                for (int x = 0; x < paddedWidth; x++)
                {
                    // Get pixel value (0 or 255 after dithering)
                    byte pixelValue = x < image.Width ? row[x].PackedValue : (byte)255;

                    // Threshold: < 128 = black (1), >= 128 = white (0)
                    bool isBlack = pixelValue < 128;

                    if (isBlack)
                    {
                        int byteIndex = rowOffset + (x / 8);
                        int bitPosition = 7 - (x % 8); // MSB first
                        rasterData[byteIndex] |= (byte)(1 << bitPosition);
                    }
                }
            }
        });

        return rasterData;
    }
}
