#include "df/construction_type.h"
#include "df/entity_position.h"
#include "df/interface_key.h"
#include "df/ui_build_selector.h"
#include "df/viewscreen_dwarfmodest.h"

#include "modules/Gui.h"
#include "modules/Maps.h"
#include "modules/World.h"

#include "Core.h"
#include "LuaTools.h"
#include "PluginManager.h"

#include "uicommon.h"
#include "listcolumn.h"
#include "buildingplan.h"

DFHACK_PLUGIN("buildingplan");
#define PLUGIN_VERSION "2.0"
REQUIRE_GLOBAL(ui);
REQUIRE_GLOBAL(ui_build_selector);
REQUIRE_GLOBAL(world); // used in buildingplan library

#define MAX_MASK 10
#define MAX_MATERIAL 21

bool show_help = false;
bool quickfort_mode = false;
bool all_enabled = false;
bool in_dummy_screen = false;
std::unordered_map<BuildingTypeKey, bool, BuildingTypeKeyHash> planmode_enabled;

bool show_debugging = false;

void debug(const char *fmt, ...)
{
    if (!show_debugging)
        return;

    color_ostream_proxy out(Core::getInstance().getConsole());
    out.print("DEBUG(buildingplan): ");
    va_list args;
    va_start(args, fmt);
    out.vprint(fmt, args);
    va_end(args);
    out.print("\n");
}

class ViewscreenChooseMaterial : public dfhack_viewscreen
{
public:
    ViewscreenChooseMaterial(ItemFilter &filter);

    void feed(set<df::interface_key> *input);

    void render();

    std::string getFocusString() { return "buildingplan_choosemat"; }

private:
    ListColumn<df::dfhack_material_category> masks_column;
    ListColumn<MaterialInfo> materials_column;
    int selected_column;
    ItemFilter &filter;

    void addMaskEntry(df::dfhack_material_category &mask, const std::string &text)
    {
        auto entry = ListEntry<df::dfhack_material_category>(pad_string(text, MAX_MASK, false), mask);
        if (filter.matches(mask))
            entry.selected = true;

        masks_column.add(entry);
    }

    void populateMasks()
    {
        masks_column.clear();
        df::dfhack_material_category mask;

        mask.whole = 0;
        mask.bits.stone = true;
        addMaskEntry(mask, "Stone");

        mask.whole = 0;
        mask.bits.wood = true;
        addMaskEntry(mask, "Wood");

        mask.whole = 0;
        mask.bits.metal = true;
        addMaskEntry(mask, "Metal");

        mask.whole = 0;
        mask.bits.soap = true;
        addMaskEntry(mask, "Soap");

        masks_column.filterDisplay();
    }

    void populateMaterials()
    {
        materials_column.clear();
        df::dfhack_material_category selected_category;
        std::vector<df::dfhack_material_category> selected_masks = masks_column.getSelectedElems();
        if (selected_masks.size() == 1)
            selected_category = selected_masks[0];
        else if (selected_masks.size() > 1)
            return;

        df::world_raws &raws = world->raws;
        for (int i = 1; i < DFHack::MaterialInfo::NUM_BUILTIN; i++)
        {
            auto obj = raws.mat_table.builtin[i];
            if (obj)
            {
                MaterialInfo material;
                material.decode(i, -1);
                addMaterialEntry(selected_category, material, material.toString());
            }
        }

        for (size_t i = 0; i < raws.inorganics.size(); i++)
        {
            MaterialInfo material;
            material.decode(0, i);
            addMaterialEntry(selected_category, material, material.toString());
        }

        decltype(selected_category) wood_flag;
        wood_flag.bits.wood = true;
        if (!selected_category.whole || selected_category.bits.wood)
        {
            for (size_t i = 0; i < raws.plants.all.size(); i++)
            {
                df::plant_raw *p = raws.plants.all[i];
                for (size_t j = 0; p->material.size() > 1 && j < p->material.size(); j++)
                {
                    if (p->material[j]->id != "WOOD")
                        continue;

                    MaterialInfo material;
                    material.decode(DFHack::MaterialInfo::PLANT_BASE+j, i);
                    auto name = material.toString();
                    ListEntry<MaterialInfo> entry(pad_string(name, MAX_MATERIAL, false), material);
                    if (filter.matches(material))
                        entry.selected = true;

                    materials_column.add(entry);
                }
            }
        }
        materials_column.sort();
    }

    void addMaterialEntry(df::dfhack_material_category &selected_category,
                          MaterialInfo &material, std::string name)
    {
        if (!selected_category.whole || material.matches(selected_category))
        {
            ListEntry<MaterialInfo> entry(pad_string(name, MAX_MATERIAL, false), material);
            if (filter.matches(material))
                entry.selected = true;

            materials_column.add(entry);
        }
    }

