// Content Compliance oracle: Unicode 17.0.0 default lowercase.
//
// This component *declares* conformance cases through the host bridge and
// owns its own memory — no impl imports, no shared linear memory, no
// failure_* exports. Case identity is (component hash, seed, ordinal).
//
// Cases come from two sources:
//   1. Curated inputs (generated by tools/casegen from UCD 17.0.0).
//   2. Deterministic fuzz cases from a xorshift32 PRNG (seed via the
//      optional uniform_set_seed), biased toward U+03A3, sigma forms, and
//      combining marks so the Final_Sigma context rule is exercised hard.
// Expected outputs are computed by the embedded UCD-derived oracle: full
// lowercase mappings, the Final_Sigma rule (Cased/Case_Ignorable context),
// and invalid UTF-8 bytes passed through unchanged.
const tables = @import("unicode-17-lowercase-tables.zig");
const fixtures = @import("unicode-17-lowercase-fixtures.zig");

extern "qip" fn must_render_exactly(
    ordinal: u64,
    input_ptr: u32,
    input_len: u32,
    expected_ptr: u32,
    expected_len: u32,
) i32;
extern "qip" fn must_render_into(
    ordinal: u64,
    input_ptr: u32,
    input_len: u32,
    out_ptr: u32,
    out_cap: u32,
) i32;
extern "qip" fn must_render_into_emit_error(ordinal: u64, message_ptr: u32, message_len: u32) i32;
extern "qip" fn must_render_into_finish(ordinal: u64, error_count: u32) i32;

const render_into_failed_message = "render failed or output did not fit";
const property_failed_message = "rendered output failed property check";

const FUZZ_CASES: u32 = 64;
const IDEMPOTENCE_CASES: u32 = 12;
const ASCII_CASES: u32 = 8;
const MAX_INPUT: usize = 256;
// Worst-case growth is 1.5x; give the oracle and must_render_into buffers 3x slack.
const MAX_EXPECTED: usize = MAX_INPUT * 3;

var input_buf: [MAX_INPUT]u8 = undefined;
var expected_buf: [MAX_EXPECTED]u8 = undefined;
var out1_buf: [MAX_EXPECTED]u8 = undefined;
var out2_buf: [MAX_EXPECTED]u8 = undefined;

var seed: u32 = 17;
var ordinal: u64 = 0;

export fn uniform_set_seed(value: i32) void {
    seed = @bitCast(value);
}

const Decoded = struct {
    cp: u32,
    size: usize,
    valid: bool,
};

// Strict UTF-8 decode with Go utf8.DecodeRune acceptance (compliance
// components are self-contained: no imports from components/).
fn decode(bytes: []const u8) Decoded {
    const b0 = bytes[0];
    if (b0 < 0x80) return .{ .cp = b0, .size = 1, .valid = true };
    if (b0 < 0xC2) return .{ .cp = 0, .size = 1, .valid = false };

    if (b0 < 0xE0) {
        if (bytes.len < 2) return .{ .cp = 0, .size = 1, .valid = false };
        const b1 = bytes[1];
        if (b1 < 0x80 or b1 > 0xBF) return .{ .cp = 0, .size = 1, .valid = false };
        return .{ .cp = (@as(u32, b0 & 0x1F) << 6) | @as(u32, b1 & 0x3F), .size = 2, .valid = true };
    }

    if (b0 < 0xF0) {
        if (bytes.len < 3) return .{ .cp = 0, .size = 1, .valid = false };
        const b1 = bytes[1];
        const lo: u8 = if (b0 == 0xE0) 0xA0 else 0x80;
        const hi: u8 = if (b0 == 0xED) 0x9F else 0xBF;
        if (b1 < lo or b1 > hi) return .{ .cp = 0, .size = 1, .valid = false };
        const b2 = bytes[2];
        if (b2 < 0x80 or b2 > 0xBF) return .{ .cp = 0, .size = 1, .valid = false };
        const cp = (@as(u32, b0 & 0x0F) << 12) | (@as(u32, b1 & 0x3F) << 6) | @as(u32, b2 & 0x3F);
        return .{ .cp = cp, .size = 3, .valid = true };
    }

    if (b0 < 0xF5) {
        if (bytes.len < 4) return .{ .cp = 0, .size = 1, .valid = false };
        const b1 = bytes[1];
        const lo: u8 = if (b0 == 0xF0) 0x90 else 0x80;
        const hi: u8 = if (b0 == 0xF4) 0x8F else 0xBF;
        if (b1 < lo or b1 > hi) return .{ .cp = 0, .size = 1, .valid = false };
        const b2 = bytes[2];
        if (b2 < 0x80 or b2 > 0xBF) return .{ .cp = 0, .size = 1, .valid = false };
        const b3 = bytes[3];
        if (b3 < 0x80 or b3 > 0xBF) return .{ .cp = 0, .size = 1, .valid = false };
        const cp = (@as(u32, b0 & 0x07) << 18) | (@as(u32, b1 & 0x3F) << 12) |
            (@as(u32, b2 & 0x3F) << 6) | @as(u32, b3 & 0x3F);
        return .{ .cp = cp, .size = 4, .valid = true };
    }

    return .{ .cp = 0, .size = 1, .valid = false };
}

