#include <functional>
#include <climits> // for CHAR_BIT

#include "df/building_design.h"
#include "df/building_doorst.h"
#include "df/building_type.h"
#include "df/general_ref_building_holderst.h"
#include "df/job_item.h"
#include "df/ui_build_selector.h"

#include "modules/Buildings.h"
#include "modules/Gui.h"
#include "modules/Job.h"

#include "LuaTools.h"
#include "uicommon.h"

#include "buildingplan-planner.h"
#include "buildingplan-lib.h"

static const std::string planned_building_persistence_key_v1 = "buildingplan/constraints";
static const std::string planned_building_persistence_key_v2 = "buildingplan/constraints2";
static const std::string global_settings_persistence_key = "buildingplan/global";

/*
 * ItemFilter
 */

ItemFilter::ItemFilter()
{
    clear();
}

void ItemFilter::clear()
{
    min_quality = df::item_quality::Ordinary;
    max_quality = df::item_quality::Masterful;
    decorated_only = false;
    clearMaterialMask();
    materials.clear();
}

bool ItemFilter::deserialize(std::string ser)
{
    clear();

    std::vector<std::string> tokens;
    split_string(&tokens, ser, "/");
    if (tokens.size() != 5)
    {
        debug("invalid ItemFilter serialization: '%s'", ser.c_str());
        return false;
    }

    if (!deserializeMaterialMask(tokens[0]) || !deserializeMaterials(tokens[1]))
        return false;

    setMinQuality(atoi(tokens[2].c_str()));
    setMaxQuality(atoi(tokens[3].c_str()));
    decorated_only = static_cast<bool>(atoi(tokens[4].c_str()));
    return true;
}

bool ItemFilter::deserializeMaterialMask(std::string ser)
{
    if (ser.empty())
        return true;

    if (!parseJobMaterialCategory(&mat_mask, ser))
    {
        debug("invalid job material category serialization: '%s'", ser.c_str());
        return false;
    }
    return true;
}

bool ItemFilter::deserializeMaterials(std::string ser)
{
    if (ser.empty())
        return true;

    std::vector<std::string> mat_names;
    split_string(&mat_names, ser, ",");
    for (auto m = mat_names.begin(); m != mat_names.end(); m++)
    {
        DFHack::MaterialInfo material;
        if (!material.find(*m) || !material.isValid())
        {
            debug("invalid material name serialization: '%s'", ser.c_str());
            return false;
        }
        materials.push_back(material);
    }
    return true;
}

// format: mat,mask,elements/materials,list/minq/maxq/decorated
std::string ItemFilter::serialize() const
{
    std::ostringstream ser;
    ser << bitfield_to_string(mat_mask, ",") << "/";
    if (!materials.empty())
    {
        ser << materials[0].getToken();
        for (size_t i = 1; i < materials.size(); ++i)
            ser << "," << materials[i].getToken();
    }
    ser << "/" << static_cast<int>(min_quality);
    ser << "/" << static_cast<int>(max_quality);
    ser << "/" << static_cast<int>(decorated_only);
    return ser.str();
}

void ItemFilter::clearMaterialMask()
{
    mat_mask.whole = 0;
}

void ItemFilter::addMaterialMask(uint32_t mask)
{
    mat_mask.whole |= mask;
}

void ItemFilter::setMaterials(std::vector<DFHack::MaterialInfo> materials)
{
    this->materials = materials;
}

static void clampItemQuality(df::item_quality *quality)
{
    if (*quality > item_quality::Artifact)
    {
        debug("clamping quality to Artifact");
        *quality = item_quality::Artifact;
    }
    if (*quality < item_quality::Ordinary)
    {
        debug("clamping quality to Ordinary");
        *quality = item_quality::Ordinary;
    }
}

void ItemFilter::setMinQuality(int quality)
{
    min_quality = static_cast<df::item_quality>(quality);
    clampItemQuality(&min_quality);
    if (max_quality < min_quality)
        max_quality = min_quality;
}

void ItemFilter::setMaxQuality(int quality)
{
    max_quality = static_cast<df::item_quality>(quality);
    clampItemQuality(&max_quality);
    if (max_quality < min_quality)
        min_quality = max_quality;
}