    void validateColumn()
    {
        set_to_limit(selected_column, 1);
    }

    void resize(int32_t x, int32_t y)
    {
        dfhack_viewscreen::resize(x, y);
        masks_column.resize();
        materials_column.resize();
    }
};

const DFHack::MaterialInfo &material_info_identity_fn(const DFHack::MaterialInfo &m) { return m; }

ViewscreenChooseMaterial::ViewscreenChooseMaterial(ItemFilter &filter)
    : filter(filter)
{
    selected_column = 0;
    masks_column.setTitle("Type");
    masks_column.multiselect = true;
    masks_column.allow_search = false;
    masks_column.left_margin = 2;
    materials_column.left_margin = MAX_MASK + 3;
    materials_column.setTitle("Material");
    materials_column.multiselect = true;

    masks_column.changeHighlight(0);

    populateMasks();
    populateMaterials();

    masks_column.selectDefaultEntry();
    materials_column.selectDefaultEntry();
    materials_column.changeHighlight(0);
}

void ViewscreenChooseMaterial::feed(set<df::interface_key> *input)
{
    bool key_processed = false;
    switch (selected_column)
    {
    case 0:
        key_processed = masks_column.feed(input);
        if (input->count(interface_key::SELECT))
            populateMaterials(); // Redo materials lists based on category selection
        break;
    case 1:
        key_processed = materials_column.feed(input);
        break;
    }

    if (key_processed)
        return;

    if (input->count(interface_key::LEAVESCREEN))
    {
        input->clear();
        Screen::dismiss(this);
        return;
    }
    if (input->count(interface_key::CUSTOM_SHIFT_C))
    {
        filter.clear();
        masks_column.clearSelection();
        materials_column.clearSelection();
        populateMaterials();
    }
    else if (input->count(interface_key::SEC_SELECT))
    {
        // Convert list selections to material filters
        filter.clearMaterialMask();

        // Category masks
        auto masks = masks_column.getSelectedElems();
        for (auto it = masks.begin(); it != masks.end(); ++it)
            filter.addMaterialMask(it->whole);

        // Specific materials
        auto materials = materials_column.getSelectedElems();
        std::vector<DFHack::MaterialInfo> materialInfos;
        transform_(materials, materialInfos, material_info_identity_fn);
        filter.setMaterials(materialInfos);

        Screen::dismiss(this);
    }
    else if (input->count(interface_key::STANDARDSCROLL_LEFT))
    {
        --selected_column;
        validateColumn();
    }
    else if (input->count(interface_key::STANDARDSCROLL_RIGHT))
    {
        selected_column++;
        validateColumn();
    }
    else if (enabler->tracking_on && enabler->mouse_lbut)
    {
        if (masks_column.setHighlightByMouse())
            selected_column = 0;
        else if (materials_column.setHighlightByMouse())
            selected_column = 1;

        enabler->mouse_lbut = enabler->mouse_rbut = 0;
    }
}

void ViewscreenChooseMaterial::render()
{
    if (Screen::isDismissed(this))
        return;

    dfhack_viewscreen::render();

    Screen::clear();
    Screen::drawBorder("  Building Material  ");

    masks_column.display(selected_column == 0);
    materials_column.display(selected_column == 1);

    int32_t y = gps->dimy - 3;
    int32_t x = 2;
    OutputHotkeyString(x, y, "Toggle", interface_key::SELECT);
    x += 3;
    OutputHotkeyString(x, y, "Save", interface_key::SEC_SELECT);
    x += 3;
    OutputHotkeyString(x, y, "Clear", interface_key::CUSTOM_SHIFT_C);
    x += 3;
    OutputHotkeyString(x, y, "Cancel", interface_key::LEAVESCREEN);
}

//START Viewscreen Hook
static bool is_planmode_enabled(BuildingTypeKey key)
{
    return planmode_enabled[key] || quickfort_mode || all_enabled;
}

static std::string get_item_label(const BuildingTypeKey &key, int item_idx)
{
    auto L = Lua::Core::State;
    color_ostream_proxy out(Core::getInstance().getConsole());
    Lua::StackUnwinder top(L);

    if (!lua_checkstack(L, 5) ||
        !Lua::PushModulePublic(
            out, L, "plugins.buildingplan", "get_item_label"))
        return "Failed push";

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

    if (!Lua::SafeCall(out, L, 4, 1))
        return "Failed call";

    const char *s = lua_tostring(L, -1);
    if (!s)
        return "No string";

    return s;
}

