const std    = @import("std");
const zm     = @import("zmath");
const enet   = @import("c.zig").enet;
// const bufzilla = @import("bufzilla");

const shared = @import("shared");

const chunk  = @import("chunk.zig");
const server = @import("server.zig");

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

const frame_dt_ns: i128 = std.time.ns_per_s / 10;

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

const log = std.log.scoped(.server);

pub fn main() !void {

    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, Server!\n");

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

    const address = enet.ENetAddress{ .host = enet.ENET_HOST_ANY, .port = shared.protocol_v1.SERVER_PORT };
    const host = enet.enet_host_create(&address, 32, 2, 0, 0);
    if (host == null) {
        @panic("host is null");
    }
    defer enet.enet_host_destroy(host);

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

    _ = try chunk.spawn(
        &the_chunk,
        allocator,
        host,
        shared.entity.Soldier,
        .{
            .hp = 10,
            .pos = zm.f32x4(5.0, 0, 0, 0),
            .vel = zm.f32x4(0,   0, 0, 0),
        }
    );

    var last_time: i128 = std.time.nanoTimestamp();
    var next_frame_deadline: i128 = last_time + frame_dt_ns;

    var accumulator: f32 = 0;

    while (true) {
        const now = std.time.nanoTimestamp();
        var delta_ns = now - last_time;
        last_time = now;

        if (delta_ns > std.time.ns_per_s / 4)
            delta_ns = std.time.ns_per_s / 4;

        const delta_s: f32 = @floatCast(@as(f64, @floatFromInt(delta_ns)) / 1e9);

        t += delta_s;
        accumulator += delta_s * k;
        
        log.info("Starting a the frame {} at time {}", .{frame, t});

        var event = enet.ENetEvent{};
        while (enet.enet_host_service(host, &event, 0) > 0) {
            switch (event.type) {
                enet.ENET_EVENT_TYPE_CONNECT => {
                    try on_connect(allocator, host, event.peer, &the_chunk);
                },
                enet.ENET_EVENT_TYPE_RECEIVE => {
                    log.info("receive", .{});
                },
                enet.ENET_EVENT_TYPE_DISCONNECT => {
                    log.info("disconnect", .{});
                },
                else => {},
            }
        }

        // const connection = try tcp_server.accept();
        // defer connection.stream.close();

        // var recv_buffer: [4096]u8 = undefined;
        // var reader = connection.stream.reader(&recv_buffer);

        // const line = try reader.interface().takeDelimiterExclusive('\n');
        // log.info("Received: {s}", .{line});

        // var send_buffer: [4096]u8 = undefined;
        // var writer = connection.stream.writer(&send_buffer);
        // const w = &writer.interface;
        // try shared.protocol.sendHello(w, .{ .msg = "Hello from server!" });

        // try chunk.spawn(&the_chunk, shared.entity.Monster, allocator, .{
        //     .pos = zm.f32x4(1, 1, 0, 0),
        //     .hp = 20,
        // }, w);

        // try chunk.spawn(&the_chunk, shared.entity.Projectile, allocator, .{
        //     .pos = zm.f32x4(0, 0, 0, 0),
        //     .vel = zm.f32x4(0.2, 0, 0, 0),
        // }, w);

        while (accumulator > dt * k) {
            the_chunk.update(dt * k);

            accumulator -= dt * k;
            gt += dt * k;
        }

        try chunk.broadcastChanges(&the_chunk, allocator, host);

        const spin_ns: i128 = 100_000; // 0.1 ms

        while (true) {
            const _t = std.time.nanoTimestamp();
            const remaining = next_frame_deadline - _t;

            if (remaining <= 0)
                break;

            if (remaining > spin_ns)
                std.Thread.sleep(@intCast(remaining - spin_ns));
        }

        next_frame_deadline += frame_dt_ns;

        frame += 1;
    }
}

fn on_connect(allocator: std.mem.Allocator, host: *enet.ENetHost, peer: *enet.ENetPeer, the_chunk: *shared.chunk.Chunk) !void {
    var aw = try std.ArrayList(u8).initCapacity(allocator, 128);
    defer aw.deinit(allocator);

    _ = peer;

    var writer = shared.bits.BitWriter.init(&aw);

    for (the_chunk.entities.items) |e| {
        const msg = shared.protocol_v1.makeSpawnMessage(e);
        try writer.serialize(allocator, msg);
    }

    const encoded = writer.written();
    
    const packet = enet.enet_packet_create(encoded.ptr, encoded.len, enet.ENET_PACKET_FLAG_RELIABLE);

    enet.enet_host_broadcast(host, 0, packet);
}

//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, .{});
// }