void ItemFilter::incMinQuality() { setMinQuality(min_quality + 1); }
void ItemFilter::decMinQuality() { setMinQuality(min_quality - 1); }
void ItemFilter::incMaxQuality() { setMaxQuality(max_quality + 1); }
void ItemFilter::decMaxQuality() { setMaxQuality(max_quality - 1); }

void ItemFilter::toggleDecoratedOnly() { decorated_only = !decorated_only; }

static std::string material_to_string_fn(const MaterialInfo &m) { return m.toString(); }

uint32_t ItemFilter::getMaterialMask() const { return mat_mask.whole; }

std::vector<std::string> ItemFilter::getMaterials() const
{
    std::vector<std::string> descriptions;
    transform_(materials, descriptions, material_to_string_fn);

    if (descriptions.size() == 0)
        bitfield_to_string(&descriptions, mat_mask);

    if (descriptions.size() == 0)
        descriptions.push_back("any");

    return descriptions;
}

std::string ItemFilter::getMinQuality() const
{
    return ENUM_KEY_STR(item_quality, min_quality);
}

std::string ItemFilter::getMaxQuality() const
{
    return ENUM_KEY_STR(item_quality, max_quality);
}

bool ItemFilter::getDecoratedOnly() const
{
    return decorated_only;
}

bool ItemFilter::matchesMask(DFHack::MaterialInfo &mat) const
{
    return mat_mask.whole ? mat.matches(mat_mask) : true;
}

bool ItemFilter::matches(df::dfhack_material_category mask) const
{
    return mask.whole & mat_mask.whole;
}

bool ItemFilter::matches(DFHack::MaterialInfo &material) const
{
    for (auto it = materials.begin(); it != materials.end(); ++it)
        if (material.matches(*it))
            return true;
    return false;
}

bool ItemFilter::matches(df::item *item) const
{
    if (item->getQuality() < min_quality || item->getQuality() > max_quality)
        return false;

    if (decorated_only && !item->hasImprovements())
        return false;

    auto imattype = item->getActualMaterial();
    auto imatindex = item->getActualMaterialIndex();
    auto item_mat = DFHack::MaterialInfo(imattype, imatindex);

    return (materials.size() == 0) ? matchesMask(item_mat) : matches(item_mat);
}


/*
 * PlannedBuilding
 */

// format: itemfilterser|itemfilterser|...
static std::string serializeFilters(const std::vector<ItemFilter> &filters)
{
    std::ostringstream ser;
    if (!filters.empty())
    {
        ser << filters[0].serialize();
        for (size_t i = 1; i < filters.size(); ++i)
            ser << "|" << filters[i].serialize();
    }
    return ser.str();
}

static std::vector<ItemFilter> deserializeFilters(std::string ser)
{
    std::vector<std::string> isers;
    split_string(&isers, ser, "|");
    std::vector<ItemFilter> ret;
    for (auto & iser : isers)
    {
        ItemFilter filter;
        if (filter.deserialize(iser))
            ret.push_back(filter);
    }
    return ret;
}

static size_t getNumFilters(BuildingTypeKey key)
{
    auto L = Lua::Core::State;
    color_ostream_proxy out(Core::getInstance().getConsole());
    Lua::StackUnwinder top(L);

    if (!lua_checkstack(L, 4) || !Lua::PushModulePublic(
            out, L, "plugins.buildingplan", "get_num_filters"))
    {
        debug("failed to push the lua method on the stack");
        return 0;
    }

    Lua::Push(L, std::get<0>(key));
    Lua::Push(L, std::get<1>(key));
    Lua::Push(L, std::get<2>(key));

    if (!Lua::SafeCall(out, L, 3, 1))
    {
        debug("lua call failed");
        return 0;
    }

    int num_filters = lua_tonumber(L, -1);
    lua_pop(L, 1);
    return num_filters;
}

PlannedBuilding::PlannedBuilding(df::building *building, const std::vector<ItemFilter> &filters)
    : building(building),
      building_id(building->id),
      filters(filters)
{
    config = DFHack::World::AddPersistentData(planned_building_persistence_key_v2);
    config.ival(0) = building_id;
    config.val() = serializeFilters(filters);
}