fn encodeUtf8(cp: u32, out: []u8) usize {
    if (cp < 0x80) {
        out[0] = @intCast(cp);
        return 1;
    }
    if (cp < 0x800) {
        out[0] = @intCast(0xC0 | (cp >> 6));
        out[1] = @intCast(0x80 | (cp & 0x3F));
        return 2;
    }
    if (cp < 0x10000) {
        out[0] = @intCast(0xE0 | (cp >> 12));
        out[1] = @intCast(0x80 | ((cp >> 6) & 0x3F));
        out[2] = @intCast(0x80 | (cp & 0x3F));
        return 3;
    }
    out[0] = @intCast(0xF0 | (cp >> 18));
    out[1] = @intCast(0x80 | ((cp >> 12) & 0x3F));
    out[2] = @intCast(0x80 | ((cp >> 6) & 0x3F));
    out[3] = @intCast(0x80 | (cp & 0x3F));
    return 4;
}

fn inRanges(ranges: []const u32, cp: u32) bool {
    var lo: usize = 0;
    var hi: usize = ranges.len / 2;
    while (lo < hi) {
        const mid = lo + (hi - lo) / 2;
        if (ranges[mid * 2 + 1] < cp) {
            lo = mid + 1;
        } else {
            hi = mid;
        }
    }
    return lo < ranges.len / 2 and ranges[lo * 2] <= cp;
}

fn isCased(cp: u32) bool {
    return inRanges(&tables.cased_ranges, cp);
}

fn isCaseIgnorable(cp: u32) bool {
    return inRanges(&tables.case_ignorable_ranges, cp);
}

// Returns the mapped UTF-8 bytes, or null when cp lowercases to itself.
fn lookupLower(cp: u32) ?[]const u8 {
    var lo: usize = 0;
    var hi: usize = tables.map_keys.len;
    while (lo < hi) {
        const mid = lo + (hi - lo) / 2;
        if (tables.map_keys[mid] < cp) {
            lo = mid + 1;
        } else {
            hi = mid;
        }
    }
    if (lo >= tables.map_keys.len or tables.map_keys[lo] != cp) return null;
    const packed_val = tables.map_packed[lo];
    const off = packed_val >> 8;
    const len = packed_val & 0xFF;
    return tables.map_blob[off .. off + len];
}

const SIGMA: u32 = 0x03A3;
const SIGMA_FINAL = [_]u8{ 0xCF, 0x82 }; // ς
const SIGMA_SMALL = [_]u8{ 0xCF, 0x83 }; // σ

fn followedByCased(bytes: []const u8) bool {
    var i: usize = 0;
    while (i < bytes.len) {
        const d = decode(bytes[i..]);
        if (!d.valid) return false;
        if (isCaseIgnorable(d.cp)) {
            i += d.size;
            continue;
        }
        return isCased(d.cp);
    }
    return false;
}

// The embedded oracle covers full default lowercase with Final_Sigma over the
// valid UTF-8 input domain.
fn oracleLower(input: []const u8, out: []u8) usize {
    var out_len: usize = 0;
    var prev_cased = false;
    var i: usize = 0;
    while (i < input.len) {
        const d = decode(input[i..]);
        if (!d.valid) @trap();
        if (d.cp == SIGMA) {
            const final = prev_cased and !followedByCased(input[i + d.size ..]);
            const mapped: []const u8 = if (final) &SIGMA_FINAL else &SIGMA_SMALL;
            for (mapped) |b| {
                out[out_len] = b;
                out_len += 1;
            }
            prev_cased = true;
            i += d.size;
            continue;
        }
        if (lookupLower(d.cp)) |lower| {
            for (lower) |b| {
                out[out_len] = b;
                out_len += 1;
            }
        } else {
            for (input[i .. i + d.size]) |b| {
                out[out_len] = b;
                out_len += 1;
            }
        }
        if (!isCaseIgnorable(d.cp)) {
            prev_cased = isCased(d.cp);
        }
        i += d.size;
    }
    return out_len;
}

