// zig fmt: off
const std = @import("std");
const Api = @import("artifacts-api");
const Allocator = std.mem.Allocator;

const GatherGoal = @import("gather_goal.zig");
const CraftGoal = @import("craft_goal.zig");

const assert = std.debug.assert;
const log = std.log.scoped(.artificer);

const Artificer = @This();

pub const GoalId = packed struct {
    const Generation = u5;
    const Index = u11;

    generation: Generation,
    index: Index,

    pub fn eql(self: GoalId, other: GoalId) bool {
        return self.index == other.index and self.generation == other.generation;
    }
};

const max_goals = std.math.maxInt(GoalId.Index);

const expiration_margin: u64 = 100 * std.time.ns_per_ms; // 100ms
const server_down_retry_interval = 5; // minutes

pub const Goal = union(enum) {
    gather: GatherGoal,
    craft: CraftGoal,

    pub fn tick(self: *Goal, goal_id: Artificer.GoalId, artificer: *Artificer) !void {
        switch (self.*) {
            .gather => |*gather| gather.tick(goal_id, artificer),
            .craft => |*craft| try craft.tick(goal_id, artificer),
        }
    }

    pub fn requirements(self: Goal, artificer: *Artificer) Requirements {
        return switch (self) {
            .gather => |gather| gather.requirements(artificer),
            .craft => |craft| craft.requirements(artificer),
        };
    }

    pub fn onActionCompleted(self: *Goal, goal_id: Artificer.GoalId, result: Artificer.ActionResult) void {
        switch (self.*) {
            .gather => |*gather| gather.onActionCompleted(goal_id, result),
            .craft => |*craft| craft.onActionCompleted(goal_id, result),
        }
    }
};

const GoalSlot = struct {
    generation: GoalId.Generation = 0,
    parent_goal: ?GoalId = null,
    goal: ?Goal = null,
};

pub const Action = union(enum) {
    move: Api.Position,
    gather,
    craft: struct {
        item: Api.Store.Id,
        quantity: u64
    }
};

pub const ActionResult = union(enum) {
    move: Api.MoveResult,
    gather: Api.GatherResult,
    craft: Api.CraftResult,
};

const ActionSlot = struct {
    goal: GoalId,
    action: Action,
};

pub const Requirements = struct {
    pub const Items = Api.SimpleItem.BoundedArray(blk: {
        var max: usize = 0;
        max = @max(max, Api.Craft.max_items);
        break :blk max;
    });

    items: Items = .{}
};

const QueuedActions = std.ArrayListUnmanaged(ActionSlot);

server: *Api.Server,
character: Api.Store.Id,
goal_slots: []GoalSlot,
queued_actions: QueuedActions,

pub fn init(allocator: Allocator, server: *Api.Server, character: Api.Store.Id) !Artificer {
    const max_queued_actions = 16;

    const goal_slots = try allocator.alloc(GoalSlot, max_goals);
    errdefer allocator.free(goal_slots);
    @memset(goal_slots, .{});

    var queued_actions = try QueuedActions.initCapacity(allocator, max_queued_actions);
    errdefer queued_actions.deinit(allocator);

    return Artificer{
        .server = server,
        .goal_slots = goal_slots,
        .character = character,
        .queued_actions = queued_actions
    };
}

pub fn deinit(self: *Artificer, allocator: Allocator) void {
    allocator.free(self.goal_slots);
    self.queued_actions.deinit(allocator);
}

pub fn appendGoal(self: *Artificer, goal: Goal) !GoalId {
    for (0.., self.goal_slots) |index, *goal_slot| {
        if (goal_slot.goal != null) {
            continue;
        }

        if (goal_slot.generation == std.math.maxInt(GoalId.Generation)) {
            continue;
        }

        goal_slot.goal = goal; 
        return GoalId{
            .index = @intCast(index),
            .generation = goal_slot.generation
        };
    }

    return error.OutOfMemory;
}

pub fn removeGoal(self: *Artificer, id: GoalId) void {
    if (self.getGoal(id)) |goal_slot| {
        goal_slot.* = .{
            .generation = goal_slot.generation + 1
        };
    }
}

pub fn getGoal(self: *Artificer, id: GoalId) ?*GoalSlot {
    const slot = &self.goal_slots[id.index];

    if (slot.generation != id.generation) {
        return null;
    }

    if (slot.goal == null) {
        return null;
    }

    return slot;
}

pub fn findBestResourceWithItem(self: *Artificer, item: Api.Store.Id) ?Api.Store.Id {
    const store = self.server.store;
    const character = store.characters.get(self.character).?;

    var best_resource: ?Api.Store.Id = null;
    var best_rate: u64 = 0;

    for (0.., store.resources.objects.items) |resource_id, optional_resource| {
        if (optional_resource != .object) {
            continue;
        }

        const resource = optional_resource.object;

        const skill = resource.skill.toCharacterSkill();
        const character_skill_level = character.skills.get(skill).level;
        if (character_skill_level < resource.level) {
            continue;
        }

        for (resource.drops.slice()) |_drop| {
            const drop: Api.Resource.Drop = _drop;
            if (drop.item != item) {
                continue;
            }

            // The lower the `drop.rate` the better
            if (best_resource == null or best_rate > drop.rate) {
                best_resource = resource_id;
                best_rate = drop.rate;
                break;
            }

        }
    }

    return best_resource;
}