PlannedBuilding::PlannedBuilding(PersistentDataItem &config)
    : config(config),
      building(df::building::find(config.ival(0))),
      building_id(config.ival(0)),
      filters(deserializeFilters(config.val()))
{
    if (building)
    {
        if (filters.size() !=
            getNumFilters(toBuildingTypeKey(building)))
        {
            debug("invalid ItemFilter vector serialization: '%s'",
                config.val().c_str());
            building = NULL;
        }
    }
}

// Ensure the building still exists and is in a valid state. It can disappear
// for lots of reasons, such as running the game with the buildingplan plugin
// disabled, manually removing the building, modifying it via the API, etc.
bool PlannedBuilding::isValid() const
{
    return building && df::building::find(building_id)
        && building->getBuildStage() == 0;
}

void PlannedBuilding::remove()
{
    DFHack::World::DeletePersistentData(config);
    building = NULL;
}

df::building * PlannedBuilding::getBuilding()
{
    return building;
}

const std::vector<ItemFilter> & PlannedBuilding::getFilters() const
{
    // if we want to be able to dynamically change the filters, we'll need to
    // re-bucket the tasks in Planner.
    return filters;
}


/*
 * BuildingTypeKey
 */

BuildingTypeKey toBuildingTypeKey(
    df::building_type btype, int16_t subtype, int32_t custom)
{
    return std::make_tuple(btype, subtype, custom);
}

BuildingTypeKey toBuildingTypeKey(df::building *bld)
{
    return std::make_tuple(
        bld->getType(), bld->getSubtype(), bld->getCustomType());
}

BuildingTypeKey toBuildingTypeKey(df::ui_build_selector *uibs)
{
    return std::make_tuple(
        uibs->building_type, uibs->building_subtype, uibs->custom_type);
}

// rotates a size_t value left by count bits
// assumes count is not 0 or >= size_t_bits
// replace this with std::rotl when we move to C++20
static std::size_t rotl_size_t(size_t val, uint32_t count)
{
    static const int size_t_bits = CHAR_BIT * sizeof(std::size_t);
    return val << count | val >> (size_t_bits - count);
}

std::size_t BuildingTypeKeyHash::operator() (const BuildingTypeKey & key) const
{
    // cast first param to appease gcc-4.8, which is missing the enum
    // specializations for std::hash
    std::size_t h1 = std::hash<int32_t>()(static_cast<int32_t>(std::get<0>(key)));
    std::size_t h2 = std::hash<int16_t>()(std::get<1>(key));
    std::size_t h3 = std::hash<int32_t>()(std::get<2>(key));

    return h1 ^ rotl_size_t(h2, 8) ^ rotl_size_t(h3, 16);
}


/*
 * Planner
 */

