// 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 EquipGoal = @import("equip_goal.zig");

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

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,
    equip: EquipGoal,

    pub fn tick(self: *Goal, ctx: *GoalContext) !void {
        switch (self.*) {
            .gather => |*gather| gather.tick(ctx),
            .craft => |*craft| try craft.tick(ctx),
            .equip => |*equip| equip.tick(ctx),
        }
    }

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

    pub fn onActionCompleted(self: *Goal, ctx: *GoalContext, result: ActionResult) void {
        switch (self.*) {
            .gather => |*gather| gather.onActionCompleted(ctx, result),
            .craft => |*craft| craft.onActionCompleted(ctx, result),
            .equip => |*equip| equip.onActionCompleted(ctx, 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
    },
    unequip: struct {
        slot: Api.Equipment.SlotId,
        quantity: u64
    },
    equip: struct {
        slot: Api.Equipment.SlotId,
        item: Api.Store.Id,
        quantity: u64
    },
};

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

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

pub const Requirements = struct {
    pub const Items = Api.SimpleItem.BoundedArray(Api.Craft.max_items);

    items: Items = .{}
};

const QueuedActions = std.ArrayListUnmanaged(ActionSlot);

pub const GoalContext = struct {
    goal_id: GoalId,
    store: *Api.Store,
    character: *Api.Character,
    queued_actions: *QueuedActions,
    completed: bool = false,

    pub fn queueAction(self: *GoalContext, action: Action) void {
        self.queued_actions.appendAssumeCapacity(ActionSlot{
            .goal = self.goal_id,
            .action = action
        });
    }

    pub fn findBestResourceWithItem(self: *GoalContext, item: Api.Store.Id) ?Api.Store.Id {
        var best_resource: ?Api.Store.Id = null;
        var best_rate: u64 = 0;

        for (0.., self.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 = self.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: *GoalContext, resource: Api.Store.Id) ?Api.Position {
        const resource_code = self.store.resources.get(resource).?.code.slice();

        var nearest_position: ?Api.Position = null;  
        for (self.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(self.character.position) < map.position.distance(nearest_position.?)) {
                nearest_position = map.position;
            }
        }

        return nearest_position;
    }

    pub fn findNearestWorkstation(self: *GoalContext, skill: Api.Craft.Skill) ?Api.Position {
        const skill_name = skill.toString();

        var nearest_position: ?Api.Position = null;  
        for (self.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(self.character.position) < map.position.distance(nearest_position.?)) {
                nearest_position = map.position;
            }
        }

        return nearest_position;
    }
};

pub fn ArtificerType(Clock: type, Server: type) type {
    return struct {
        const Self = @This();

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

        pub fn init(allocator: Allocator, store: *Api.Store, clock: *Clock, server: *Server, character: Api.Store.Id) !Self {
            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 Self{
                .clock = clock,
                .server = server,
                .store = store,
                .goal_slots = goal_slots,
                .character = character,
                .queued_actions = queued_actions
            };
        }

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

        pub fn appendGoal(self: *Self, 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: *Self, id: GoalId) void {
            if (self.getGoal(id)) |goal_slot| {
                goal_slot.* = .{
                    .generation = goal_slot.generation + 1
                };
            }
        }

        pub fn getGoal(self: *Self, 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;
        }

        fn timeUntilCooldownExpires(self: *Self) 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 = self.clock.nanoTimestamp();
                if (cooldown_expiration_ns > now) {
                    return @intCast(@as(i128, @intCast(cooldown_expiration_ns)) - now);
                }
            }

            return 0;
        }

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

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

                count += 1;
            }

            return count;
        }

        fn hasSubGoals(self: *Self, 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;
        }

        fn createGoalContext(self: *Self, goal_id: GoalId) GoalContext {
            return GoalContext{
                .goal_id = goal_id,
                .queued_actions = &self.queued_actions,
                .character = self.store.characters.get(self.character).?,
                .store = self.store,
                .completed = false,
            };
        }

        pub fn tick(self: *Self) !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 character_name = character.name.slice();
                log.debug("(action) {}", .{ action_slot.action });
                const action_result = switch (action_slot.action) {
                    .move => |position| ActionResult{
                        .move = try self.server.move(character_name, position)
                    },
                    .gather => ActionResult{
                        .gather = try self.server.gather(character_name)
                    },
                    .craft => |craft| ActionResult{
                        .craft = try self.server.craft(character_name, store.items.getName(craft.item).?, craft.quantity)
                    },
                    .equip => |equip| ActionResult{
                        .equip = try self.server.equip(character_name, equip.slot, store.items.getName(equip.item).?, equip.quantity)
                    },
                    .unequip => |unequip| ActionResult{
                        .unequip = try self.server.unequip(character_name, unequip.slot, unequip.quantity)
                    }
                };

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

                    var goal_context = self.createGoalContext(action_slot.goal);
                    goal.onActionCompleted(&goal_context, action_result);
                    if (goal_context.completed) {
                        self.removeGoal(action_slot.goal);
                    }
                }

            } 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;
                    }

                    var goal_context = self.createGoalContext(goal_id);

                    const reqs = goal.requirements(&goal_context);
                    for (reqs.items.slice()) |req_item| {
                        const inventory_quantity = character.inventory.getQuantity(req_item.id);
                        if (inventory_quantity < req_item.quantity) {
                            const missing_quantity = req_item.quantity - inventory_quantity;
                            const item = store.items.get(req_item.id).?;

                            if (goal_context.findBestResourceWithItem(req_item.id) != null) {
                                const subgoal_id = try self.appendGoal(.{
                                    .gather = .{
                                        .item = req_item.id,
                                        .quantity = missing_quantity
                                    }
                                });

                                const subgoal = self.getGoal(subgoal_id).?;
                                subgoal.parent_goal = goal_id;
                            } else if (item.craft != null) {
                                const subgoal_id = try self.appendGoal(.{
                                    .craft = .{
                                        .item = req_item.id,
                                        .quantity = missing_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_context);
                    if (goal_context.completed) {
                        self.removeGoal(goal_id);
                    }
                }
            }
        }

        pub fn runUntilGoalsComplete(self: *Self) !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 });
                    self.clock.sleep(expires_in);
                }

                try self.tick();
            }
        }

        pub fn runForever(self: *Self) !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 });
                    self.clock.sleep(expires_in);
                    log.debug("Finished sleeping", .{});
                }

                try self.tick();
            }
        }
    };
}
