const std = @import("std");
const r = @import("raylib.zig");
const tmx = @import("tmx.zig");
const assets = @import("assets.zig");
const Walrus = @import("walrus.zig");
const Funge = @import("funge.zig");

const Map = @This();

const allocator = std.heap.page_allocator;

pub const MapError = error{
    CannotLoadMap,
};

pub fn init(name: []const u8) !Map {
    const data = try assets.get(name);
    //std.debug.print("[Map] read {} bytes\n", .{data.len});
    defer assets.free(data);
    tmx.tmx_img_load_func = tmx.texLoad;
    tmx.tmx_img_free_func = tmx.texFree;
    const map = tmx.tmx_load_buffer(data.ptr, @as(c_int, @intCast(data.len)));
    if (map == null) {
        std.debug.print("[Map] error {}: {s}\n", .{ tmx.tmx_errno, tmx.tmx_strerr() });
        return MapError.CannotLoadMap;
    }
    const sw: f32 = 1920; //@floatFromInt(r.GetScreenWidth());
    var m = Map{
        .render = r.LoadRenderTexture(sw, 1080),
        .map = map,
        .font = try assets.getFont("Shantell_Sans-Informal_Regular.otf", 64),
        .dispfont = try assets.getBitmapFont(
            "cp863.png",
            r.Vector2{ .x = 16, .y = 16 },
            r.Vector2{ .x = 9, .y = 16 },
            0,
        ),
        .camera = r.Camera2D{
            .target = r.Vector2{ .x = -sw * 0.25, .y = 0 },
            .offset = r.Vector2{ .x = 0, .y = 0 },
            .rotation = 0.0,
            .zoom = 2.0,
        },
        .walrus = try Walrus.init("walrus.png"),
        .player = try Walrus.init("player.png"),
        .ui = try assets.getTexture("ui.qoi"),
        .funge = Funge.init(),
    };
    const width: u32 = m.map.*.width;
    const height: u32 = m.map.*.height;
    m.walrus.offset = r.Vector2{ .x = @floatFromInt(m.map.*.tile_width), .y = @floatFromInt(m.map.*.tile_height) };
    m.walrus.bounds = r.Rectangle{ .x = 0, .y = 0, .width = @floatFromInt(width * m.map.*.tile_width), .height = @floatFromInt(height * m.map.*.tile_height) };
    m.player.frames = 4;
    m.player.size = r.Rectangle{ .x = 0, .y = -20, .width = 32, .height = 48 };
    m.player.bounds = m.walrus.bounds;
    const level = m.getLayerFromClass("level");
    if (level != null) {
        if (level.*.type == tmx.L_LAYER) {
            var i: u32 = 0;
            while (i < height * width) : (i += 1) {
                const ti = i / width;
                const tj = i % width;
                const gid = level.*.content.gids[i] & tmx.TMX_FLIP_BITS_REMOVAL;
                if (m.map.*.tiles[gid] != null) {
                    const t = m.map.*.tiles[gid].*;
                    if (t.id < 4) {
                        const ts = t.tileset.*;
                        const w: f32 = @floatFromInt(ts.tile_width);
                        const h: f32 = @floatFromInt(ts.tile_height);
                        const fi = @as(f32, @floatFromInt(ti)) * h;
                        const fj = @as(f32, @floatFromInt(tj)) * w;
                        m.walrus.spawn(r.Vector2{ .x = fj, .y = fi }, @enumFromInt(t.id));
                        m.funge.direction = m.walrus.dir;
                    }
                }
            }
        }
    }
    return m;
}

render: r.RenderTexture2D,
map: [*c]tmx.tmx_map,
camera: r.Camera2D,
font: r.Font,
dispfont: r.Font,
walrus: Walrus,
player: Walrus,
ui: r.Texture2D,
funge: Funge,

pub fn deinit(self: *@This()) void {
    tmx.tmx_map_free(self.map);
    r.UnloadFont(self.font);
    //r.UnloadFont(self.dispfont);
    r.UnloadTexture(self.ui);
    self.walrus.deinit();
    self.funge.deinit();
    r.UnloadRenderTexture(self.render);
}

