/// Generates random terminal OSC requests.
const Osc = @This();

const std = @import("std");
const assert = std.debug.assert;
const Generator = @import("Generator.zig");
const Bytes = @import("Bytes.zig");
const urlPercentEncode = @import("../os/string_encoding.zig").urlPercentEncode;

/// Valid OSC request kinds that can be generated.
pub const ValidKind = enum {
    change_window_title,
    prompt_start,
    prompt_end,
    end_of_input,
    end_of_command,
    rxvt_notify,
    mouse_shape,
    clipboard_operation,
    report_pwd,
    hyperlink_start,
    hyperlink_end,
    conemu_progress,
    iterm2_notification,
};

/// Invalid OSC request kinds that can be generated.
pub const InvalidKind = enum {
    /// Literally random bytes. Might even be valid, but probably not.
    random,

    /// A good prefix, but ultimately invalid format.
    good_prefix,
};

/// Random number generator.
rand: std.Random,

/// Probability of a valid OSC sequence being generated.
p_valid: f64 = 1.0,

/// Probabilities of specific valid or invalid OSC request kinds.
/// The probabilities are weighted relative to each other, so they
/// can sum greater than 1.0. A kind of weight 1.0 and a kind of
/// weight 2.0 will have a 2:1 chance of the latter being selected.
p_valid_kind: std.enums.EnumArray(ValidKind, f64) = .initFill(1.0),
p_invalid_kind: std.enums.EnumArray(InvalidKind, f64) = .initFill(1.0),

fn checkKvAlphabet(c: u8) bool {
    return switch (c) {
        std.ascii.control_code.esc, std.ascii.control_code.bel, ';', '=' => false,
        else => std.ascii.isPrint(c),
    };
}

/// The alphabet for random bytes in OSC key/value pairs (omitting 0x1B,
/// 0x07, ';', '=').
pub const kv_alphabet = Bytes.generateAlphabet(checkKvAlphabet);

fn checkOscAlphabet(c: u8) bool {
    return switch (c) {
        std.ascii.control_code.esc, std.ascii.control_code.bel => false,
        else => true,
    };
}

/// The alphabet for random bytes in OSCs (omitting 0x1B and 0x07).
pub const osc_alphabet = Bytes.generateAlphabet(checkOscAlphabet);
pub const ascii_alphabet = Bytes.generateAlphabet(std.ascii.isPrint);
pub const alphabetic_alphabet = Bytes.generateAlphabet(std.ascii.isAlphabetic);
pub const alphanumeric_alphabet = Bytes.generateAlphabet(std.ascii.isAlphanumeric);

pub fn generator(self: *Osc) Generator {
    return .init(self, next);
}

const osc = std.fmt.comptimePrint("{c}]", .{std.ascii.control_code.esc});
const st = std.fmt.comptimePrint("{c}", .{std.ascii.control_code.bel});

/// Get the next OSC request in bytes. The generated OSC request will
/// have the prefix `ESC ]` and the terminator `BEL` (0x07).
///
/// This will generate both valid and invalid OSC requests (based on
/// the `p_valid` probability value). Invalid requests still have the
/// prefix and terminator, but the content in between is not a valid
/// OSC request.
///
/// The buffer must be at least 3 bytes long to accommodate the
/// prefix and terminator.
pub fn next(self: *Osc, writer: *std.Io.Writer, max_len: usize) Generator.Error!void {
    assert(max_len >= 3);
    try writer.writeAll(osc);
    try self.nextUnwrapped(writer, max_len - (osc.len + st.len));
    try writer.writeAll(st);
}

fn nextUnwrapped(self: *Osc, writer: *std.Io.Writer, max_len: usize) Generator.Error!void {
    return switch (self.chooseValidity()) {
        .valid => valid: {
            const Indexer = @TypeOf(self.p_valid_kind).Indexer;
            const idx = self.rand.weightedIndex(f64, &self.p_valid_kind.values);
            break :valid try self.nextUnwrappedValidExact(
                writer,
                Indexer.keyForIndex(idx),
                max_len,
            );
        },

        .invalid => invalid: {
            const Indexer = @TypeOf(self.p_invalid_kind).Indexer;
            const idx = self.rand.weightedIndex(f64, &self.p_invalid_kind.values);
            break :invalid try self.nextUnwrappedInvalidExact(
                writer,
                Indexer.keyForIndex(idx),
                max_len,
            );
        },
    };
}