// convert v1 persistent data into v2 format
// we can remove this conversion code once v2 has been live for a while
void migrateV1ToV2()
{
    std::vector<PersistentDataItem> configs;
    DFHack::World::GetPersistentData(&configs, planned_building_persistence_key_v1);
    if (configs.empty())
        return;

    debug("migrating %zu persisted configs to new format", configs.size());
    for (auto config : configs)
    {
        df::building *bld = df::building::find(config.ival(1));
        if (!bld)
        {
            debug("buliding no longer exists; removing config");
            DFHack::World::DeletePersistentData(config);
            continue;
        }

        if (bld->getBuildStage() != 0 || bld->jobs.size() != 1
            || bld->jobs[0]->job_items.size() != 1)
        {
            debug("building in invalid state; removing config");
            DFHack::World::DeletePersistentData(config);
            continue;
        }

        // fix up the building so we can set the material properties later
        bld->mat_type = -1;
        bld->mat_index = -1;

        // the v1 filters are not initialized correctly and will match any item.
        // we need to fix them up a bit.
        auto filter = bld->jobs[0]->job_items[0];
        df::item_type type;
        switch (bld->getType())
        {
        case df::building_type::Armorstand: type = df::item_type::ARMORSTAND; break;
        case df::building_type::Bed: type = df::item_type::BED; break;
        case df::building_type::Chair: type = df::item_type::CHAIR; break;
        case df::building_type::Coffin: type = df::item_type::COFFIN; break;
        case df::building_type::Door: type = df::item_type::DOOR; break;
        case df::building_type::Floodgate: type = df::item_type::FLOODGATE; break;
        case df::building_type::Hatch: type = df::item_type::HATCH_COVER; break;
        case df::building_type::GrateWall: type = df::item_type::GRATE; break;
        case df::building_type::GrateFloor: type = df::item_type::GRATE; break;
        case df::building_type::BarsVertical: type = df::item_type::BAR; break;
        case df::building_type::BarsFloor: type = df::item_type::BAR; break;
        case df::building_type::Cabinet: type = df::item_type::CABINET; break;
        case df::building_type::Box: type = df::item_type::BOX; break;
        case df::building_type::Weaponrack: type = df::item_type::WEAPONRACK; break;
        case df::building_type::Statue: type = df::item_type::STATUE; break;
        case df::building_type::Slab: type = df::item_type::SLAB; break;
        case df::building_type::Table: type = df::item_type::TABLE; break;
        case df::building_type::WindowGlass: type = df::item_type::WINDOW; break;
        case df::building_type::AnimalTrap: type = df::item_type::ANIMALTRAP; break;
        case df::building_type::Chain: type = df::item_type::CHAIN; break;
        case df::building_type::Cage: type = df::item_type::CAGE; break;
        case df::building_type::TractionBench: type = df::item_type::TRACTION_BENCH; break;
        default:
            debug("building has unhandled type; removing config");
            DFHack::World::DeletePersistentData(config);
            continue;
        }
        filter->item_type = type;
        filter->item_subtype = -1;
        filter->mat_type = -1;
        filter->mat_index = -1;
        filter->flags1.whole = 0;
        filter->flags2.whole = 0;
        filter->flags2.bits.allow_artifact = true;
        filter->flags3.whole = 0;
        filter->flags4 = 0;
        filter->flags5 = 0;
        filter->metal_ore = -1;
        filter->min_dimension = -1;
        filter->has_tool_use = df::tool_uses::NONE;
        filter->quantity = 1;

        std::vector<std::string> tokens;
        split_string(&tokens, config.val(), "/");
        if (tokens.size() != 2)
        {
            debug("invalid v1 format; removing config");
            DFHack::World::DeletePersistentData(config);
            continue;
        }

        ItemFilter item_filter;
        item_filter.deserializeMaterialMask(tokens[0]);
        item_filter.deserializeMaterials(tokens[1]);
        item_filter.setMinQuality(config.ival(2) - 1);
        item_filter.setMaxQuality(config.ival(4) - 1);
        if (config.ival(3) - 1)
            item_filter.toggleDecoratedOnly();

        // create the v2 record
        std::vector<ItemFilter> item_filters;
        item_filters.push_back(item_filter);
        PlannedBuilding pb(bld, item_filters);

        // remove the v1 record
        DFHack::World::DeletePersistentData(config);
        debug("v1 %s(%d) record successfully migrated",
              ENUM_KEY_STR(building_type, bld->getType()).c_str(),
              bld->id);
    }
}

// assumes no setting has '=' or '|' characters
static std::string serialize_settings(std::map<std::string, bool> & settings)
{
    std::ostringstream ser;
    for (auto & entry : settings)
    {
        ser << entry.first << "=" << (entry.second ? "1" : "0") << "|";
    }
    return ser.str();
}

static void deserialize_settings(std::map<std::string, bool> & settings,
                                 std::string ser)
{
    std::vector<std::string> tokens;
    split_string(&tokens, ser, "|");
    for (auto token : tokens)
    {
        if (token.empty())
            continue;

        std::vector<std::string> parts;
        split_string(&parts, token, "=");
        if (parts.size() != 2)
        {
            debug("invalid serialized setting format: '%s'", token.c_str());
            continue;
        }
        std::string key = parts[0];
        if (settings.count(key) == 0)
        {
            debug("unknown serialized setting: '%s", key.c_str());
            continue;
        }
        settings[key] = static_cast<bool>(atoi(parts[1].c_str()));
        debug("deserialized setting: %s = %d", key.c_str(), settings[key]);
    }
}

