const std = @import("std");
const sorvi = @import("sorvi");

pub const std_options: std.Options = .{
    .logFn = sorvi.defaultLog,
    .queryPageSize = sorvi.queryPageSize,
    .page_size_max = sorvi.page_size_max,
    .log_level = .debug,
};

pub const os = sorvi.os;
pub const panic = std.debug.FullPanic(sorvi.defaultPanic);

comptime {
    sorvi.init(@This(), .{
        .id = "org.sorvi.test.core.video_v1",
        .name = "test.core.video_v1",
        .version = "0.0.0",
        .core_extensions = &.{ .core_v1, .video_v1 },
        .frontend_extensions = &.{ .core_v1, .raster_v1 },
    });
}

ticks: usize = 0,
accumulated_error_ns: i64 = 0,
max_error_ns: i64 = std.math.minInt(i64),
min_error_ns: i64 = std.math.maxInt(i64),
prev_error_ns: i64 = 0,
max_delta_error_ns: u64 = 0,
bad_short_frames: usize = 0,

pub fn init(_: *@This()) !void {
    std.debug.assert(sorvi.video_v1.current_display_mode().id == .default);

    var num_defaults: usize = 0;
    for (sorvi.video_v1.query_display_modes()) |mode| {
        num_defaults += @intFromBool(mode.id == .default);
        std.debug.assert(mode.scale > 0.0);
        std.debug.assert(mode.w > 0);
        std.debug.assert(mode.h > 0);
    }
    std.debug.assert(num_defaults == 1);

    try sorvi.raster_v1.init(.{
        .format = .xrgb8888,
        .scaling = null,
        .direct = false,
    });

    try sorvi.video_v1.configure(.{
        .w = 1,
        .h = 1,
        .flags = .{ .border = false },
        .presentation = .fixed,
        .mode = .default,
    });
}

pub fn deinit(self: *@This()) void {
    const mean_error_ns = @divFloor(self.accumulated_error_ns, total_runs);
    const mean_bad = @abs(mean_error_ns) > std.time.ns_per_ms;
    const max_bad = @abs(self.max_error_ns) > std.time.ns_per_ms * 2;
    const min_bad = @abs(self.min_error_ns) > std.time.ns_per_ms * 2;
    const rel_bad = self.bad_short_frames > total_runs / 5;
    const delta_bad = self.max_delta_error_ns > std.time.ns_per_ms;
    if (mean_bad or max_bad or min_bad or rel_bad) {
        std.log.info("sorvi.test.suite: failed frontend timing tests", .{});
    } else {
        std.log.info("sorvi.test.suite: passed frontend timing tests", .{});
    }
    std.log.info("mean signed error: {}ns ({s})", .{ mean_error_ns, if (mean_bad) "BAD" else "OK" });
    std.log.info("max late: {}ns ({s})", .{ self.max_error_ns, if (max_bad) "BAD" else "OK" });
    std.log.info("max early: {}ns ({s})", .{ self.min_error_ns, if (min_bad) "BAD" else "OK" });
    std.log.info("bad short frames: {} ({s})", .{ self.bad_short_frames, if (rel_bad) "BAD" else "OK" });
    std.log.info("max pacing delta: {}ns ({s})", .{ self.max_delta_error_ns, if (delta_bad) "BAD" else "OK" });
}

const total_runs: usize = 128;
const targets: []const i64 = &.{
    std.time.ns_per_ms * 16,
    std.time.ns_per_ms * 33,
    std.time.ns_per_ms * 23,
    std.time.ns_per_ms * 8,
    std.time.ns_per_ms * 7,
};

pub fn videoTick(self: *@This(), frame: sorvi.video_v1.frame_t) !u64 {
    std.debug.assert(frame.w == 1 and frame.h == 1);
    if (self.ticks > 0) {
        const idx = self.ticks % targets.len;
        const error_ns: i64 = @as(i64, @intCast(frame.time_ns)) - targets[idx];
        self.accumulated_error_ns += error_ns;
        self.max_error_ns = @max(error_ns, self.max_error_ns);
        self.min_error_ns = @min(error_ns, self.min_error_ns);
        const rel = @as(f64, @floatFromInt(@abs(error_ns))) / @as(f64, @floatFromInt(targets[idx]));
        if (targets[idx] < 10 * std.time.ns_per_ms and @abs(error_ns) > 500_000 and rel > 0.10) {
            self.bad_short_frames += 1;
        }
        const delta = error_ns - self.prev_error_ns;
        self.max_delta_error_ns = @max(self.max_delta_error_ns, @abs(delta));
        self.prev_error_ns = error_ns;
    }
    if (self.ticks >= total_runs) {
        sorvi.core_v1.exit();
    }
    self.ticks += 1;
    const idx = self.ticks % targets.len;
    return @intCast(targets[idx]);
}