pub fn findNearestMapWithResource(self: *Artificer, resource: Api.Store.Id) ?Api.Position {
    const store = self.server.store;
    const character = store.characters.get(self.character).?;
    const resource_code = store.resources.get(resource).?.code.slice();

    var nearest_position: ?Api.Position = null;  
    for (store.maps.items) |map| {
        const content = map.content orelse continue;

        if (content.type != .resource) {
            continue;
        }
        if (!std.mem.eql(u8, resource_code, content.code.slice())) {
            continue;
        }

        if (nearest_position == null or map.position.distance(character.position) < map.position.distance(nearest_position.?)) {
            nearest_position = map.position;
        }
    }

    return nearest_position;
}

pub fn findNearestWorkstation(self: *Artificer, skill: Api.Craft.Skill) ?Api.Position {
    const store = self.server.store;
    const character = store.characters.get(self.character).?;
    const skill_name = skill.toString();

    var nearest_position: ?Api.Position = null;  
    for (store.maps.items) |map| {
        const content = map.content orelse continue;

        if (content.type != .workshop) {
            continue;
        }
        if (!std.mem.eql(u8, skill_name, content.code.slice())) {
            continue;
        }

        if (nearest_position == null or map.position.distance(character.position) < map.position.distance(nearest_position.?)) {
            nearest_position = map.position;
        }
    }

    return nearest_position;
}

fn timeUntilCooldownExpires(self: *Artificer) u64 {
    const store = self.server.store;

    const character = store.characters.get(self.character).?;
    if (character.cooldown_expiration) |cooldown_expiration| {
        const cooldown_expiration_ns: i64 = @intFromFloat(cooldown_expiration * std.time.ns_per_s);
        const now = std.time.nanoTimestamp();
        if (cooldown_expiration_ns > now) {
            return @intCast(@as(i128, @intCast(cooldown_expiration_ns)) - now);
        }
    }

    return 0;
}

fn getGoalCount(self: *Artificer) u32 {
    var count: u32 = 0;

    for (self.goal_slots) |goal_slot| {
        if (goal_slot.goal == null) {
            continue;
        }

        count += 1;
    }

    return count;
}

fn hasSubGoals(self: *Artificer, goal_id: GoalId) bool {
    for (self.goal_slots) |goal_slot| {
        if (goal_slot.goal != null and goal_slot.parent_goal != null and goal_slot.parent_goal.?.eql(goal_id)) {
            return true;
        }
    }

    return false;
}

pub fn tick(self: *Artificer) !void {
    const store = self.server.store;
    const character = store.characters.get(self.character).?;

    if (self.queued_actions.items.len > 0) {
        const expires_in = self.timeUntilCooldownExpires();
        if (expires_in > 0) {
            return;
        }

        const action_slot = self.queued_actions.orderedRemove(0);
        const action_result = switch (action_slot.action) {
            .move => |position| ActionResult{
                .move = try self.server.move(character.name.slice(), position)
            },
            .gather => ActionResult{
                .gather = try self.server.gather(character.name.slice())
            },
            .craft => |craft| ActionResult{
                .craft = try self.server.craft(character.name.slice(), store.items.getName(craft.item).?, craft.quantity)
            }
        };

        if (self.getGoal(action_slot.goal)) |goal_slot| {
            const goal = &goal_slot.goal.?;
            goal.onActionCompleted(action_slot.goal, action_result);
        }

    } else {
        for (0.., self.goal_slots) |index, *goal_slot| {
            if (goal_slot.goal == null) {
                continue;
            }

            const goal = &(goal_slot.*.goal orelse continue);

            const goal_id = GoalId{
                .index = @intCast(index),
                .generation = goal_slot.generation
            };

            if (self.hasSubGoals(goal_id)) {
                continue;
            }

            const reqs = goal.requirements(self);
            for (reqs.items.slice()) |req_item| {
                const inventory_quantity = character.inventory.getQuantity(req_item.id);
                if (inventory_quantity < req_item.quantity) {
                    if (self.findBestResourceWithItem(req_item.id) != null) {
                        const subgoal_id = try self.appendGoal(.{
                            .gather = .{
                                .item = req_item.id,
                                .quantity = req_item.quantity - inventory_quantity,
                            }
                        });

                        const subgoal = self.getGoal(subgoal_id).?;
                        subgoal.parent_goal = goal_id;
                    } else {
                        @panic("Not all requirements were handled");
                    }
                }
            }

            if (self.hasSubGoals(goal_id)) {
                continue;
            }

            try goal.tick(goal_id, self);
        }
    }
}

pub fn runUntilGoalsComplete(self: *Artificer) !void {
    while (self.getGoalCount() > 0) {
        const expires_in = self.timeUntilCooldownExpires();
        if (expires_in > 0) {
            log.debug("Sleeping for {d:.3}s", .{ @as(f64, @floatFromInt(expires_in)) / std.time.ns_per_s });
            std.time.sleep(expires_in);
        }

        try self.tick();
    }
}

pub fn runForever(self: *Artificer) !void {
    while (true) {
        const expires_in = self.timeUntilCooldownExpires();
        if (expires_in > 0) {
            log.debug("Sleeping for {d:.3}s", .{ @as(f64, @floatFromInt(expires_in)) / std.time.ns_per_s });
            std.time.sleep(expires_in);
            log.debug("Finished sleeping", .{});
        }

        try self.tick();
    }
}