static DFHack::PersistentDataItem init_global_settings(
        std::map<std::string, bool> & settings)
{
    settings.clear();
    settings["blocks"] = true;
    settings["boulders"] = true;
    settings["logs"] = true;
    settings["bars"] = false;

    // load persistent global settings if they exist; otherwise create them
    std::vector<PersistentDataItem> items;
    DFHack::World::GetPersistentData(&items, global_settings_persistence_key);
    if (items.size() == 1)
    {
        DFHack::PersistentDataItem & config = items[0];
        deserialize_settings(settings, config.val());
        return config;
    }

    debug("initializing persistent global settings");
    DFHack::PersistentDataItem config =
        DFHack::World::AddPersistentData(global_settings_persistence_key);
    config.val() = serialize_settings(settings);
    return config;
}

const std::map<std::string, bool> & Planner::getGlobalSettings() const
{
    return global_settings;
}

bool Planner::setGlobalSetting(std::string name, bool value)
{
    if (global_settings.count(name) == 0)
    {
        debug("attempted to set invalid setting: '%s'", name.c_str());
        return false;
    }
    debug("global setting '%s' %d -> %d",
          name.c_str(), global_settings[name], value);
    global_settings[name] = value;
    if (config.isValid())
        config.val() = serialize_settings(global_settings);
    return true;
}

void Planner::reset()
{
    debug("resetting Planner state");
    default_item_filters.clear();
    planned_buildings.clear();
    tasks.clear();

    config = init_global_settings(global_settings);

    migrateV1ToV2();

    std::vector<PersistentDataItem> items;
    DFHack::World::GetPersistentData(&items, planned_building_persistence_key_v2);
    debug("found data for %zu planned building(s)", items.size());

    for (auto i = items.begin(); i != items.end(); i++)
    {
        PlannedBuilding pb(*i);
        if (!pb.isValid())
        {
            debug("discarding invalid planned building");
            pb.remove();
            continue;
        }

        if (registerTasks(pb))
            planned_buildings.insert(std::make_pair(pb.getBuilding()->id, pb));
    }
}

void Planner::addPlannedBuilding(df::building *bld)
{
    auto item_filters = getItemFilters(toBuildingTypeKey(bld)).get();
    // not a supported type
    if (item_filters.empty())
    {
        debug("failed to add building: unsupported type");
        return;
    }

    // protect against multiple registrations
    if (planned_buildings.count(bld->id) != 0)
    {
        debug("failed to add building: already registered");
        return;
    }

    PlannedBuilding pb(bld, item_filters);
    if (pb.isValid() && registerTasks(pb))
    {
        for (auto job : bld->jobs)
            job->flags.bits.suspend = true;

        planned_buildings.insert(std::make_pair(bld->id, pb));
    }
    else
    {
        pb.remove();
    }
}

static std::string getBucket(const df::job_item & ji,
                             const std::vector<ItemFilter> & item_filters)
{
    std::ostringstream ser;

    // pull out and serialize only known relevant fields. if we miss a few, then
    // the filter bucket will be slighly less specific than it could be, but
    // that's probably ok. we'll just end up bucketing slightly different items
    // together. this is only a problem if the different filter at the front of
    // the queue doesn't match any available items and blocks filters behind it
    // that could be matched.
    ser << ji.item_type << ':' << ji.item_subtype << ':' << ji.mat_type << ':'
        << ji.mat_index << ':' << ji.flags1.whole << ':' << ji.flags2.whole
        << ':' << ji.flags3.whole << ':' << ji.flags4 << ':' << ji.flags5 << ':'
        << ji.metal_ore << ':' << ji.has_tool_use;

    for (auto & item_filter : item_filters)
    {
        ser << ':' << item_filter.serialize();
    }

    return ser.str();
}

// get a list of item vectors that we should search for matches
static std::vector<df::job_item_vector_id> getVectorIds(df::job_item *job_item,
        const std::map<std::string, bool> & global_settings)
{
    std::vector<df::job_item_vector_id> ret;

    // if the filter already has the vector_id set to something specific, use it
    if (job_item->vector_id > df::job_item_vector_id::IN_PLAY)
    {
        debug("using vector_id from job_item: %s",
              ENUM_KEY_STR(job_item_vector_id, job_item->vector_id).c_str());
        ret.push_back(job_item->vector_id);
        return ret;
    }

    // if the filer is for building material, refer to our global settings for
    // which vectors to search
    if (job_item->flags2.bits.building_material)
    {
        if (global_settings.at("blocks"))
            ret.push_back(df::job_item_vector_id::BLOCKS);
        if (global_settings.at("boulders"))
            ret.push_back(df::job_item_vector_id::BOULDER);
        if (global_settings.at("logs"))
            ret.push_back(df::job_item_vector_id::WOOD);
        if (global_settings.at("bars"))
            ret.push_back(df::job_item_vector_id::BAR);
    }

    // fall back to IN_PLAY if no other vector was appropriate
    if (ret.empty())
        ret.push_back(df::job_item_vector_id::IN_PLAY);
    return ret;
}