fn declare(input: []const u8) i32 {
    const expected_len = oracleLower(input, expected_buf[0..]);
    const status = must_render_exactly(
        ordinal,
        @intCast(@intFromPtr(input.ptr)),
        @intCast(input.len),
        @intCast(@intFromPtr(&expected_buf)),
        @intCast(expected_len),
    );
    ordinal += 1;
    return status;
}

fn renderInto(input: []const u8, out: []u8) i32 {
    return must_render_into(
        ordinal,
        @intCast(@intFromPtr(input.ptr)),
        @intCast(input.len),
        @intCast(@intFromPtr(out.ptr)),
        @intCast(out.len),
    );
}

fn renderIntoVerdict(ok: bool) void {
    if (ok) {
        _ = must_render_into_finish(ordinal, 0);
    } else {
        _ = must_render_into_emit_error(ordinal, @intCast(@intFromPtr(property_failed_message.ptr)), property_failed_message.len);
        _ = must_render_into_finish(ordinal, 1);
    }
    ordinal += 1;
}

fn nextRandom(state: *u32) u32 {
    var x = state.*;
    x ^= x << 13;
    x ^= x >> 17;
    x ^= x << 5;
    state.* = x;
    return x;
}

// Sigma-heavy alphabet so Final_Sigma boundaries (word-final, before
// punctuation, and across combining marks) come up
// constantly instead of rarely.
const SIGMA_MIX = [_]u32{ 0x03A3, 0x03A3, 0x03C2, 0x03C3, 0x0301, 0x0307, 0x0345, 0x0391, 0x0041, 0x0020, 0x002E, 0x0027 };

fn buildFuzzInput(rng: *u32) []const u8 {
    const target = nextRandom(rng) % 48;
    var len: usize = 0;
    var unit: usize = 0;
    while (unit < target and len + 4 <= MAX_INPUT) : (unit += 1) {
        switch (nextRandom(rng) % 5) {
            0 => {
                input_buf[len] = @intCast(0x20 + (nextRandom(rng) % 0x5F));
                len += 1;
            },
            1 => {
                input_buf[len] = @intCast(0x20 + (nextRandom(rng) % 0x5F));
                len += 1;
            },
            2 => {
                const key = tables.map_keys[nextRandom(rng) % tables.map_keys.len];
                len += encodeUtf8(key, input_buf[len..]);
            },
            3 => {
                len += encodeUtf8(SIGMA_MIX[nextRandom(rng) % SIGMA_MIX.len], input_buf[len..]);
            },
            else => {
                var cp = nextRandom(rng) % 0x2000;
                if (cp >= 0xD800 and cp <= 0xDFFF) cp = 0x3B1;
                len += encodeUtf8(cp, input_buf[len..]);
            },
        }
    }
    return input_buf[0..len];
}

export fn comply() i32 {
    ordinal = 0;

    for (fixtures.cases) |case| {
        _ = declare(case.input);
    }

    var rng: u32 = if (seed == 0) 0x9E3779B9 else seed;
    var i: u32 = 0;
    while (i < FUZZ_CASES) : (i += 1) {
        const input = buildFuzzInput(&rng);
        _ = declare(input);
    }

    // Property: lowercase is idempotent — lower(lower(x)) == lower(x).
    // Holds even across Final_Sigma: ς and σ both lowercase to themselves.
    i = 0;
    while (i < IDEMPOTENCE_CASES) : (i += 1) {
        const input = buildFuzzInput(&rng);
        const n1 = renderInto(input, out1_buf[0..]);
        if (n1 < 0) {
            _ = must_render_into_emit_error(ordinal, @intCast(@intFromPtr(render_into_failed_message.ptr)), render_into_failed_message.len);
            _ = must_render_into_finish(ordinal, 1);
            ordinal += 1;
            continue;
        }
        const len1: usize = @intCast(n1);
        _ = must_render_into_finish(ordinal, 0);
        ordinal += 1;
        _ = must_render_exactly(ordinal, @intCast(@intFromPtr(&out1_buf)), @intCast(len1), @intCast(@intFromPtr(&out1_buf)), @intCast(len1));
        ordinal += 1;
    }

    // Property: ASCII input must produce ASCII output.
    i = 0;
    while (i < ASCII_CASES) : (i += 1) {
        const target = nextRandom(&rng) % 40;
        var len: usize = 0;
        while (len < target) : (len += 1) {
            input_buf[len] = @intCast(0x20 + (nextRandom(&rng) % 0x5F));
        }
        const n = renderInto(input_buf[0..len], out1_buf[0..]);
        var ok = n >= 0;
        if (ok) {
            for (out1_buf[0..@intCast(n)]) |b| {
                if (b >= 0x80) {
                    ok = false;
                    break;
                }
            }
        }
        renderIntoVerdict(ok);
    }

    return @intCast(ordinal & 0x7FFFFFFF);
}
