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

var btn: ff.Buttons = undefined;
var pad: ff.Pad = undefined;

const pal: ff.Palette = .{
    .black = 0x020403,
    .dark_gray = 0x050A08,
    .purple = 0x06100D,
    .dark_blue = 0x07130F,
    .red = 0x081811,
    .orange = 0x0A2217,
    .dark_green = 0x0D2E1C,
    .green = 0x12462A,
    .light_green = 0x18663C,
    .yellow = 0x1F854A,
    .light_blue = 0x25985A,
    .cyan = 0x2AAC66,
    .blue = 0x2FB873,
    .gray = 0x34C27A,
    .light_gray = 0x3ACF82,
    .white = 0x42DD8A,
};

pub export fn boot() void {
    pal.set();
}

pub export fn update() void {
    const me = ff.getMe();

    btn = ff.readButtons(me);
    pad = ff.readPad(me).?;

    engine.update();
}

pub export fn render() void {
    engine.render();
}

const Engine = struct {
    px: f32 = 3.5,
    py: f32 = 3.5,
    angle: f32 = 0,

    fn update(self: *Engine) void {
        const rs: f32 = 0.04;
        const ms: f32 = 0.06;
        const pu: f32 = 0.01;
        const px: f32 = @floatFromInt(pad.x);
        const py: f32 = @floatFromInt(pad.y);

        const len2 = px * px + py * py;

        if (len2 > 2500.0) {
            const inv_len = std.math.sqrt(len2);
            self.angle += (px / inv_len) * rs;
        }

        const ca = std.math.cos(self.angle);
        const sa = std.math.sin(self.angle);

        const fx = ca;
        const fy = sa;

        const rx = -sa;
        const ry = ca;

        const fwd = @as(f32, @intFromBool(btn.n)) - @as(f32, @intFromBool(btn.s));
        const str = @as(f32, @intFromBool(btn.e)) - @as(f32, @intFromBool(btn.w));

        const mx = (fx * fwd + rx * str) * ms;
        const my = (fy * fwd + ry * str) * ms;

        const try_x = self.px + mx;

        if (!mapAt(try_x, self.py)) {
            self.px = try_x;
        } else if (mx != 0) {
            self.px += if (mx > 0) -pu else pu;
        }

        const try_y = self.py + my;

        if (!mapAt(self.px, try_y)) {
            self.py = try_y;
        } else if (my != 0) {
            self.py += if (my > 0) -pu else pu;
        }
    }

    fn render(self: *Engine) void {
        const w: f32 = @floatFromInt(ff.width);
        const h: f32 = @floatFromInt(ff.height);

        const hh: i32 = @intFromFloat(h * 0.5);
        const iw: f32 = 1.0 / w;

        ff.draw.rect(0, 0, ff.width, ff.height / 2, .{
            .fill_color = .dark_gray,
        });

        ff.draw.rect(0, ff.height / 2, ff.width, ff.height / 2, .{
            .fill_color = .dark_blue,
        });

        const px = self.px;
        const py = self.py;

        const fov: f32 = 0.8;
        const imd: f32 = 1.0 / 12.0;

        const dir_x = std.math.cos(self.angle);
        const dir_y = std.math.sin(self.angle);

        const plane_x = -dir_y * fov;
        const plane_y = dir_x * fov;

        var x: i32 = 0;
        while (x < ff.width) : (x += 2) {
            const camera_x: f32 = (2.0 * @as(f32, @floatFromInt(x)) * iw) - 1.0;
            const rdx = dir_x + plane_x * camera_x;
            const rdy = dir_y + plane_y * camera_x;

            var map_x: i32 = @intFromFloat(px);
            var map_y: i32 = @intFromFloat(py);

            const dx = @abs(1.0 / rdx);
            const dy = @abs(1.0 / rdy);

            var step_x: i32 = 0;
            var step_y: i32 = 0;

            var sdx: f32 = 0;
            var sdy: f32 = 0;

            if (rdx < 0) {
                step_x = -1;
                sdx = (px - @as(f32, @floatFromInt(map_x))) * dx;
            } else {
                step_x = 1;
                sdx = (@as(f32, @floatFromInt(map_x)) + 1.0 - px) * dx;
            }

            if (rdy < 0) {
                step_y = -1;
                sdy = (py - @as(f32, @floatFromInt(map_y))) * dy;
            } else {
                step_y = 1;
                sdy = (@as(f32, @floatFromInt(map_y)) + 1.0 - py) * dy;
            }

            var side: i32 = 0;

            while (true) {
                if (sdx < sdy) {
                    sdx += dx;
                    map_x += step_x;
                    side = 0;
                } else {
                    sdy += dy;
                    map_y += step_y;
                    side = 1;
                }

                if (world_map[@intCast(map_y)][@intCast(map_x)] == 1)
                    break;
            }

            var dist: f32 =
                if (side == 0)
                    sdx - dx
                else
                    sdy - dy;

            if (dist < 0.001) dist = 0.001;

            const lh: i32 = @intFromFloat(h / dist);
            const start = hh - @divTrunc(lh, 2);
            const end = hh + @divTrunc(lh, 2);

            var shade_i: u8 = @intFromFloat(dist * imd * 6.0);
            if (shade_i > 5) shade_i = 5;

            const base: ff.Color = switch (shade_i) {
                0 => .white,
                1 => .light_green,
                2 => .green,
                3 => .dark_green,
                4 => .dark_gray,
                else => .black,
            };

            const top: ff.Color = switch (base) {
                .black => .dark_gray,
                .dark_gray => .green,
                .dark_green => .light_green,
                .green => .cyan,
                .light_green => .white,
                else => .white,
            };

            const mid: ff.Color = base;

            const bot: ff.Color = switch (base) {
                .white => .light_green,
                .cyan => .green,
                .light_green => .dark_green,
                .green => .dark_gray,
                .dark_green => .black,
                else => .black,
            };

            const lh_f: i32 = end - start;
            const strip: i32 = @max(1, @divTrunc(lh_f, 6));
            const ms = start + strip;
            const me = end - strip;

            ff.draw.rect(x, start, 2, strip, .{ .fill_color = top });
            ff.draw.rect(x, ms, 1, me - ms, .{ .fill_color = mid });
            ff.draw.rect(x, me, 2, strip, .{ .fill_color = bot });
        }
    }
};

