const std      = @import("std");
const zm       = @import("zmath");
// const bufzilla = @import("bufzilla");
const ft       = @import("freetype");
const tracy    = @import("tracy");
const kb       = @import("kb");

const shared   = @import("shared");

const c        = @import("c.zig").c;
const enet     = @import("c.zig").enet;
const rl       = @import("c.zig").rl;
const client   = @import("client.zig");
const entity   = @import("entity.zig");
const misc     = @import("misc.zig");
const font     = @import("font.zig");
const chunk    = @import("chunk.zig");

const dt          : f32 = 1.0 / 240.0;
var   t           : f32 = 0;
var   gt          : f32 = 0;
var   accumulator : f32 = 0;
var   k           : f32 = 1;
var   frame       : i32 = 0;

const screen_width = 1280;
const screen_height = 720;

var running: bool = true;

var dbg_allocator = std.heap.DebugAllocator(.{}){};

var the_chunk : shared.chunk.Chunk = undefined;

pub fn main() !void {
    const tracy_zone = tracy.ZoneNC(@src(), "main", 0x00_ff_00_00);
    defer tracy_zone.End();

    const allocator = dbg_allocator.allocator();
    defer {
        const deinit_status = dbg_allocator.deinit();
        if (deinit_status == .leak) {
            @panic("LEAKED!");
        }
    }

    var stdout = std.fs.File.stdout();

    _ = try stdout.write("Hello, Client!\n");

    if (enet.enet_initialize() != 0) {
        std.log.info("Failed to load ENet", .{});
        return;
    }

    defer enet.enet_deinitialize();

    rl.SetConfigFlags(rl.FLAG_WINDOW_HIGHDPI);
    rl.InitWindow(screen_width, screen_height, "zzz");
    defer rl.CloseWindow();

    font.ft_lib = try ft.Library.init();

    const sizes = [_]i32{
        30,
    };

    var vollkorn: [sizes.len]font.Font = undefined;

    for (sizes, 0..) |pt, i| {
        vollkorn[i] = try font.Font.init(
            "assets/fonts/Vollkorn/static/Vollkorn-Regular.ttf",
            pt,
            allocator
        );
    }

    defer {
        for (&vollkorn) |*f| f.deinit(allocator);
    }

    font.shader = rl.LoadShader(null, "assets/text.frag");
    // const test_shader1 = try rl.LoadShader(null, "assets/test_1.frag");
    // const test_shader2 = try rl.LoadShader(null, "assets/test_2.frag");

    const host = enet.enet_host_create(null, 32, 2, 0, 0);
    defer enet.enet_host_destroy(host);

    var address = enet.ENetAddress{ .port = shared.protocol_v1.SERVER_PORT };
     _ = enet.enet_address_set_host(&address, "localhost");

    const peer = enet.enet_host_connect(host, &address, 2, 0);
    defer enet.enet_peer_reset(peer);

    the_chunk = try shared.chunk.Chunk.init(allocator);
    defer the_chunk.deinit(allocator);

    // const camera = rl.Camera{
                    //     .fovy = 45,
                    //     .position = .{ .x = 0, .y = 0, .z = 10 },
                    //     .projection = .perspective,
                    //     .target = .{ .x = 0, .y = 0, .z = 0 },
                    //     .up = .{ .x = 0, .y = 1, .z = 0 }
                    // };

    // rl.initAudioDevice();

    // if (rl.isAudioDeviceReady()) {
    //     std.log.info("audio device is ready!", .{});
    // } else {
    //     std.log.info("audio device is NOT ready!", .{});
    // }

    // const music = try rl.loadMusicStream("assets/romantic-piano-431010.mp3");
    // std.log.info("{}", .{rl.isMusicValid(music)});
    // rl.playMusicStream(music);
    // std.log.info("is music playing? {}", .{rl.isMusicStreamPlaying(music)});

    var old_time : f32 = @floatCast(rl.GetTime());

    while (!rl.WindowShouldClose()) {
        const tracy_main_zone = tracy.ZoneNC(@src(), "loop", 0x00_aa_33_10);
        defer tracy_main_zone.End();

        const new_time : f32 = @floatCast(rl.GetTime());
        var frame_time = new_time - old_time;
        old_time = new_time;

        if (frame_time > 0.25)
            frame_time = 0.25;

        t += frame_time;
        accumulator += frame_time * k;


        var event = enet.ENetEvent{};
        while (enet.enet_host_service(host, &event, 0) > 0) {
            switch (event.type) {
                enet.ENET_EVENT_TYPE_CONNECT => {
                    std.log.info("A new client connected from {d}:{d}.", .{
                        event.peer.*.address.host,
                        event.peer.*.address.port
                    });

                    event.peer.*.data = @constCast(@ptrCast("Client information"));
                },
                enet.ENET_EVENT_TYPE_RECEIVE => {
                    try on_packet(allocator, event.packet, event.peer, event.channelID);

                    enet.enet_packet_destroy(event.packet);
                },
                enet.ENET_EVENT_TYPE_DISCONNECT => {
                    std.log.info("disconnect", .{});
                },
                else => {}
            }
        }

        rl.PollInputEvents();

        // if (peer.state() == .connected) {
        //     const packet = try znet.Packet.init("Hello, Server!", 0, .reliable);
        //     try peer.send(packet);
        // }
        // // _ = peer;

        // rl.updateMusicStream(music);

        while (accumulator > dt * k) {
            the_chunk.update(dt * k);
            accumulator -= dt * k;
            gt += dt * k;
        }

        const drawing_zone = tracy.ZoneNC(@src(), "Drawing", 0x00_ff_00_00);

        rl.BeginDrawing();
        rl.ClearBackground(rl.SKYBLUE);

        // const lorem = @embedFile("embeds/lorem.txt");

        vollkorn[0].render_text(
            "The night is long and cold outside.",
            rl.Vector2{ .x = 0, .y = 0 },
            true,
            rl.WHITE,
            rl.BLANK,
            false,
            true
        );

        // rl.beginShaderMode(test_shader1);
        // rl.gl.rlBegin(rl.gl.rl_quads);

        // {
        //     const topLeft     : rl.Vector2 = .{
                    // .x = 0.0,
                    // .y = 0.0
                // };
        //     const bottomRight : rl.Vector2 = .{
            //     .x = @as(f32, @floatFromInt(rl.getScreenWidth())) / 2.0,
            //     .y = @as(f32, @floatFromInt(rl.getScreenHeight()))
            // };

        //     rl.gl.rlVertex2f(topLeft.x, topLeft.y);
        //     rl.gl.rlVertex2f(topLeft.x, bottomRight.y);
        //     rl.gl.rlVertex2f(bottomRight.x, bottomRight.y);
        //     rl.gl.rlVertex2f(bottomRight.x, topLeft.y);

        // }

        // rl.gl.rlEnd();
        // rl.endShaderMode();

        // rl.beginShaderMode(test_shader2);
        // rl.gl.rlBegin(rl.gl.rl_quads);

        // {
        //     const topLeft     : rl.Vector2 = .{
            //     .x = @as(f32, @floatFromInt(rl.getScreenWidth())) / 2.0,
            //     .y = 0
            // };
        //     const bottomRight : rl.Vector2 = .{
                //     .x = @floatFromInt(rl.getScreenWidth()),
                //     .y = @floatFromInt(rl.getScreenHeight())
                // };

        //     rl.gl.rlVertex2f(topLeft.x, topLeft.y);
        //     rl.gl.rlVertex2f(topLeft.x, bottomRight.y);
        //     rl.gl.rlVertex2f(bottomRight.x, bottomRight.y);
        //     rl.gl.rlVertex2f(bottomRight.x, topLeft.y);
        // }

        // rl.gl.rlEnd();
        // rl.endShaderMode();

        // rl.beginShaderMode(test_shader2);
        // rl.gl.rlBegin(rl.gl.rl_quads);

        // {
        //     const topLeft     : rl.Vector2 = .{ .x = @as(f32, @floatFromInt(rl.getScreenWidth())) / 2.0, .y = 0 };
        //     const bottomRight : rl.Vector2 = .{
                //     .x = @floatFromInt(rl.getScreenWidth()),
                //     .y = @floatFromInt(rl.getScreenHeight())
                // };

        //     rl.gl.rlVertex2f(topLeft.x, topLeft.y);
        //     rl.gl.rlVertex2f(topLeft.x, bottomRight.y);
        //     rl.gl.rlVertex2f(bottomRight.x, bottomRight.y);
        //     rl.gl.rlVertex2f(bottomRight.x, topLeft.y);
        // }

        // rl.gl.rlEnd();
        // rl.endShaderMode();

        // rl.drawRectangle(400, 0, 400, 450, rl.Color{ .r = 54, .g = 54, .b = 54, .a = 255 });


        // f.texture.drawPro(
        //     .{.x = 0, .y = 0, .width = 4096, .height = 4096},
        //     .{.x = 0, .y = 0, .width = 512, .height = 512 },
        //     .zero(), 0, .white);

        // const connected_text = "Connected";
        //const not_connected_text = "Not Connected";

        // switch (peer.state()) {
        //     .connected => rl.drawText(
                            //     connected_text,
                            //     @divFloor(rl.getScreenWidth(), 2) - @divFloor(rl.measureText(connected_text, f.baseSize), 2),
                            //     50,
                            //     20,
                            //     .white
                            // ),
        //     else       => rl.drawText(
                            //     not_connected_text,
                            //     @divFloor(rl.getScreenWidth(), 2) - @divFloor(rl.measureText(not_connected_text, f.baseSize), 1),
                            //     50,
                            //     20,
                            //     .white
                            // ),
        // }
        //@divFloor(rl.getScreenWidth(), 2) - @divFloor(rl.measureText(connected_text, f.baseSize), 2), 50




        // const font_size : i32 = 180;
        // const text_size = rl.Vector2{.x = @floatFromInt(rl.measureText(connected_text, font_size)),
        //                              .y = font_size};//rl.measureTextEx(try rl.getFontDefault(), connected_text, font_size, font_size / 10);
        // const pos = rl.Vector2{.x = 0, .y = 0};
        // rl.drawText(connected_text, pos.x, pos.y, font_size, .white);
        // rl.drawRectangleLines(pos.x, pos.y, @intFromFloat(text_size.x), @intFromFloat(text_size.y), .red);
        // rl.drawRectangle(pos.x, pos.y, rl.getScreenWidth(), rl.getScreenHeight(), .white);
        // rl.drawTexturePro(tx,
        //                   rl.Rectangle{.x = 0, .y = 0, .width = 32, .height = 32},
        //                   rl.Rectangle{.x = 100, .y = 100, .width = 500, .height = 500},
        //                   .{.x = 0, .y = 0},
        //                   0,
        //                   .white);

        //rl.drawLineV(.{.x = @floatFromInt(@divFloor(rl.getScreenWidth(), 2)), .y = 0}, .{.x = @floatFromInt(@divFloor(rl.getScreenWidth(), 2)),
        //               .y = @floatFromInt(rl.getScreenHeight())}, .red);

        //rl.drawText("Congrats! You created your first window!", rl.getMouseX(), rl.getMouseY(), 20, .white);
        //rl.drawRectangleLines(0, 0, 100, 100, .red);
        misc.drawFPS(0, 0, frame_time);

        //elf.draw();

        // rl.beginMode3D(camera);
        // rl.drawSphere(.{ .x = 0, .y = 0, .z = 0 }, 1, .red);
        // rl.endMode3D();

        rl.EndDrawing();
        drawing_zone.End();

        const swap_zone = tracy.ZoneNC(@src(), "SwapScreenBuffer", 0x00_ff_00_00);
        rl.SwapScreenBuffer();
        swap_zone.End();

        frame += 1;

        tracy.FrameMark();
    }
}