static bool item_can_be_improved(const BuildingTypeKey &key, int item_idx)
{
    auto L = Lua::Core::State;
    color_ostream_proxy out(Core::getInstance().getConsole());
    Lua::StackUnwinder top(L);

    if (!lua_checkstack(L, 5) ||
        !Lua::PushModulePublic(
            out, L, "plugins.buildingplan", "item_can_be_improved"))
        return false;

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

    if (!Lua::SafeCall(out, L, 4, 1))
        return false;

    return lua_toboolean(L, -1);
}

static bool construct_planned_building()
{
    auto L = Lua::Core::State;
    color_ostream_proxy out(Core::getInstance().getConsole());

    CoreSuspendClaimer suspend;
    Lua::StackUnwinder top(L);

    if (!(lua_checkstack(L, 1) &&
          Lua::PushModulePublic(out, L, "plugins.buildingplan",
                                "construct_buildings_from_ui_state") &&
          Lua::SafeCall(out, L, 0, 1)))
    {
        return false;
    }

    // register all returned buildings with planner
    lua_pushnil(L);
    while (lua_next(L, -2) != 0)
    {
        auto bld = Lua::GetDFObject<df::building>(L, -1);
        if (!bld)
        {
            out.printerr(
                "buildingplan: construct_buildings_from_ui_state() failed\n");
            return false;
        }

        planner.addPlannedBuilding(bld);
        lua_pop(L, 1);
    }

    return true;
}

static void show_global_settings_dialog()
{
    auto L = Lua::Core::State;
    color_ostream_proxy out(Core::getInstance().getConsole());
    Lua::StackUnwinder top(L);

    if (!lua_checkstack(L, 2) ||
        !Lua::PushModulePublic(
            out, L, "plugins.buildingplan", "show_global_settings_dialog"))
    {
        debug("Failed to push the module");
        return;
    }

    lua_newtable(L);
    int ctable = lua_gettop(L);
    Lua::SetField(L, quickfort_mode, ctable, "quickfort_mode");
    Lua::SetField(L, all_enabled, ctable, "all_enabled");

    for (auto & setting : planner.getGlobalSettings())
    {
        Lua::SetField(L, setting.second, ctable, setting.first.c_str());
    }

    if (!Lua::SafeCall(out, L, 1, 0))
    {
        debug("Failed call to show_global_settings_dialog");
        return;
    }
}

static bool is_automaterial_enabled()
{
    auto L = Lua::Core::State;
    color_ostream_proxy out(Core::getInstance().getConsole());
    Lua::StackUnwinder top(L);

    if (!(lua_checkstack(L, 1) &&
          Lua::PushModulePublic(out, L, "plugins.automaterial", "isEnabled") &&
          Lua::SafeCall(out, L, 0, 1)))
    {
        return false;
    }

    return lua_toboolean(L, -1);
}

static bool is_automaterial_managed(df::building_type type, int16_t subtype)
{
    return is_automaterial_enabled()
        && type == df::building_type::Construction
        && subtype < df::construction_type::TrackN;
}

struct buildingplan_query_hook : public df::viewscreen_dwarfmodest
{
    typedef df::viewscreen_dwarfmodest interpose_base;

    // no non-static fields allowed (according to VTableInterpose.h)
    static df::building *bld;
    static PlannedBuilding *pb;
    static int filter_count;
    static int filter_idx;

    // logic is reversed since we're starting at the last filter
    bool hasNextFilter() const { return filter_idx > 0; }
    bool hasPrevFilter() const { return filter_idx + 1 < filter_count; }

    bool isInPlannedBuildingQueryMode()
    {
        return (ui->main.mode == df::ui_sidebar_mode::QueryBuilding ||
            ui->main.mode == df::ui_sidebar_mode::BuildingItems) &&
            planner.getPlannedBuilding(world->selected_building);
    }

    // reinit static fields when selected building changes
    void initStatics()
    {
        df::building *cur_bld = world->selected_building;
        if (bld != cur_bld)
        {
            bld = cur_bld;
            pb = planner.getPlannedBuilding(bld);
            filter_count = pb->getFilters().size();
            filter_idx = filter_count - 1;
        }
    }

    static void invalidateStatics()
    {
        bld = NULL;
    }

    bool handleInput(set<df::interface_key> *input)
    {
        if (!isInPlannedBuildingQueryMode() || Gui::inRenameBuilding())
            return false;

        initStatics();

        if (input->count(interface_key::SUSPENDBUILDING))
            return true; // Don't unsuspend planned buildings
        if (input->count(interface_key::DESTROYBUILDING))
        {
            // remove persistent data
            pb->remove();
            // still allow the building to be removed
            return false;
        }

        // ctrl+Right
        if (input->count(interface_key::A_MOVE_E_DOWN) && hasNextFilter())
            --filter_idx;
        // ctrl+Left
        else if (input->count(interface_key::A_MOVE_W_DOWN) && hasPrevFilter())
            ++filter_idx;
        else
            return false;
        return true;
    }