pub fn update(self: *@This()) void {
    const pos = r.GetMousePosition();
    const sw: f32 = @floatFromInt(self.render.texture.width); //@floatFromInt(r.GetScreenWidth());
    const sh: f32 = @floatFromInt(self.render.texture.height); //sw * 9.0 / 16.0; //@floatFromInt(r.GetScreenHeight());
    if (r.IsMouseButtonDown(0) and pos.x / sw > 0.2 and pos.y / sh < 1) {
        const d = r.GetMouseDelta();
        self.camera.target.x -= d.x;
        self.camera.target.y -= d.y;
    }
    const width: u32 = self.map.*.width;
    const height: u32 = self.map.*.height;
    const tw: u32 = self.map.*.tile_width;
    const th: u32 = self.map.*.tile_height;
    const pw: f32 = @floatFromInt(width * tw / 2);
    const ph: f32 = @floatFromInt(height * th / 2);
    if (self.camera.target.x > pw - sw) self.camera.target.x = pw - sw;
    if (self.camera.target.x < -0.1 * sw) self.camera.target.x = -0.1 * sw;
    if (self.camera.target.y > ph - sh) self.camera.target.y = ph - sh;
    if (self.camera.target.y < 0) self.camera.target.y = 0;
    if (!self.player.isMoving) {
        if (r.IsKeyDown(r.KEY_LEFT)) {
            self.player.moveDir(.West);
        } else if (r.IsKeyDown(r.KEY_UP)) {
            self.player.moveDir(.North);
        } else if (r.IsKeyDown(r.KEY_RIGHT)) {
            self.player.moveDir(.East);
        } else if (r.IsKeyDown(r.KEY_DOWN)) {
            self.player.moveDir(.South);
        }
    }
    if (!self.walrus.isMoving) {
        const level = self.getLayerFromClass("level");
        if (level != null) {
            if (level.*.type == tmx.L_LAYER) {
                const x: u32 = @intFromFloat(self.walrus.pos.x / self.walrus.offset.x);
                const y: u32 = @intFromFloat(self.walrus.pos.y / self.walrus.offset.y);
                const i: u32 = y * width + x;
                const gid = level.*.content.gids[i] & tmx.TMX_FLIP_BITS_REMOVAL;
                var result = Funge.Result{ .action = .Move, .teleport = undefined };
                if (self.map.*.tiles[gid] != null) {
                    const opcode: Funge.Opcode = @enumFromInt(self.map.*.tiles[gid].*.id);
                    result = self.funge.step(opcode) catch Funge.Result{ .action = .Stop, .teleport = undefined };
                }
                self.walrus.dir = self.funge.direction;
                switch (result.action) {
                    .Stop, .End => {},
                    .Move => {
                        self.walrus.move();
                    },
                    .Teleport => {
                        self.walrus.moveRel(result.teleport);
                    },
                }
            }
        }
    }
    self.player.update();
    self.walrus.update();
}

pub fn draw(self: *@This()) void {
    r.BeginTextureMode(self.render);
    defer r.EndTextureMode();

    r.ClearBackground(r.getTmxColor(self.map.*.backgroundcolor));
    r.BeginMode2D(self.camera);
    self.drawAllLayers(self.map.*.ly_head);
    r.EndMode2D();
    const sh: f32 = @floatFromInt(self.render.texture.height); //@floatFromInt(r.GetScreenHeight());
    const th: f32 = @floatFromInt(self.ui.height);
    r.DrawTextureEx(self.ui, .{ .x = 0, .y = 0 }, 0, sh / th, r.WHITE);
    const title = tmx.getProperty(self.map.*.properties, "name");
    if (title != .none) {
        r.DrawTextCenteredEx(
            self.font,
            title.string.ptr,
            r.Vector2{ .x = 192, .y = 48 },
            48,
            0,
            r.WHITE,
        );
    }
    r.DrawTextCenteredEx(
        self.font,
        "Restart",
        r.Vector2{ .x = 192, .y = 144 },
        48,
        0,
        r.WHITE,
    );
    if (self.funge.stack.items.len > 0) {
        var i = self.funge.stack.items.len;
        var j: usize = 0;
        while (i > 0 and j < 8) : (i -= 1) {
            const text = std.fmt.allocPrint(allocator, "{: >3}", .{self.funge.stack.items[i - 1]}) catch "0";
            defer allocator.free(text);
            r.DrawTextEx(
                self.dispfont,
                text.ptr,
                r.Vector2{ .x = 16, .y = 296 - @as(f32, @floatFromInt(i)) * 64 + 64 * @as(f32, @floatFromInt(self.funge.stack.items.len)) },
                64,
                0,
                r.BLACK,
            );
            j += 1;
        }
    }
    const str = self.funge.display.items.ptr;
    r.DrawTextCodepoints(
        self.dispfont,
        str,
        @intCast(self.funge.display.items.len),
        r.Vector2{ .x = 16, .y = 810 },
        64,
        0,
        r.BLACK,
    );
}

fn getLayerFromClass(self: *@This(), name: []const u8) [*c]tmx.tmx_layer {
    return self.getSublayerFromClass(name, self.map.*.ly_head);
}

fn getSublayerFromClass(self: *@This(), name: []const u8, layer: [*c]tmx.tmx_layer) [*c]tmx.tmx_layer {
    var l = layer;
    while (l != null) : (l = l.*.next) {
        if (l.*.type == tmx.L_GROUP) {
            const ll = self.getSublayerFromClass(name, l.*.content.group_head);
            if (ll != null) return ll;
        }
        if (l.*.class_type != null) {
            var split = std.mem.splitSequence(u8, std.mem.span(l.*.class_type), ",");
            while (split.next()) |str| {
                if (std.mem.eql(u8, str, name)) {
                    return l;
                }
            }
        }
    }
    return null;
}