fn nextUnwrappedValidExact(self: *const Osc, writer: *std.Io.Writer, k: ValidKind, max_len: usize) Generator.Error!void {
    switch (k) {
        .change_window_title => change_window_title: {
            if (max_len < 3) break :change_window_title;
            try writer.print("0;{f}", .{self.bytes().atMost(max_len - 3)}); // Set window title
        },

        .prompt_start => prompt_start: {
            if (max_len < 4) break :prompt_start;
            var remaining = max_len;

            try writer.writeAll("133;A"); // Start prompt
            remaining -= 4;

            // aid
            if (self.rand.boolean()) aid: {
                if (remaining < 6) break :aid;
                try writer.writeAll(";aid=");
                remaining -= 5;
                remaining -= try self.bytes().newAlphabet(kv_alphabet).atMost(@min(16, remaining)).write(writer);
            }

            // redraw
            if (self.rand.boolean()) redraw: {
                if (remaining < 9) break :redraw;
                try writer.writeAll(";redraw=");
                if (self.rand.boolean()) {
                    try writer.writeAll("1");
                } else {
                    try writer.writeAll("0");
                }
                remaining -= 9;
            }
        },

        .prompt_end => prompt_end: {
            if (max_len < 4) break :prompt_end;
            try writer.writeAll("133;B"); // End prompt
        },

        .end_of_input => end_of_input: {
            if (max_len < 5) break :end_of_input;
            var remaining = max_len;
            try writer.writeAll("133;C"); // End prompt
            remaining -= 5;
            if (self.rand.boolean()) cmdline: {
                const prefix = ";cmdline_url=";
                if (remaining < prefix.len + 1) break :cmdline;
                try writer.writeAll(prefix);
                remaining -= prefix.len;
                var buf: [128]u8 = undefined;
                var w: std.Io.Writer = .fixed(&buf);
                try self.bytes().newAlphabet(ascii_alphabet).atMost(@min(remaining, buf.len)).format(&w);
                try urlPercentEncode(writer, w.buffered());
                remaining -= w.buffered().len;
            }
        },

        .end_of_command => end_of_command: {
            if (max_len < 4) break :end_of_command;
            try writer.writeAll("133;D"); // End prompt
            if (self.rand.boolean()) exit_code: {
                if (max_len < 7) break :exit_code;
                try writer.print(";{d}", .{self.rand.int(u8)});
            }
        },

        .mouse_shape => mouse_shape: {
            if (max_len < 4) break :mouse_shape;
            try writer.print("22;{f}", .{self.bytes().newAlphabet(alphabetic_alphabet).atMost(@min(32, max_len - 3))}); // Start prompt
        },

        .rxvt_notify => rxvt_notify: {
            const prefix = "777;notify;";
            if (max_len < prefix.len) break :rxvt_notify;
            var remaining = max_len;
            try writer.writeAll(prefix);
            remaining -= prefix.len;
            remaining -= try self.bytes().newAlphabet(kv_alphabet).atMost(@min(remaining - 2, 32)).write(writer);
            try writer.writeByte(';');
            remaining -= 1;
            remaining -= try self.bytes().newAlphabet(osc_alphabet).atMost(remaining).write(writer);
        },

        .clipboard_operation => {
            try writer.writeAll("52;");
            var remaining = max_len - 3;
            if (self.rand.boolean()) {
                remaining -= try self.bytes().newAlphabet(alphabetic_alphabet).atMost(1).write(writer);
            }
            try writer.writeByte(';');
            remaining -= 1;
            if (self.rand.boolean()) {
                remaining -= try self.bytes().newAlphabet(osc_alphabet).atMost(remaining).write(writer);
            }
        },

        .report_pwd => report_pwd: {
            const prefix = "7;file://localhost";
            if (max_len < prefix.len) break :report_pwd;
            var remaining = max_len;
            try writer.writeAll(prefix);
            remaining -= prefix.len;
            for (0..self.rand.intRangeAtMost(usize, 2, 5)) |_| {
                try writer.writeByte('/');
                remaining -= 1;
                remaining -= try self.bytes().newAlphabet(alphanumeric_alphabet).atMost(@min(16, remaining)).write(writer);
            }
        },

        .hyperlink_start => {
            try writer.writeAll("8;");
            if (self.rand.boolean()) {
                try writer.print("id={f}", .{self.bytes().newAlphabet(alphanumeric_alphabet).atMost(16)});
            }
            try writer.writeAll(";https://localhost");
            for (0..self.rand.intRangeAtMost(usize, 2, 5)) |_| {
                try writer.print("/{f}", .{self.bytes().newAlphabet(alphanumeric_alphabet).atMost(16)});
            }
        },

        .hyperlink_end => hyperlink_end: {
            if (max_len < 3) break :hyperlink_end;
            try writer.writeAll("8;;");
        },

        .conemu_progress => {
            try writer.writeAll("9;");
            switch (self.rand.intRangeAtMost(u3, 0, 4)) {
                0, 3 => |c| {
                    try writer.print(";{d}", .{c});
                },
                1, 2, 4 => |c| {
                    if (self.rand.boolean()) {
                        try writer.print(";{d}", .{c});
                    } else {
                        try writer.print(";{d};{d}", .{ c, self.rand.intRangeAtMost(u8, 0, 100) });
                    }
                },
                else => unreachable,
            }
        },

        .iterm2_notification => iterm2_notification: {
            if (max_len < 3) break :iterm2_notification;
            // add a prefix to ensure that this is not interpreted as a ConEmu OSC
            try writer.print("9;_{f}", .{self.bytes().newAlphabet(ascii_alphabet).atMost(max_len - 3)});
        },
    }
}