bool Planner::registerTasks(PlannedBuilding & pb)
{
    df::building * bld = pb.getBuilding();
    if (bld->jobs.size() != 1)
    {
        debug("unexpected number of jobs: want 1, got %zu", bld->jobs.size());
        return false;
    }
    auto job_items = bld->jobs[0]->job_items;
    int num_job_items = job_items.size();
    if (num_job_items < 1)
    {
        debug("unexpected number of job items: want >0, got %d", num_job_items);
        return false;
    }
    int32_t id = bld->id;
    for (int job_item_idx = 0; job_item_idx < num_job_items; ++job_item_idx)
    {
        auto job_item = job_items[job_item_idx];
        auto bucket = getBucket(*job_item, pb.getFilters());
        auto vector_ids = getVectorIds(job_item, global_settings);

        // if there are multiple vector_ids, schedule duplicate tasks. after
        // the correct number of items are matched, the extras will get popped
        // as invalid
        for (auto vector_id : vector_ids)
        {
            for (int item_num = 0; item_num < job_item->quantity; ++item_num)
            {
                tasks[vector_id][bucket].push(std::make_pair(id, job_item_idx));
                debug("added task: %s/%s/%d,%d; "
                      "%zu vector(s), %zu filter bucket(s), %zu task(s) in bucket",
                      ENUM_KEY_STR(job_item_vector_id, vector_id).c_str(),
                      bucket.c_str(), id, job_item_idx, tasks.size(),
                      tasks[vector_id].size(), tasks[vector_id][bucket].size());
            }
        }
    }
    return true;
}

PlannedBuilding * Planner::getPlannedBuilding(df::building *bld)
{
    if (!bld || planned_buildings.count(bld->id) == 0)
        return NULL;
    return &planned_buildings.at(bld->id);
}

bool Planner::isPlannableBuilding(BuildingTypeKey key)
{
    return getNumFilters(key) >= 1;
}

Planner::ItemFiltersWrapper Planner::getItemFilters(BuildingTypeKey key)
{
    static std::vector<ItemFilter> empty_vector;
    static const ItemFiltersWrapper empty_ret(empty_vector);

    size_t nfilters = getNumFilters(key);
    if (nfilters < 1)
        return empty_ret;
    while (default_item_filters[key].size() < nfilters)
        default_item_filters[key].push_back(ItemFilter());
    return ItemFiltersWrapper(default_item_filters[key]);
}

// precompute a bitmask with bad item flags
struct BadFlags
{
    uint32_t whole;

    BadFlags()
    {
        df::item_flags flags;
        #define F(x) flags.bits.x = true;
        F(dump); F(forbid); F(garbage_collect);
        F(hostile); F(on_fire); F(rotten); F(trader);
        F(in_building); F(construction); F(in_job);
        F(owned); F(in_chest); F(removed); F(encased);
        #undef F
        whole = flags.whole;
    }
};

static bool itemPassesScreen(df::item * item)
{
    static BadFlags bad_flags;
    return !(item->flags.whole & bad_flags.whole)
        && !item->isAssignedToStockpile()
        // TODO: make this configurable
        && !(item->getType() == df::item_type::BOX && item->isBag());
}

