//! Kitty graphics protocol support for image rendering
//!
//! This module provides functionality to convert images to Kitty graphics protocol format,
//! which is supported by Kitty terminal and other compatible terminal emulators.
//!
//! The Kitty graphics protocol allows displaying raster images directly in the terminal
//! with features like alpha blending, positioning, and scaling.

const std = @import("std");
const Allocator = std.mem.Allocator;

const Image = @import("../image.zig").Image;
const Interpolation = @import("../image/interpolation.zig").Interpolation;
const Rgb = @import("../color.zig").Rgb(u8);
const detect = @import("detect.zig");
const payload = @import("payload.zig");

/// Maximum payload size per escape sequence
const max_chunk_size: usize = 4096;

/// Options for Kitty graphics protocol encoding
pub const Options = struct {
    /// Suppress terminal responses (0=normal, 1=suppress OK, 2=suppress all)
    quiet: u2 = 1,
    /// Image placement ID (optional, for referencing the image later)
    image_id: ?u32 = null,
    /// Placement ID (optional, for multiple placements of same image)
    placement_id: ?u32 = null,
    /// Delete image after display
    delete_after: bool = false,
    /// Enable chunking for increased reliability (Ghostty doesn't support it)
    enable_chunking: bool = false,
    /// Display width in pixels (null = image's natural width)
    width: ?u32 = null,
    /// Display height in pixels (null = aspect-preserved from width, or natural height)
    height: ?u32 = null,
    /// Interpolation method to use when scaling the image
    interpolation: Interpolation = .bilinear,

    /// Default options for automatic formatting
    pub const default: Options = .{
        .quiet = 1,
        .image_id = null,
        .placement_id = null,
        .delete_after = false,
        .enable_chunking = false,
        .width = null,
        .height = null,
        .interpolation = .bilinear,
    };
};

fn writeControlHeader(output: *std.ArrayList(u8), gpa: Allocator, options: Options) !void {
    try output.print(gpa, "a=T,f=100,q={d}", .{options.quiet});
    if (options.image_id) |id| try output.print(gpa, ",i={d}", .{id});
    if (options.placement_id) |id| try output.print(gpa, ",p={d}", .{id});
    if (options.delete_after) try output.appendSlice(gpa, ",d=1");
}

/// Converts an image to Kitty graphics protocol format
pub fn fromImage(
    comptime T: type,
    image: Image(T),
    gpa: Allocator,
    options: Options,
) ![]u8 {
    const encoded = try payload.scaledPngBase64(T, image, gpa, options.width, options.height, options.interpolation);
    const base64_data = encoded.base64;
    defer gpa.free(base64_data);

    var output: std.ArrayList(u8) = .empty;
    errdefer output.deinit(gpa);
    try output.ensureTotalCapacity(gpa, base64_data.len + 64);

    if (!options.enable_chunking) {
        try output.appendSlice(gpa, "\x1b_G");
        try writeControlHeader(&output, gpa, options);
        try output.append(gpa, ';');
        try output.appendSlice(gpa, base64_data);
        try output.appendSlice(gpa, "\x1b\\");
        return output.toOwnedSlice(gpa);
    }

    var offset: usize = 0;
    var chunk_index: usize = 0;
    while (offset < base64_data.len) : (chunk_index += 1) {
        const is_last = offset + max_chunk_size >= base64_data.len;
        const chunk_end = if (is_last)
            base64_data.len
        else
            // Non-final chunks must be multiples of 4 to keep base64 aligned
            offset + (max_chunk_size & ~@as(usize, 3));
        const chunk = base64_data[offset..chunk_end];

        try output.appendSlice(gpa, "\x1b_G");
        if (chunk_index == 0) {
            try writeControlHeader(&output, gpa, options);
            if (!is_last) try output.appendSlice(gpa, ",m=1");
        } else if (!is_last) {
            try output.appendSlice(gpa, "m=1");
        }
        try output.append(gpa, ';');
        try output.appendSlice(gpa, chunk);
        try output.appendSlice(gpa, "\x1b\\");

        offset = chunk_end;
    }

    return output.toOwnedSlice(gpa);
}

/// Detects if the terminal supports Kitty graphics protocol
pub fn isSupported(io: std.Io) bool {
    if (!detect.isStdoutTty(io)) return false;
    return detect.isKittySupported(io) catch false;
}

