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

const ly_ui = @import("ly-ui");
const Cell = ly_ui.Cell;
const TerminalBuffer = ly_ui.TerminalBuffer;
const Widget = ly_ui.Widget;

const ly_core = ly_ui.ly_core;
const interop = ly_core.interop;
const TimeOfDay = interop.TimeOfDay;

const Doom = @This();

pub const STEPS = 12;
pub const HEIGHT_MAX = 9;
pub const SPREAD_MAX = 4;

instance: ?Widget = null,
start_time: TimeOfDay,
allocator: Allocator,
terminal_buffer: *TerminalBuffer,
animate: *bool,
timeout_sec: u12,
frame_delay: u16,
buffer: []u8,
height: u8,
spread: u8,
fire: [STEPS + 1]Cell,

pub fn init(
    allocator: Allocator,
    terminal_buffer: *TerminalBuffer,
    top_color: u32,
    middle_color: u32,
    bottom_color: u32,
    fire_height: u8,
    fire_spread: u8,
    animate: *bool,
    timeout_sec: u12,
    frame_delay: u16,
) !Doom {
    const buffer = try allocator.alloc(u8, terminal_buffer.width * terminal_buffer.height);
    initBuffer(buffer, terminal_buffer.width);

    const levels =
        [_]Cell{
            Cell.init(' ', terminal_buffer.bg, terminal_buffer.bg),
            Cell.init(0x2591, top_color, terminal_buffer.bg),
            Cell.init(0x2592, top_color, terminal_buffer.bg),
            Cell.init(0x2593, top_color, terminal_buffer.bg),
            Cell.init(0x2588, top_color, terminal_buffer.bg),
            Cell.init(0x2591, middle_color, top_color),
            Cell.init(0x2592, middle_color, top_color),
            Cell.init(0x2593, middle_color, top_color),
            Cell.init(0x2588, middle_color, top_color),
            Cell.init(0x2591, bottom_color, middle_color),
            Cell.init(0x2592, bottom_color, middle_color),
            Cell.init(0x2593, bottom_color, middle_color),
            Cell.init(0x2588, bottom_color, middle_color),
        };

    return .{
        .instance = null,
        .start_time = try interop.getTimeOfDay(),
        .allocator = allocator,
        .terminal_buffer = terminal_buffer,
        .animate = animate,
        .timeout_sec = timeout_sec,
        .frame_delay = frame_delay,
        .buffer = buffer,
        .height = @min(HEIGHT_MAX, fire_height),
        .spread = @min(SPREAD_MAX, fire_spread),
        .fire = levels,
    };
}

pub fn widget(self: *Doom) *Widget {
    if (self.instance) |*instance| return instance;
    self.instance = Widget.init(
        "Doom",
        null,
        self,
        deinit,
        realloc,
        draw,
        update,
        null,
        calculateTimeout,
    );
    return &self.instance.?;
}

fn deinit(self: *Doom) void {
    self.allocator.free(self.buffer);
}

fn realloc(self: *Doom) !void {
    const buffer = try self.allocator.realloc(self.buffer, self.terminal_buffer.width * self.terminal_buffer.height);
    initBuffer(buffer, self.terminal_buffer.width);
    self.buffer = buffer;
}

fn draw(self: *Doom) void {
    if (!self.animate.*) return;

    for (0..self.terminal_buffer.width) |x| {
        // We start from 1 so that we always have the topmost line when spreading fire
        for (1..self.terminal_buffer.height) |y| {
            // Get index of current cell in fire level buffer
            const from = y * self.terminal_buffer.width + x;

            // Generate random data for fire propagation
            const rand_loss = self.terminal_buffer.random.intRangeAtMost(u8, 0, HEIGHT_MAX);
            const rand_spread = self.terminal_buffer.random.intRangeAtMost(u8, 0, self.spread * 2);

            // Select semi-random target cell
            const to = from -| self.terminal_buffer.width + self.spread -| rand_spread;
            const to_x = to % self.terminal_buffer.width;
            const to_y = to / self.terminal_buffer.width;

            // Get fire level of current cell
            const level_buf_from = self.buffer[from];

            // Choose new fire level and store in level buffer
            const level_buf_to = level_buf_from -| @intFromBool(rand_loss >= self.height);
            self.buffer[to] = level_buf_to;

            // Send known fire levels to terminal buffer
            const from_cell = self.fire[level_buf_from];
            const to_cell = self.fire[level_buf_to];
            from_cell.put(x, y);
            to_cell.put(to_x, to_y);
        }

        // Draw bottom line (fire source)
        const src_cell = self.fire[STEPS];
        src_cell.put(x, self.terminal_buffer.height - 1);
    }
}

fn initBuffer(buffer: []u8, width: usize) void {
    const length = buffer.len - width;
    const slice_start = buffer[0..length];
    const slice_end = buffer[length..];

    // Initialize the framebuffer in black, except for the "fire source" as the
    // last color
    @memset(slice_start, 0);
    @memset(slice_end, STEPS);
}

fn update(self: *Doom, _: *anyopaque) !void {
    const time = try interop.getTimeOfDay();

    if (self.timeout_sec > 0 and time.seconds - self.start_time.seconds > self.timeout_sec) {
        self.animate.* = false;
    }
}

fn calculateTimeout(self: *Doom, _: *anyopaque) !?usize {
    return self.frame_delay;
}
