const std = @import("std");
const Api = @import("artifacts-api");
const Allocator = std.mem.Allocator;

const TaskGraph = @This();
const CharacterTask = @import("./task.zig").Task;

const TaskNodeId = u16;
const TaskNode = struct {
    const Dependencies = std.BoundedArray(TaskNodeId, 8);
    const MissingItems = Api.BoundedSlotsArray(8);

    task: CharacterTask,
    dependencies: Dependencies = Dependencies.init(0) catch unreachable,
    missing_items: MissingItems = MissingItems.init(),
};

const Nodes = std.ArrayList(TaskNode);

nodes: Nodes,

pub fn init(allocator: Allocator) TaskGraph {
    return TaskGraph{ .nodes = Nodes.init(allocator) };
}

pub fn deinit(self: TaskGraph) void {
    self.nodes.deinit();
}

fn get(self: *TaskGraph, id: TaskNodeId) *TaskNode {
    return &self.nodes.items[id];
}

fn addTask(self: *TaskGraph, node: TaskNode) !TaskNodeId {
    try self.nodes.append(node);
    return @intCast(self.nodes.items.len-1);
}

fn addFightTask(self: *TaskGraph, api: *Api.Server, item_id: Api.CodeId, quantity: u64) !TaskNodeId {
    const item_code = api.store.getCode(item_id) orelse return error.ItemNotFound;
    const monsters = try api.getMonsters(.{ .drop = item_code });
    defer monsters.deinit();

    if (monsters.items.len == 0) return error.ResourceNotFound;
    if (monsters.items.len > 1) std.log.warn("Multiple monsters exist for target item", .{});
    const monster_code = monsters.items[0].code;

    const resource_maps = try self.api.getMaps(.{ .code = monster_code });
    defer resource_maps.deinit();

    // This monster currently doesn't exist on the map. Probably only spawns in certain situations.
    if (resource_maps.items.len == 0) return error.MapNotFound;

    if (resource_maps.items.len > 1) std.log.warn("Multiple map locations exist for monster", .{});
    const resource_map = resource_maps.items[0];

    return try self.addTask(TaskNode{
        .task = .{
            .fight = .{
                .at = resource_map.position,
                .until = .{ .item = Api.ItemQuantity.init(item_id, quantity) }
            }
        }
    });
}

fn addGatherTask(self: *TaskGraph, api: *Api.Server, item_id: Api.CodeId, quantity: u64) !TaskNodeId {
    const item_code = api.store.getCode(item_id) orelse return error.ItemNotFound;
    const resources = try api.getResources(.{ .drop = item_code });
    defer resources.deinit();

    if (resources.items.len == 0) return error.ResourceNotFound;
    if (resources.items.len > 1) std.log.warn("Multiple resources exist for target item", .{});
    const resource_code = resources.items[0].code;

    const resource_maps = try self.api.getMaps(.{ .code = resource_code });
    defer resource_maps.deinit();

    if (resource_maps.items.len == 0) return error.MapNotFound;
    if (resource_maps.items.len > 1) std.log.warn("Multiple map locations exist for resource", .{});
    const resource_map = resource_maps.items[0];

    return try self.addTask(TaskNode{
        .task = .{
            .gather = .{
                .at = resource_map.position,
                .until = .{ .item = Api.ItemQuantity.init(item_id, quantity) }
            }
        }
    });
}

fn addCraftTaskShallow(self: *TaskGraph, api: *Api.Server, item_id: Api.CodeId, quantity: u64) !TaskNodeId {
    const item = (try api.getItemById(item_id)) orelse return error.ItemNotFound;
    const recipe = item.craft orelse return error.RecipeNotFound;

    const skill_str = Api.Server.SkillUtils.toString(recipe.skill);
    const workshop_maps = try self.api.getMaps(.{ .code = skill_str, .type = .workshop });
    defer workshop_maps.deinit();

    if (workshop_maps.items.len == 0) return error.WorkshopNotFound;
    if (workshop_maps.items.len > 1) std.log.warn("Multiple workshop locations exist", .{});

    return try self.addTask(TaskNode{
        .task = .{
            .craft = .{
                .at = workshop_maps.items[0].position,
                .target = Api.ItemQuantity.init(item_id, quantity)
            }
        }
    });
}

fn addCraftTask(self: *TaskGraph, api: *Api.Server, item_id: Api.CodeId, quantity: u64) !TaskNodeId {
    const node_id = try self.addCraftTaskShallow(api, item_id, quantity);
    var node = self.get(node_id);

    const item = (try api.getItemById(item_id)) orelse return error.ItemNotFound;
    const recipe = item.craft orelse return error.RecipeNotFound;

    const craft_count = recipe.quantity;

    for (recipe.items.slots.constSlice()) |material| {
        const needed_quantity = material.quantity * craft_count;

        if (try self.addAutoTask(api, material.id, needed_quantity)) |dependency_id| {
            try node.dependencies.append(dependency_id);
        } else {
            try node.missing_items.add(material.id, needed_quantity);
        }
    }

    return node_id;
}

// TODO: Remove `anyerror` from function declaration
fn addAutoTask(self: *TaskGraph, api: *Api.Server, item_id: Api.CodeId, quantity: u64) anyerror!?TaskNodeId {
    const item = (try self.api.getItemById(item_id)) orelse return error.ItemNotFound;

    if (item.craft != null) {
        return try self.addCraftTask(api, item_id, quantity);
    } else if (item.type == .resource) {
        const eql = std.mem.eql;
        if (eql(u8, item.subtype, "mining") or eql(u8, item.subtype, "fishing") or eql(u8, item.subtype, "woodcutting")) {
            return try self.addGatherTask(api, item_id, quantity);
        } else if (eql(u8, item.subtype, "mob")) {
            return try self.addFightTask(api, item_id, quantity);
        }
    }

    return null;
}

fn printTask(self: *TaskGraph, api: *Api.Server, node_id: TaskNodeId) void {
    self.printTaskLevel(api, node_id, 0);
}

fn writeIdentation(level: u32) void {
    const mutex = std.debug.getStderrMutex();
    mutex.lock();
    defer mutex.unlock();

    const stderr = std.io.getStdErr().writer();
    stderr.writeBytesNTimes("  ", level) catch return;
}

fn printTaskLevel(self: *TaskGraph, api: *Api.Server, node_id: TaskNodeId, level: u32) void {
    const node = self.get(node_id);
    const print = std.debug.print;

    writeIdentation(level);
    switch (node.task) {
        .fight => |args| {
            const target_item = args.until.item;
            const item = api.store.getCode(target_item.id).?;
            print("Task{{ .fight = {{ \"{s}\" x {} }}, .at = {} }}\n", .{item, target_item.quantity, args.at});
        },
        .gather => |args| {
            const target_item = args.until.item;
            const item = api.store.getCode(target_item.id).?;
            print("Task{{ .gather = {{ \"{s}\" x {} }}, .at = {} }}\n", .{item, target_item.quantity, args.at});
        },
        .craft => |args| {
            const item = api.store.getCode(args.target.id).?;
            print("Task{{ .craft = {{ \"{s}\" x {} }}, .at = {} }}\n", .{item, args.target.quantity, args.at});
        },
    }

    for (node.dependencies.constSlice()) |dependency| {
        self.printTaskLevel(dependency, level + 1);
    }
    for (node.missing_items.slots.constSlice()) |slot| {
        const item_code = api.getItemCode(slot.id).?;
        writeIdentation(level+1);
        print("+ {{ \"{s}\" x {} }}\n", .{item_code, slot.quantity});
    }
}