var engine = Engine{};

const MAP_W = 32;
const MAP_H = 32;

fn mapAt(x: f32, y: f32) bool {
    const ix = @as(i32, @intFromFloat(x));
    const iy = @as(i32, @intFromFloat(y));

    if (ix < 0 or iy < 0) return true;
    if (iy >= MAP_H or ix >= MAP_W) return true;

    return world_map[@intCast(iy)][@intCast(ix)] != 0;
}

pub const world_map = [MAP_H][MAP_W]u8{
    .{ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 },
    .{ 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1 },
    .{ 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1 },
    .{ 1, 1, 1, 0, 0, 1, 1, 1, 0, 0, 1, 1, 1, 0, 0, 1, 1, 1, 0, 0, 1, 1, 1, 0, 0, 1, 1, 1, 0, 0, 1, 1 },
    .{ 1, 1, 1, 0, 0, 1, 1, 1, 0, 0, 1, 1, 1, 0, 0, 1, 1, 1, 0, 0, 1, 1, 1, 0, 0, 1, 1, 1, 0, 0, 1, 1 },
    .{ 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1 },
    .{ 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1 },
    .{ 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1 },
    .{ 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1 },
    .{ 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 },
    .{ 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 },
    .{ 1, 1, 1, 0, 0, 1, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 1, 0, 0, 1, 1, 1 },
    .{ 1, 1, 1, 0, 0, 1, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 1, 0, 0, 1, 1, 1 },
    .{ 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1 },
    .{ 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1 },
    .{ 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 1 },
    .{ 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 1 },
    .{ 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 },
    .{ 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 },
    .{ 1, 1, 1, 0, 0, 1, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 1, 0, 0, 1, 1, 1 },
    .{ 1, 1, 1, 0, 0, 1, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 1, 0, 0, 1, 1, 1 },
    .{ 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1 },
    .{ 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1 },
    .{ 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1 },
    .{ 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1 },
    .{ 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 },
    .{ 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 },
    .{ 1, 1, 1, 0, 0, 1, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 1, 0, 0, 1, 1, 1 },
    .{ 1, 1, 1, 0, 0, 1, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 1, 0, 0, 1, 1, 1 },
    .{ 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1 },
    .{ 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1 },
    .{ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 },
};