fn drawAllLayers(self: *@This(), layer: [*c]tmx.tmx_layer) void {
    var l = layer;
    while (l != null) : (l = l.*.next) {
        if (l.*.visible != 0) {
            const op = @as(u8, @intFromFloat(l.*.opacity * 255));
            const opc = r.Color{ .r = op, .g = op, .b = op, .a = op };
            const origin = r.Vector2{
                .x = @floatFromInt(l.*.offsetx),
                .y = @floatFromInt(l.*.offsety),
            };
            switch (l.*.type) {
                tmx.L_NONE => {},
                tmx.L_GROUP => {
                    self.drawAllLayers(l.*.content.group_head);
                },
                tmx.L_OBJGR => {
                    const objgr = l.*.content.objgr.*;
                    var head = objgr.head;
                    const color = r.getTmxColor(objgr.color);
                    while (head != null) : (head = head.*.next) {
                        const h = head.*;
                        if (h.visible != 0) {
                            switch (h.obj_type) {
                                tmx.OT_NONE => {},
                                tmx.OT_SQUARE => {
                                    r.DrawRectanglePro(
                                        r.Rectangle{
                                            .x = @floatCast(h.x),
                                            .y = @floatCast(h.y),
                                            .width = @floatCast(h.width),
                                            .height = @floatCast(h.height),
                                        },
                                        r.Vector2{ .x = 0, .y = 0 },
                                        @floatCast(h.rotation),
                                        color,
                                    );
                                },
                                tmx.OT_POLYGON => {
                                    // TODO
                                },
                                tmx.OT_POLYLINE => {
                                    // TODO
                                },
                                tmx.OT_ELLIPSE => {
                                    r.DrawEllipse(
                                        @intFromFloat(h.x + h.width / 2.0),
                                        @intFromFloat(h.y + h.height / 2.0),
                                        @floatCast(h.width / 2.0),
                                        @floatCast(h.height / 2.0),
                                        color,
                                    );
                                },
                                tmx.OT_TILE => {
                                    self.drawTile(
                                        @intCast(h.content.gid),
                                        @intFromFloat(h.y),
                                        @intFromFloat(h.x),
                                        op,
                                        false,
                                    );
                                },
                                tmx.OT_TEXT => {
                                    const text = h.content.text.*;
                                    r.DrawTextPro(
                                        self.font,
                                        text.text,
                                        r.Vector2{ .x = @floatCast(h.x), .y = @floatCast(h.y) },
                                        origin,
                                        @floatCast(h.rotation),
                                        @floatFromInt(text.pixelsize),
                                        @floatFromInt(text.kerning),
                                        r.getTmxColor(text.color),
                                    );
                                },
                                tmx.OT_POINT => {},
                                else => unreachable,
                            }
                        }
                    }
                },
                tmx.L_IMAGE => {
                    const tex = @as(*r.Texture2D, @alignCast(@ptrCast(l.*.content.image.*.resource_image)));
                    r.DrawTexture(tex.*, l.*.offsetx, l.*.offsety, opc);
                },
                tmx.L_LAYER => {
                    var i: u32 = 0;
                    const width: u32 = self.map.*.width;
                    const height: u32 = self.map.*.height;
                    while (i < height * width) : (i += 1) {
                        const ti = i / width;
                        const tj = i % width;
                        const gid = l.*.content.gids[i] & tmx.TMX_FLIP_BITS_REMOVAL;
                        self.drawTile(gid, ti, tj, op, true);
                    }
                },
                else => unreachable,
            }
            if (l.*.class_type != null) {
                var split = std.mem.splitSequence(u8, std.mem.span(l.*.class_type), ",");
                while (split.next()) |str| {
                    if (std.mem.eql(u8, str, "walrus")) {
                        self.walrus.draw();
                    }
                    if (std.mem.eql(u8, str, "player")) {
                        self.player.draw();
                    }
                }
            }
        }
    }
}

fn drawTile(self: *@This(), gid: u32, i: u32, j: u32, op: u8, grid: bool) void {
    if (self.map.*.tiles[gid] != null) {
        const t = self.map.*.tiles[gid].*;
        const ts = t.tileset.*;
        const x: f32 = @floatFromInt(t.ul_x);
        const y: f32 = @floatFromInt(t.ul_y);
        const w: f32 = @floatFromInt(ts.tile_width);
        const h: f32 = @floatFromInt(ts.tile_height);
        const fi = @as(f32, @floatFromInt(i)) * if (grid) h else 1;
        const fj = @as(f32, @floatFromInt(j)) * if (grid) w else 1;
        var image: *r.Texture2D = undefined;
        if (t.image != null) {
            image = @as(*r.Texture2D, @alignCast(@ptrCast(t.image.*.resource_image)));
        } else {
            image = @as(*r.Texture2D, @alignCast(@ptrCast(ts.image.*.resource_image)));
        }
        r.DrawTextureRec(
            image.*,
            r.Rectangle{ .x = x, .y = y, .width = w, .height = h },
            r.Vector2{ .x = fj, .y = fi },
            r.Color{ .r = op, .g = op, .b = op, .a = op },
        );
    }
}