    DEFINE_VMETHOD_INTERPOSE(void, feed, (set<df::interface_key> *input))
    {
        if (!handleInput(input))
            INTERPOSE_NEXT(feed)(input);
    }

    static bool is_filter_satisfied(df::building *bld, int filter_idx)
    {
        if (!bld
                || bld->jobs.size() < 1
                || int(bld->jobs[0]->job_items.size()) <= filter_idx)
            return false;

        // if all items for this filter are attached, the quantity will be 0
        return bld->jobs[0]->job_items[filter_idx]->quantity == 0;
    }

    DEFINE_VMETHOD_INTERPOSE(void, render, ())
    {
        INTERPOSE_NEXT(render)();

        if (!isInPlannedBuildingQueryMode())
            return;

        initStatics();

        // Hide suspend toggle option
        auto dims = Gui::getDwarfmodeViewDims();
        int left_margin = dims.menu_x1 + 1;
        int x = left_margin;
        int y = 20;
        Screen::Pen pen(' ', COLOR_BLACK);
        Screen::fillRect(pen, x, y, dims.menu_x2, y);

        bool attached = is_filter_satisfied(pb->getBuilding(), filter_idx);

        auto & filter = pb->getFilters()[filter_idx];
        y = 24;
        std::string item_label =
            stl_sprintf("Item %d of %d (%s)", filter_count - filter_idx, filter_count, attached ? "attached" : "pending");
        OutputString(COLOR_WHITE, x, y, "Planned Building Filter", true, left_margin + 1);
        OutputString(COLOR_WHITE, x, y, item_label.c_str(), true, left_margin + 1);
        OutputString(COLOR_WHITE, x, y, get_item_label(toBuildingTypeKey(bld), filter_idx).c_str(), true, left_margin);
        ++y;
        if (item_can_be_improved(toBuildingTypeKey(bld), filter_idx))
        {
            OutputString(COLOR_BROWN, x, y, "Min Quality: ", false, left_margin);
            OutputString(COLOR_BLUE, x, y, filter.getMinQuality(), true, left_margin);
            OutputString(COLOR_BROWN, x, y, "Max Quality: ", false, left_margin);
            OutputString(COLOR_BLUE, x, y, filter.getMaxQuality(), true, left_margin);
            if (filter.getDecoratedOnly())
                OutputString(COLOR_BLUE, x, y, "Decorated Only", true, left_margin);
        }

        OutputString(COLOR_BROWN, x, y, "Materials:", true, left_margin);
        auto filters = filter.getMaterials();
        for (auto it = filters.begin(); it != filters.end(); ++it)
            OutputString(COLOR_BLUE, x, y, "*" + *it, true, left_margin);

        ++y;
        if (hasPrevFilter())
            OutputHotkeyString(x, y, "Prev Item", "Ctrl+Left", true, left_margin, COLOR_WHITE, COLOR_LIGHTRED);
        if (hasNextFilter())
            OutputHotkeyString(x, y, "Next Item", "Ctrl+Right", true, left_margin, COLOR_WHITE, COLOR_LIGHTRED);
    }
};

df::building * buildingplan_query_hook::bld;
PlannedBuilding * buildingplan_query_hook::pb;
int buildingplan_query_hook::filter_count;
int buildingplan_query_hook::filter_idx;

struct buildingplan_place_hook : public df::viewscreen_dwarfmodest
{
    typedef df::viewscreen_dwarfmodest interpose_base;

    // no non-static fields allowed (according to VTableInterpose.h)
    static BuildingTypeKey key;
    static std::vector<ItemFilter>::reverse_iterator filter_rbegin;
    static std::vector<ItemFilter>::reverse_iterator filter_rend;
    static std::vector<ItemFilter>::reverse_iterator filter;
    static int filter_count;
    static int filter_idx;

    bool hasNextFilter() const { return filter + 1 != filter_rend; }
    bool hasPrevFilter() const { return filter != filter_rbegin; }

    bool isInPlannedBuildingPlacementMode()
    {
        return ui->main.mode == ui_sidebar_mode::Build &&
            df::global::ui_build_selector &&
            df::global::ui_build_selector->stage < 2 &&
            planner.isPlannableBuilding(toBuildingTypeKey(ui_build_selector));
    }