fn nextUnwrappedInvalidExact(
    self: *const Osc,
    writer: *std.Io.Writer,
    k: InvalidKind,
    max_len: usize,
) Generator.Error!void {
    switch (k) {
        .random => {
            try self.bytes().atMost(max_len).format(writer);
        },

        .good_prefix => {
            try writer.print("133;{f}", .{self.bytes().atMost(max_len - 4)});
        },
    }
}

fn bytes(self: *const Osc) Bytes {
    return .{
        .rand = self.rand,
        .alphabet = osc_alphabet,
    };
}

/// Choose whether to generate a valid or invalid OSC request based
/// on the validity probability.
fn chooseValidity(self: *const Osc) Validity {
    return if (self.rand.float(f64) > self.p_valid)
        .invalid
    else
        .valid;
}

const Validity = enum { valid, invalid };

/// A fixed seed we can use for our tests to avoid flakes.
const test_seed = 0xC0FFEEEEEEEEEEEE;

test "OSC generator" {
    const testing = std.testing;
    var prng = std.Random.DefaultPrng.init(test_seed);
    var buf: [256]u8 = undefined;
    {
        var v: Osc = .{
            .rand = prng.random(),
        };
        const gen = v.generator();
        for (0..50) |_| {
            var writer: std.Io.Writer = .fixed(&buf);
            try gen.next(&writer, buf.len);
            const result = writer.buffered();
            try testing.expect(result.len > 0);
        }
    }
}

test "OSC generator valid" {
    const testing = std.testing;
    const terminal = @import("../terminal/main.zig");

    var prng = std.Random.DefaultPrng.init(test_seed);
    var buf: [256]u8 = undefined;
    var gen: Osc = .{
        .rand = prng.random(),
        .p_valid = 1.0,
    };
    for (0..50) |_| {
        var writer: std.Io.Writer = .fixed(&buf);
        try gen.next(&writer, buf.len);
        const seq = writer.buffered();
        var parser: terminal.osc.Parser = .init(null);
        for (seq[2 .. seq.len - 1]) |c| parser.next(c);
        try testing.expect(parser.end(null) != null);
    }
}

test "OSC generator invalid" {
    const testing = std.testing;
    const terminal = @import("../terminal/main.zig");

    var prng = std.Random.DefaultPrng.init(test_seed);
    var buf: [256]u8 = undefined;
    var gen: Osc = .{
        .rand = prng.random(),
        .p_valid = 0.0,
    };
    for (0..50) |_| {
        var writer: std.Io.Writer = .fixed(&buf);
        try gen.next(&writer, buf.len);
        const seq = writer.buffered();
        var parser: terminal.osc.Parser = .init(null);
        for (seq[2 .. seq.len - 1]) |c| parser.next(c);
        try testing.expect(parser.end(null) == null);
    }
}