fn on_packet(allocator: std.mem.Allocator, packet: *enet.ENetPacket, peer: *enet.ENetPeer, channelID: i32) !void {
    // std.log.info("A packet of length {d} containing {s} was received from {s} on channel {d}.", .{
    //                 packet.*.dataLength,
    //                 packet.*.data,
    //                 @as([*:0]const u8, @ptrCast(peer.*.data.?)),
    //                 channelID});

    _ = peer;
    _ = channelID;

    const bytes: []const u8 = packet.*.data[0 .. packet.*.dataLength];

    var reader = shared.bits.BitReader.init(bytes);

    const msg : shared.protocol_v1.Message = try reader.deserialize(allocator, shared.protocol_v1.Message);

    try on_message(allocator, msg);
}

fn on_message(allocator: std.mem.Allocator, msg: shared.protocol_v1.Message) !void {
    switch (msg) {
        .spawn_entity   => |se| { try on_spawn_entity(allocator, se); },
        .update_entity  => |ue| { on_update_entity(ue); },
        .despawn_entity => |de| { on_despawn_entity(allocator, de); },
    }
}

fn on_spawn_entity(allocator: std.mem.Allocator, se: shared.protocol_v1.SpawnEntity) !void {
    // _ = allocator; _ = se;
    switch (se) {
        .soldier => |s| {
            const soldier = shared.entity.Soldier{
                .id = s.id,
                .hp = s.hp,
                .pos = s.pos,
                .vel = s.vel,
            };
             _ = try chunk.spawn(&the_chunk, allocator, shared.entity.Soldier, soldier);
        },
        .alien => |a| {
            const alien = shared.entity.Alien{
                .id = a.id,
                .hp = a.hp,
                .pos = a.pos,
            };
            _ = try chunk.spawn(&the_chunk, allocator, shared.entity.Alien, alien);
        },
    }
}