// Tests
test "imageToKitty basic functionality" {
    const testing = std.testing;
    const allocator = testing.allocator;

    // Create a small test image
    var img = try Image(Rgb).init(allocator, 2, 2);
    defer img.deinit(allocator);

    // Fill with test colors
    img.at(0, 0).* = Rgb{ .r = 255, .g = 0, .b = 0 };
    img.at(0, 1).* = Rgb{ .r = 0, .g = 255, .b = 0 };
    img.at(1, 0).* = Rgb{ .r = 0, .g = 0, .b = 255 };
    img.at(1, 1).* = Rgb{ .r = 255, .g = 255, .b = 255 };

    // Convert to Kitty format
    const kitty_data = try fromImage(Rgb, img, allocator, .default);
    defer allocator.free(kitty_data);

    // Basic validation - should start with Kitty escape sequence
    try testing.expect(std.mem.startsWith(u8, kitty_data, "\x1b_G"));

    // Should end with escape sequence terminator
    try testing.expect(std.mem.endsWith(u8, kitty_data, "\x1b\\"));

    // Should contain PNG format specifier
    try testing.expect(std.mem.find(u8, kitty_data, "f=100") != null);

    // Should contain action=transmit
    try testing.expect(std.mem.find(u8, kitty_data, "a=T") != null);
}

test "imageToKitty with options" {
    const testing = std.testing;
    const allocator = testing.allocator;

    // Create a small test image
    var img = try Image(u8).init(allocator, 1, 1);
    defer img.deinit(allocator);
    img.at(0, 0).* = 128;

    // Test with custom options
    const options: Options = .{
        .quiet = 2,
        .image_id = 42,
        .placement_id = 7,
        .delete_after = true,
    };

    const kitty_data = try fromImage(u8, img, allocator, options);
    defer allocator.free(kitty_data);

    // Check that options are included
    try testing.expect(std.mem.find(u8, kitty_data, "q=2") != null);
    try testing.expect(std.mem.find(u8, kitty_data, "i=42") != null);
    try testing.expect(std.mem.find(u8, kitty_data, "p=7") != null);
    try testing.expect(std.mem.find(u8, kitty_data, "d=1") != null);
}

test "imageToKitty with scaling" {
    const testing = std.testing;
    const allocator = testing.allocator;

    // Create a small 4x4 test image
    var img = try Image(Rgb).init(allocator, 4, 4);
    defer img.deinit(allocator);

    // Fill with a pattern
    for (0..4) |y| {
        for (0..4) |x| {
            const val: u8 = @intCast((x + y) * 32);
            img.at(y, x).* = Rgb{ .r = val, .g = val, .b = val };
        }
    }

    // Test scaling up to 16x16
    const options: Options = .{ .width = 16, .height = 16 };

    const kitty_data = try fromImage(Rgb, img, allocator, options);
    defer allocator.free(kitty_data);

    // Should produce valid kitty output
    try testing.expect(std.mem.startsWith(u8, kitty_data, "\x1b_G"));
    try testing.expect(std.mem.endsWith(u8, kitty_data, "\x1b\\"));

    // The actual image data should be larger due to scaling
    // We can't easily verify the exact size due to PNG compression,
    // but we can check that it produces valid output
    try testing.expect(kitty_data.len > 100);
}

test "imageToKitty with chunking enabled" {
    const testing = std.testing;
    const allocator = testing.allocator;

    // Create a larger image with low-compressibility pixel data to trigger chunking
    var img = try Image(Rgb).init(allocator, 128, 128);
    defer img.deinit(allocator);

    for (0..img.rows) |y| {
        for (0..img.cols) |x| {
            const r: u8 = @intCast((x * 17 + y * 31) & 0xff);
            const g: u8 = @intCast((x * 43 + y * 7) & 0xff);
            const b: u8 = @intCast((x * 11 + y * 19) & 0xff);
            img.at(y, x).* = Rgb{ .r = r, .g = g, .b = b };
        }
    }

    const options: Options = .{
        .enable_chunking = true,
        .width = 512,
        .height = 512,
    };

    const kitty_data = try fromImage(Rgb, img, allocator, options);
    defer allocator.free(kitty_data);

    // Expect multiple escape sequences when chunking is active
    try testing.expect(std.mem.count(u8, kitty_data, "\x1b_G") >= 2);
    // Continuation flag should be present for non-final chunks
    try testing.expect(std.mem.find(u8, kitty_data, "m=1") != null);
    // Ensure the final terminator is present
    try testing.expect(std.mem.endsWith(u8, kitty_data, "\x1b\\"));
}