static bool matchesFilters(df::item * item,
                           df::job_item * job_item,
                           const ItemFilter & item_filter)
{
    // check the properties that are not checked by Job::isSuitableItem()
    if (job_item->item_type > -1 && job_item->item_type != item->getType())
        return false;

    if (job_item->item_subtype > -1 &&
        job_item->item_subtype != item->getSubtype())
        return false;

    if (job_item->flags2.bits.building_material && !item->isBuildMat())
        return false;

    if (job_item->metal_ore > -1 && !item->isMetalOre(job_item->metal_ore))
        return false;

    if (job_item->has_tool_use > df::tool_uses::NONE
        && !item->hasToolUse(job_item->has_tool_use))
        return false;

    return DFHack::Job::isSuitableItem(
            job_item, item->getType(), item->getSubtype())
        && DFHack::Job::isSuitableMaterial(
            job_item, item->getMaterial(), item->getMaterialIndex(),
            item->getType())
        && item_filter.matches(item);
}

// note that this just removes the PlannedBuilding. the tasks will get dropped
// as we discover them in the tasks queues and they fail their isValid() check.
// this "lazy" task cleaning algorithm works because there is no way to
// re-register a building once it has been removed -- if it fails isValid()
// then it has either been built or desroyed. therefore there is no chance of
// duplicate tasks getting added to the tasks queues.
void Planner::unregisterBuilding(int32_t id)
{
    if (planned_buildings.count(id) > 0)
    {
        planned_buildings.at(id).remove();
        planned_buildings.erase(id);
    }
}

static bool isJobReady(df::job * job)
{
    int needed_items = 0;
    for (auto job_item : job->job_items) { needed_items += job_item->quantity; }
    if (needed_items)
    {
        debug("building needs %d more item(s)", needed_items);
        return false;
    }
    return true;
}

static bool job_item_idx_lt(df::job_item_ref *a, df::job_item_ref *b)
{
    // we want the items in the opposite order of the filters
    return a->job_item_idx > b->job_item_idx;
}

// this function does not remove the job_items since their quantity fields are
// now all at 0, so there is no risk of having extra items attached. we don't
// remove them to keep the "finalize with buildingplan active" path as similar
// as possible to the "finalize with buildingplan disabled" path.
static void finalizeBuilding(df::building * bld)
{
    debug("finalizing building %d", bld->id);
    auto job = bld->jobs[0];

    // sort the items so they get added to the structure in the correct order
    std::sort(job->items.begin(), job->items.end(), job_item_idx_lt);

    // derive the material properties of the building and job from the first
    // applicable item, though if any boulders are involved, it makes the whole
    // structure "rough".
    bool rough = false;
    for (auto attached_item : job->items)
    {
        df::item *item = attached_item->item;
        rough = rough || item->getType() == item_type::BOULDER;
        if (bld->mat_type == -1)
        {
            bld->mat_type = item->getMaterial();
            job->mat_type = bld->mat_type;
        }
        if (bld->mat_index == -1)
        {
            bld->mat_index = item->getMaterialIndex();
            job->mat_index = bld->mat_index;
        }
    }

    if (bld->needsDesign())
    {
        auto act = (df::building_actual *)bld;
        if (!act->design)
            act->design = new df::building_design();
        act->design->flags.bits.rough = rough;
    }

    // we're good to go!
    job->flags.bits.suspend = false;
    Job::checkBuildingsNow();
}

void Planner::popInvalidTasks(std::queue<std::pair<int32_t, int>> & task_queue)
{
    while (!task_queue.empty())
    {
        auto & task = task_queue.front();
        auto id = task.first;
        if (planned_buildings.count(id) > 0)
        {
            PlannedBuilding & pb = planned_buildings.at(id);
            if (pb.isValid() &&
                pb.getBuilding()->jobs[0]->job_items[task.second]->quantity)
            {
                break;
            }
        }
        debug("discarding invalid task: bld=%d, job_item_idx=%d",
              id, task.second);
        task_queue.pop();
        unregisterBuilding(id);
    }
}