fn on_update_entity(ue: shared.protocol_v1.UpdateEntity) void {
    switch (ue) {
        .soldier => |s| {
            if (chunk.findT(&the_chunk, shared.entity.Soldier, s.id)) |soldier| {
                soldier.hp = s.hp;
                soldier.pos = s.pos;
                soldier.vel = s.vel;
            }
        },
        .alien => |a| {
            if (chunk.findT(&the_chunk, shared.entity.Alien, a.id)) |alien| {
                alien.hp = a.hp;
                alien.pos = a.pos;
            }
        },
    }
}

fn on_despawn_entity(allocator: std.mem.Allocator, de: shared.protocol_v1.DespawnEntity) void {
    switch (de) {
        .soldier => |s| { _ = chunk.despawn(&the_chunk, allocator, s.id); },
        .alien   => |a| { _ = chunk.despawn(&the_chunk, allocator, a.id); },
    }
}

fn connect() !void {
    // const address = try std.net.Address.parseIp4("127.0.0.1", shared.protocol_v1.SERVER_PORT);

    // connection = try std.net.tcpConnectToAddress(address);

    // std.log.info("Connected to server", .{});
}

//fn handle_connection(connection: std.net.Server.Connection) !void {}

// test "simple test" {
//     const gpa = std.testing.allocator;
//     var list: std.ArrayList(i32) = .empty;
//     defer list.deinit(gpa); // Try commenting this out and see if zig detects the memory leak!
//     try list.append(gpa, 42);
//     try std.testing.expectEqual(@as(i32, 42), list.pop());
// }

// test "fuzz example" {
//     const Context = struct {
//         fn testOne(context: @This(), input: []const u8) anyerror!void {
//             _ = context;
//             // Try passing `--fuzz` to `zig build test` and see if it manages to fail this test case!
//             try std.testing.expect(!std.mem.eql(u8, "canyoufindme", input));
//         }
//     };
//     try std.testing.fuzz(Context{}, Context.testOne, .{});
// }