    // reinit static fields when selected building type changes
    void initStatics()
    {
        BuildingTypeKey cur_key = toBuildingTypeKey(ui_build_selector);
        if (key != cur_key)
        {
            key = cur_key;
            auto wrapper = planner.getItemFilters(key);
            filter_rbegin = wrapper.rbegin();
            filter_rend = wrapper.rend();
            filter = filter_rbegin;
            filter_count = wrapper.get().size();
            filter_idx = filter_count - 1;
        }
    }

    static void invalidateStatics()
    {
        key = BuildingTypeKey();
    }

    bool handleInput(set<df::interface_key> *input)
    {
        if (!isInPlannedBuildingPlacementMode())
        {
            show_help = false;
            return false;
        }

        initStatics();

        if (in_dummy_screen)
        {
            if (input->count(interface_key::SELECT) || input->count(interface_key::SEC_SELECT)
                 || input->count(interface_key::LEAVESCREEN))
            {
                in_dummy_screen = false;
                // pass LEAVESCREEN up to parent view
                input->clear();
                input->insert(interface_key::LEAVESCREEN);
                return false;
            }
            return true;
        }

        if (input->count(interface_key::CUSTOM_P) ||
            input->count(interface_key::CUSTOM_G) ||
            input->count(interface_key::CUSTOM_D) ||
            input->count(interface_key::CUSTOM_Q) ||
            input->count(interface_key::CUSTOM_W) ||
            input->count(interface_key::CUSTOM_A) ||
            input->count(interface_key::CUSTOM_S) ||
            input->count(interface_key::CUSTOM_M))
        {
            show_help = true;
        }

        if (!quickfort_mode && !all_enabled
            && input->count(interface_key::CUSTOM_SHIFT_P))
        {
            planmode_enabled[key] = !planmode_enabled[key];
            if (!is_planmode_enabled(key))
                Gui::refreshSidebar();
            return true;
        }
        if (input->count(interface_key::CUSTOM_SHIFT_G))
        {
            show_global_settings_dialog();
            return true;
        }

        if (!is_planmode_enabled(key))
            return false;

        // if automaterial is enabled, let it handle building allocation and
        // registration with planner
        if (input->count(interface_key::SELECT) &&
            !is_automaterial_managed(ui_build_selector->building_type,
                                     ui_build_selector->building_subtype))
        {
            if (ui_build_selector->errors.size() == 0 && construct_planned_building())
            {
                Gui::refreshSidebar();
                if (quickfort_mode)
                    in_dummy_screen = true;
            }
            return true;
        }



        if (input->count(interface_key::CUSTOM_SHIFT_M))
            Screen::show(dts::make_unique<ViewscreenChooseMaterial>(*filter), plugin_self);

        if (item_can_be_improved(key, filter_idx))
        {
            if (input->count(interface_key::CUSTOM_SHIFT_Q))
                filter->decMinQuality();
            else if (input->count(interface_key::CUSTOM_SHIFT_W))
                filter->incMinQuality();
            else if (input->count(interface_key::CUSTOM_SHIFT_A))
                filter->decMaxQuality();
            else if (input->count(interface_key::CUSTOM_SHIFT_S))
                filter->incMaxQuality();
            else if (input->count(interface_key::CUSTOM_SHIFT_D))
                filter->toggleDecoratedOnly();
        }

        // ctrl+Right
        if (input->count(interface_key::A_MOVE_E_DOWN) && hasNextFilter())
        {
            ++filter;
            --filter_idx;
        }
        // ctrl+Left
        else if (input->count(interface_key::A_MOVE_W_DOWN) && hasPrevFilter())
        {
            --filter;
            ++filter_idx;
        }
        else
            return false;
        return true;
    }

    DEFINE_VMETHOD_INTERPOSE(void, feed, (set<df::interface_key> *input))
    {
        if (!handleInput(input))
            INTERPOSE_NEXT(feed)(input);
    }