void Planner::doVector(df::job_item_vector_id vector_id,
       std::map<std::string, std::queue<std::pair<int32_t, int>>> & buckets)
{
    auto other_id = ENUM_ATTR(job_item_vector_id, other, vector_id);
    auto item_vector = df::global::world->items.other[other_id];
    debug("matching %zu item(s) in vector %s against %zu filter bucket(s)",
          item_vector.size(),
          ENUM_KEY_STR(job_item_vector_id, vector_id).c_str(),
          buckets.size());
    for (auto item_it = item_vector.rbegin();
         item_it != item_vector.rend();
         ++item_it)
    {
        auto item = *item_it;
        if (!itemPassesScreen(item))
            continue;
        for (auto bucket_it = buckets.begin(); bucket_it != buckets.end();)
        {
            auto & task_queue = bucket_it->second;
            popInvalidTasks(task_queue);
            if (task_queue.empty())
            {
                debug("removing empty bucket: %s/%s; %zu bucket(s) left",
                      ENUM_KEY_STR(job_item_vector_id, vector_id).c_str(),
                      bucket_it->first.c_str(),
                      buckets.size() - 1);
                bucket_it = buckets.erase(bucket_it);
                continue;
            }
            auto & task = task_queue.front();
            auto id = task.first;
            auto & pb = planned_buildings.at(id);
            auto building = pb.getBuilding();
            auto job = building->jobs[0];
            auto filter_idx = task.second;
            if (matchesFilters(item, job->job_items[filter_idx],
                    pb.getFilters()[filter_idx])
               && DFHack::Job::attachJobItem(job, item,
                        df::job_item_ref::Hauled, filter_idx))
            {
                MaterialInfo material;
                material.decode(item);
                ItemTypeInfo item_type;
                item_type.decode(item);
                debug("attached %s %s to filter %d for %s(%d): %s/%s",
                      material.toString().c_str(),
                      item_type.toString().c_str(),
                      filter_idx,
                      ENUM_KEY_STR(building_type, building->getType()).c_str(),
                      id,
                      ENUM_KEY_STR(job_item_vector_id, vector_id).c_str(),
                      bucket_it->first.c_str());
                // keep quantity aligned with the actual number of remaining
                // items so if buildingplan is turned off, the building will
                // be completed with the correct number of items.
                --job->job_items[filter_idx]->quantity;
                task_queue.pop();
                if (isJobReady(job))
                {
                    finalizeBuilding(building);
                    unregisterBuilding(id);
                }
                if (task_queue.empty())
                {
                    debug(
                        "removing empty item bucket: %s/%s; %zu left",
                        ENUM_KEY_STR(job_item_vector_id, vector_id).c_str(),
                        bucket_it->first.c_str(),
                        buckets.size() - 1);
                    buckets.erase(bucket_it);
                }
                // we found a home for this item; no need to look further
                break;
            }
            ++bucket_it;
        }
        if (buckets.empty())
            break;
    }
}

struct VectorsToScanLast
{
    std::vector<df::job_item_vector_id> vectors;
    VectorsToScanLast()
    {
        // order is important here. we want to match boulders before wood and
        // everything before bars. blocks are not listed here since we'll have
        // already scanned them when we did the first pass through the buckets.
        vectors.push_back(df::job_item_vector_id::BOULDER);
        vectors.push_back(df::job_item_vector_id::WOOD);
        vectors.push_back(df::job_item_vector_id::BAR);
    }
};

void Planner::doCycle()
{
    debug("running cycle for %zu registered building(s)",
          planned_buildings.size());
    static const VectorsToScanLast vectors_to_scan_last;
    for (auto it = tasks.begin(); it != tasks.end();)
    {
        auto vector_id = it->first;
        // we could make this a set, but it's only three elements
        if (std::find(vectors_to_scan_last.vectors.begin(),
                      vectors_to_scan_last.vectors.end(),
                      vector_id) != vectors_to_scan_last.vectors.end())
        {
            ++it;
            continue;
        }

        auto & buckets = it->second;
        doVector(vector_id, buckets);
        if (buckets.empty())
        {
            debug("removing empty vector: %s; %zu vector(s) left",
                  ENUM_KEY_STR(job_item_vector_id, vector_id).c_str(),
                  tasks.size() - 1);
            it = tasks.erase(it);
        }
        else
            ++it;
    }
    for (auto vector_id : vectors_to_scan_last.vectors)
    {
        if (tasks.count(vector_id) == 0)
            continue;
        auto & buckets = tasks[vector_id];
        doVector(vector_id, buckets);
        if (buckets.empty())
        {
            debug("removing empty vector: %s; %zu vector(s) left",
                  ENUM_KEY_STR(job_item_vector_id, vector_id).c_str(),
                  tasks.size() - 1);
            tasks.erase(vector_id);
        }
    }
    debug("cycle done; %zu registered building(s) left",
          planned_buildings.size());
}

Planner planner;