// zig fmt: off
const std = @import("std");
const Api = @import("artifacts-api");
const SimClock = @import("./sim_clock.zig");
const Server = @This();

const max_level = 40;

// https://docs.artifactsmmo.com/concepts/skills#experience-to-level
const max_xp_per_level = [max_level]u64{
    150, // level 1
    250,
    350,
    450,
    700,
    950,
    1200,
    1450,
    1700,
    2100, // level 10
    2500,
    2900,
    3300,
    3700,
    4400,
    5100,
    5800,
    6500,
    7200,
    8200, // level 20
    9200,
    10200,
    11200,
    12200,
    13400,
    14600,
    15800,
    17000,
    18200,
    19700, // level 30
    21200,
    22700,
    24200,
    25700,
    27200,
    28700,
    30500,
    32300,
    34100,
    35900, // level 40
};


store: *Api.Store,
clock: SimClock = .{},
rng: std.Random.DefaultPrng,

pub fn init(seed: u64, store: *Api.Store) Server {
    return Server{
        .rng = std.Random.DefaultPrng.init(seed),
        .store = store
    };
}

fn sleepNorm(self: *Server, stddev_ns: u64, mean_ns: u64) void {
    const stddev_ns_f64: f64 = @floatFromInt(stddev_ns);
    const mean_ns_f64: f64 = @floatFromInt(mean_ns);

    var rng = self.rng.random();
    const duration = rng.floatNorm(f64) * stddev_ns_f64 + mean_ns_f64;
    self.clock.sleep(@intFromFloat(duration));
}

fn sleepRequestBegin(self: *Server) void {
    self.sleepNorm(
        100 * std.time.ns_per_ms,
        350 * std.time.ns_per_ms,
    );
}

fn sleepRequestEnd(self: *Server) void {
    self.sleepNorm(
        10  * std.time.ns_per_ms,
        300 * std.time.ns_per_ms,
    );
}

pub fn move(self: *Server, character_name: []const u8, position: Api.Position) Api.MoveError!Api.MoveResult {
    self.sleepRequestBegin();
    defer self.sleepRequestEnd();

    const map: *Api.Map = self.store.getMap(position) orelse return Api.MoveError.MapNotFound;

    const character_id = self.store.characters.getId(character_name) orelse return Api.MoveError.CharacterNotFound;
    const character: *Api.Character = self.store.characters.get(character_id).?;

    if (character.position.eql(position)) {
        return Api.MoveError.CharacterAlreadyMap;
    }

    const now = @as(f64, @floatFromInt(self.clock.nanoTimestamp())) / std.time.ns_per_s;
    if (character.cooldown_expiration) |cooldown_expiration| {
        if (cooldown_expiration > now) {
            return Api.MoveError.CharacterInCooldown;
        }
    }

    const duration_i64 = (@abs(position.x - character.position.x) + @abs(position.y - character.position.y)) * 5;
    const duration_f64: f64 = @floatFromInt(duration_i64);
    const expiration = now + duration_f64;

    character.cooldown_expiration = expiration;
    character.position = position;

    return Api.MoveResult{
        .cooldown = Api.Cooldown{
            .reason = .movement,
            .started_at = now,
            .expiration = expiration
        },
        .character = character.*,
        .destination = map.*
    };
}

pub fn gather(self: *Server, character_name: []const u8) Api.GatherError!Api.GatherResult {
    self.sleepRequestBegin();
    defer self.sleepRequestEnd();

    const character_id = self.store.characters.getId(character_name) orelse return Api.GatherError.CharacterNotFound;
    const character: *Api.Character = self.store.characters.get(character_id).?;

    const now = @as(f64, @floatFromInt(self.clock.nanoTimestamp())) / std.time.ns_per_s;
    if (character.cooldown_expiration) |cooldown_expiration| {
        if (cooldown_expiration > now) {
            return Api.GatherError.CharacterInCooldown;
        }
    }

    const map: *Api.Map = self.store.getMap(character.position) orelse return Api.GatherError.FatalError;
    const map_content = map.content orelse return Api.GatherError.MapContentNotFound;
    if (map_content.type != .resource) {
        return Api.GatherError.MapContentNotFound;
    }

    const map_content_code = map_content.code.slice();
    const resource_id = self.store.resources.getId(map_content_code) orelse return Api.GatherError.MapContentNotFound;
    const resource = self.store.resources.get(resource_id) orelse return Api.GatherError.FatalError;

    const character_skill = character.skills.getPtr(resource.skill.toCharacterSkill());
    if (character_skill.level < resource.level) {
        return Api.GatherError.CharacterNotSkillLevelRequired;
    }

    const duration = 25; // TODO: Update duration calculation
    const expiration = now + duration;

    var items: Api.GatherResult.Details.Items = .{};
    const xp = 8; // TODO: Update xp calculation

    var rng = self.rng.random();
    for (resource.drops.slice()) |_drop| {
        const drop: Api.Resource.Drop = _drop;
        if (rng.uintLessThan(u64, drop.rate) == 0) {
            const quantity = rng.intRangeAtMost(u64, drop.min_quantity, drop.max_quantity);
            items.add(drop.item, quantity) catch return Api.GatherError.FatalError;
        }
    }

    var inventory = character.inventory;
    inventory.addSlice(items.slice()) catch return Api.GatherError.CharacterInventoryFull;

    if (inventory.totalQuantity() > character.inventory_max_items) {
        return Api.GatherError.CharacterInventoryFull;
    }
    character.inventory = inventory;

    character_skill.xp += xp;
    while (character_skill.xp > character_skill.max_xp and character_skill.level < max_level) {
        character_skill.level += 1;
        character_skill.max_xp = max_xp_per_level[character_skill.level - 1];
    }
    if (character_skill.level == max_level) {
        character_skill.xp = 0;
        character_skill.max_xp = 0;
    }

    character.cooldown_expiration = expiration;

    return Api.GatherResult{
        .character = character.*,
        .cooldown = Api.Cooldown{
            .reason = .gathering,
            .started_at = now,
            .expiration = expiration
        },
        .details = Api.GatherResult.Details{
            .xp = xp,
            .items = items
        }
    };
}

pub fn craft(self: *Server, character: []const u8, item: []const u8, quantity: u64) Api.CraftError!Api.CraftResult {
    _ = self;
    _ = character;
    _ = item;
    _ = quantity;
    return Api.FetchError.FatalError;
}

pub fn equip(self: *Server, character: []const u8, slot: Api.Equipment.SlotId, item: []const u8, quantity: u64) Api.EquipError!Api.EquipResult {
    _ = self;
    _ = character;
    _ = item;
    _ = slot;
    _ = quantity;
    return Api.FetchError.FatalError;
}

pub fn unequip(self: *Server, character: []const u8, slot: Api.Equipment.SlotId, quantity: u64) Api.EquipError!Api.EquipResult {
    _ = self;
    _ = character;
    _ = slot;
    _ = quantity;
    return Api.FetchError.FatalError;
}