    DEFINE_VMETHOD_INTERPOSE(void, render, ())
    {
        bool plannable = isInPlannedBuildingPlacementMode();
        if (plannable && is_planmode_enabled(key))
        {
            if (ui_build_selector->stage < 1)
                // No materials but turn on cursor
                ui_build_selector->stage = 1;

            for (auto iter = ui_build_selector->errors.begin();
                 iter != ui_build_selector->errors.end();)
            {
                // FIXME Hide bags
                if (((*iter)->find("Needs") != string::npos
                     && **iter != "Needs adjacent wall")
                    || (*iter)->find("No access") != string::npos)
                    iter = ui_build_selector->errors.erase(iter);
                else
                    ++iter;
            }
        }

        INTERPOSE_NEXT(render)();

        if (!plannable)
            return;

        initStatics();

        auto dims = Gui::getDwarfmodeViewDims();
        int left_margin = dims.menu_x1 + 1;
        int x = left_margin;

        if (in_dummy_screen)
        {
            Screen::Pen pen(' ',COLOR_BLACK);
            int y = dims.y1 + 1;
            Screen::fillRect(pen, x, y, dims.menu_x2, y + 20);

            ++y;

            OutputString(COLOR_BROWN, x, y,
                "Placeholder for legacy Quickfort. This screen is not required for DFHack native quickfort.",
                true, left_margin);
            OutputString(COLOR_WHITE, x, y, "Enter, Shift-Enter or Esc", true, left_margin);
            return;
        }

        int y = 23;

        if (is_automaterial_managed(ui_build_selector->building_type,
                                    ui_build_selector->building_subtype))
        {
            // avoid conflict with the automaterial plugin UI
            y = 36;
        }

        if (show_help)
        {
            OutputString(COLOR_BROWN, x, y, "Note: ");
            OutputString(COLOR_WHITE, x, y, "Use Shift-Keys here", true, left_margin);
        }

        OutputHotkeyString(x, y, "Planning Mode", interface_key::CUSTOM_SHIFT_P);
        OutputString(COLOR_WHITE, x, y, ": ");
        if (quickfort_mode)
            OutputString(COLOR_YELLOW, x, y, "Quickfort", true, left_margin);
        else if (all_enabled)
            OutputString(COLOR_YELLOW, x, y, "All", true, left_margin);
        else if (planmode_enabled[key])
            OutputString(COLOR_GREEN, x, y, "On", true, left_margin);
        else
            OutputString(COLOR_GREY, x, y, "Off", true, left_margin);
        OutputHotkeyString(x, y, "Global Settings", interface_key::CUSTOM_SHIFT_G,
            true, left_margin, COLOR_WHITE, COLOR_LIGHTRED);

        if (!is_planmode_enabled(key))
            return;

        y += 2;
        std::string title =
            stl_sprintf("Filter for Item %d of %d:",
                        filter_count - filter_idx, filter_count);
        OutputString(COLOR_WHITE, x, y, title.c_str(), true, left_margin + 1);
        OutputString(COLOR_WHITE, x, y, get_item_label(key, filter_idx).c_str(), true, left_margin);

        if (item_can_be_improved(key, filter_idx))
        {
            OutputHotkeyString(x, y, "Min Quality: ", "QW", false, 0, COLOR_WHITE, COLOR_LIGHTRED);
            OutputString(COLOR_BROWN, x, y, filter->getMinQuality(), true, left_margin);

            OutputHotkeyString(x, y, "Max Quality: ", "AS", false, 0, COLOR_WHITE, COLOR_LIGHTRED);
            OutputString(COLOR_BROWN, x, y, filter->getMaxQuality(), true, left_margin);

            OutputToggleString(x, y, "Decorated Only", interface_key::CUSTOM_SHIFT_D,
                filter->getDecoratedOnly(), true, left_margin, COLOR_WHITE, COLOR_LIGHTRED);
        }

        OutputHotkeyString(x, y, "Material Filter:", interface_key::CUSTOM_SHIFT_M, true,
            left_margin, COLOR_WHITE, COLOR_LIGHTRED);
        auto filter_descriptions = filter->getMaterials();
        for (auto it = filter_descriptions.begin();
             it != filter_descriptions.end(); ++it)
             OutputString(COLOR_BROWN, x, y, "   *" + *it, true, left_margin);

        y += 2;
        if (hasPrevFilter())
            OutputHotkeyString(x, y, "Prev Item", "Ctrl+Left", true,
                left_margin, COLOR_WHITE, COLOR_LIGHTRED);
        if (hasNextFilter())
            OutputHotkeyString(x, y, "Next Item", "Ctrl+Right", true,
                left_margin, COLOR_WHITE, COLOR_LIGHTRED);
    }
};

BuildingTypeKey buildingplan_place_hook::key;
std::vector<ItemFilter>::reverse_iterator buildingplan_place_hook::filter_rbegin;
std::vector<ItemFilter>::reverse_iterator buildingplan_place_hook::filter_rend;
std::vector<ItemFilter>::reverse_iterator buildingplan_place_hook::filter;
int buildingplan_place_hook::filter_count;
int buildingplan_place_hook::filter_idx;

struct buildingplan_room_hook : public df::viewscreen_dwarfmodest
{
    typedef df::viewscreen_dwarfmodest interpose_base;

    std::vector<Units::NoblePosition> getNoblePositionOfSelectedBuildingOwner()
    {
        std::vector<Units::NoblePosition> np;
        if (ui->main.mode != df::ui_sidebar_mode::QueryBuilding ||
            !world->selected_building ||
            !world->selected_building->owner)
        {
            return np;
        }

        switch (world->selected_building->getType())
        {
        case building_type::Bed:
        case building_type::Chair:
        case building_type::Table:
            break;
        default:
            return np;
        }

        return getUniqueNoblePositions(world->selected_building->owner);
    }

    bool isInNobleRoomQueryMode()
    {
        if (getNoblePositionOfSelectedBuildingOwner().size() > 0)
            return canReserveRoom(world->selected_building);
        else
            return false;
    }

    bool handleInput(set<df::interface_key> *input)
    {
        if (!isInNobleRoomQueryMode())
            return false;

        if (Gui::inRenameBuilding())
            return false;
        auto np = getNoblePositionOfSelectedBuildingOwner();
        df::interface_key last_token = get_string_key(input);
        if (last_token >= Screen::charToKey('1')
            && last_token <= Screen::charToKey('9'))
        {
            size_t index = last_token - Screen::charToKey('1');
            if (index >= np.size())
                return false;
            roomMonitor.toggleRoomForPosition(world->selected_building->id, np.at(index).position->code);
            return true;
        }

        return false;
    }

    DEFINE_VMETHOD_INTERPOSE(void, feed, (set<df::interface_key> *input))
    {
        if (!handleInput(input))
            INTERPOSE_NEXT(feed)(input);
    }

    DEFINE_VMETHOD_INTERPOSE(void, render, ())
    {
        INTERPOSE_NEXT(render)();

        if (!isInNobleRoomQueryMode())
            return;

        auto np = getNoblePositionOfSelectedBuildingOwner();
        auto dims = Gui::getDwarfmodeViewDims();
        int left_margin = dims.menu_x1 + 1;
        int x = left_margin;
        int y = 24;
        OutputString(COLOR_BROWN, x, y, "DFHack", true, left_margin);
        OutputString(COLOR_WHITE, x, y, "Auto-allocate to:", true, left_margin);
        for (size_t i = 0; i < np.size() && i < 9; i++)
        {
            bool enabled =
                roomMonitor.getReservedNobleCode(world->selected_building->id)
                == np[i].position->code;
            OutputToggleString(x, y, np[i].position->name[0].c_str(),
                int_to_string(i+1).c_str(), enabled, true, left_margin);
        }
    }
};

IMPLEMENT_VMETHOD_INTERPOSE(buildingplan_query_hook, feed);
IMPLEMENT_VMETHOD_INTERPOSE(buildingplan_place_hook, feed);
IMPLEMENT_VMETHOD_INTERPOSE(buildingplan_room_hook, feed);
IMPLEMENT_VMETHOD_INTERPOSE(buildingplan_query_hook, render);
IMPLEMENT_VMETHOD_INTERPOSE(buildingplan_place_hook, render);
IMPLEMENT_VMETHOD_INTERPOSE(buildingplan_room_hook, render);

DFHACK_PLUGIN_IS_ENABLED(is_enabled);

static bool setSetting(std::string name, bool value);

static bool isTrue(std::string val)
{
    val = toLower(val);
    return val == "on" || val == "true" || val == "y" || val == "yes"
        || val == "1";
}

static command_result buildingplan_cmd(color_ostream &out, vector <string> & parameters)
{
    if (parameters.empty())
        return CR_OK;

    std::string cmd = toLower(parameters[0]);

    if (cmd.size() >= 1 && cmd[0] == 'v')
    {
        out.print("buildingplan version: %s\n", PLUGIN_VERSION);
    }
    else if (parameters.size() >= 2 && cmd == "debug")
    {
        show_debugging = isTrue(parameters[1]);
        out.print("buildingplan debugging: %s\n",
                  show_debugging ? "enabled" : "disabled");
    }
    else if (cmd == "set")
    {
        if (!is_enabled)
        {
            out.printerr(
                "ERROR: buildingplan must be enabled before you can"
                " read or set buildingplan global settings.");
            return CR_FAILURE;
        }

        if (!DFHack::Core::getInstance().isMapLoaded())
        {
            out.printerr(
                "ERROR: A map must be loaded before you can read or set"
                "buildingplan global settings. Try adding your"
                "'buildingplan set' commands to the onMapLoad.init file.\n");
            return CR_FAILURE;
        }

        if (parameters.size() == 1)
        {
            // display current settings
            out.print("active settings:\n");

            out.print("  all_enabled = %s\n", all_enabled ? "true" : "false");
            for (auto & setting : planner.getGlobalSettings())
            {
                out.print("  %s = %s\n", setting.first.c_str(),
                          setting.second ? "true" : "false");
            }

            out.print("  quickfort_mode = %s\n",
                      quickfort_mode ? "true" : "false");
        }
        else if (parameters.size() == 3)
        {
            // set a setting
            std::string setting = toLower(parameters[1]);
            bool val = isTrue(parameters[2]);
            if (!setSetting(setting, val))
            {
                out.printerr("ERROR: invalid parameter: '%s'\n",
                    parameters[1].c_str());
            }
        }
        else
        {
            out.printerr("ERROR: invalid syntax\n");
        }
    }

    return CR_OK;
}

DFhackCExport command_result plugin_enable(color_ostream &out, bool enable)
{
    if (!gps)
        return CR_FAILURE;

    if (enable != is_enabled)
    {
        if (DFHack::Core::getInstance().isMapLoaded())
            planner.reset();

        if (!INTERPOSE_HOOK(buildingplan_query_hook, feed).apply(enable) ||
            !INTERPOSE_HOOK(buildingplan_place_hook, feed).apply(enable) ||
            !INTERPOSE_HOOK(buildingplan_room_hook, feed).apply(enable) ||
            !INTERPOSE_HOOK(buildingplan_query_hook, render).apply(enable) ||
            !INTERPOSE_HOOK(buildingplan_place_hook, render).apply(enable) ||
            !INTERPOSE_HOOK(buildingplan_room_hook, render).apply(enable))
            return CR_FAILURE;

        is_enabled = enable;
    }

    return CR_OK;
}

DFhackCExport command_result plugin_init ( color_ostream &out, std::vector <PluginCommand> &commands)
{
    commands.push_back(
        PluginCommand(
        "buildingplan", "Plan building construction before you have materials",
        buildingplan_cmd, false, "Run 'buildingplan debug [on|off]' to toggle debugging, or 'buildingplan version' to query the plugin version."));

    return CR_OK;
}

DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event)
{
    switch (event) {
    case SC_MAP_LOADED:
        buildingplan_place_hook::invalidateStatics();
        buildingplan_query_hook::invalidateStatics();
        planner.reset();
        roomMonitor.reset(out);
        break;
    default:
        break;
    }

    return CR_OK;
}

static bool is_paused()
{
    return World::ReadPauseState() ||
        ui->main.mode > df::ui_sidebar_mode::Squads ||
        !strict_virtual_cast<df::viewscreen_dwarfmodest>(Gui::getCurViewscreen(true));
}

static bool cycle_requested = false;

#define DAY_TICKS 1200
DFhackCExport command_result plugin_onupdate(color_ostream &)
{
    if (Maps::IsValid() && !is_paused()
        && (cycle_requested || world->frame_counter % (DAY_TICKS/2) == 0))
    {
        planner.doCycle();
        roomMonitor.doCycle();
        cycle_requested = false;
    }

    return CR_OK;
}

DFhackCExport command_result plugin_shutdown(color_ostream &)
{
    return CR_OK;
}

// Lua API section

static bool isPlanModeEnabled(df::building_type type,
                              int16_t subtype,
                              int32_t custom) {
    return is_planmode_enabled(toBuildingTypeKey(type, subtype, custom));
}

static bool isPlannableBuilding(df::building_type type,
                                int16_t subtype,
                                int32_t custom) {
    return planner.isPlannableBuilding(
        toBuildingTypeKey(type, subtype, custom));
}

static bool isPlannedBuilding(df::building *bld) {
    return !!planner.getPlannedBuilding(bld);
}

static void addPlannedBuilding(df::building *bld) {
    planner.addPlannedBuilding(bld);
}

static void doCycle() {
    planner.doCycle();
}

static void scheduleCycle() {
    cycle_requested = true;
}

static bool setSetting(std::string name, bool value) {
    if (name == "quickfort_mode")
    {
        debug("setting quickfort_mode %d -> %d", quickfort_mode, value);
        quickfort_mode = value;
        return true;
    }
    if (name == "all_enabled")
    {
        debug("setting all_enabled %d -> %d", all_enabled, value);
        all_enabled = value;
        return true;
    }
    return planner.setGlobalSetting(name, value);
}

DFHACK_PLUGIN_LUA_FUNCTIONS {
    DFHACK_LUA_FUNCTION(isPlanModeEnabled),
    DFHACK_LUA_FUNCTION(isPlannableBuilding),
    DFHACK_LUA_FUNCTION(isPlannedBuilding),
    DFHACK_LUA_FUNCTION(addPlannedBuilding),
    DFHACK_LUA_FUNCTION(doCycle),
    DFHACK_LUA_FUNCTION(scheduleCycle),
    DFHACK_LUA_FUNCTION(setSetting),
    DFHACK_LUA_END
};
