// changeitem plugin
// allows to change the material type and quality of selected items
#include <iostream>
#include <iomanip>
#include <sstream>
#include <climits>
#include <vector>
#include <string>
#include <algorithm>
#include <set>
using namespace std;

#include "Core.h"
#include "Console.h"
#include "Export.h"
#include "PluginManager.h"
#include "modules/Maps.h"
#include "modules/Gui.h"
#include "modules/Items.h"
#include "modules/Materials.h"
#include "modules/MapCache.h"

#include "DataDefs.h"
#include "df/item.h"
#include "df/itemdef.h"
#include "df/world.h"
#include "df/general_ref.h"

using namespace DFHack;
using namespace df::enums;

using MapExtras::Block;
using MapExtras::MapCache;

DFHACK_PLUGIN("changeitem");
REQUIRE_GLOBAL(world);

command_result df_changeitem(color_ostream &out, vector <string> & parameters);

DFhackCExport command_result plugin_init ( color_ostream &out, vector <PluginCommand> &commands)
{
    commands.push_back(PluginCommand(
        "changeitem",
        "Change item attributes (material, quality).",
        df_changeitem));

    return CR_OK;
}

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

// probably there is some method in the library which does the same
// todo: look for it :)
string describeQuality(int q)
{
    switch(q)
    {
    case 0:
        return "Basic";
    case 1:
        return "-Well-crafted-";
    case 2:
        return "+Finely-crafted+";
    case 3:
        return "*Superior quality*";
    case 4:
        return "#Exceptional#";
    case 5:
        return "$Masterful$";
    default:
        return "!INVALID!";
    }
}

command_result changeitem_execute(
    color_ostream &out, df::item * item,
    bool info, bool force,
    bool change_material, string new_material,
    bool change_quality, int new_quality,
    bool change_subtype, string new_subtype);

command_result df_changeitem(color_ostream &out, vector <string> & parameters)
{
    CoreSuspender suspend;

    bool here = false;
    bool info = false;
    bool force = false;

    bool change_material = false;
    string new_material = "none";

    bool change_subtype = false;
    string new_subtype = "NONE";

    bool change_quality = false;
    int new_quality = 0;

    for (size_t i = 0; i < parameters.size(); i++)
    {
        string & p = parameters[i];

        if (p == "help" || p == "?")
        {
            return CR_WRONG_USAGE;
        }
        else if (p == "here")
        {
            here = true;
        }
        else if (p == "info")
        {
            info = true;
        }
        else if (p == "force")
        {
            force = true;
        }
        else if (p == "material" || p == "m" )
        {
            // must be followed by material RAW id
            // (string like 'INORGANIC:GRANITE', 'PLANT:MAPLE:WOOD', ...)
            if(i == parameters.size()-1)
            {
                out.printerr("no material specified!\n");
                return CR_WRONG_USAGE;
            }
            change_material = true;
            new_material = parameters[i+1];
            i++;
        }
        else if (p == "quality" || p == "q")
        {
            // must be followed by numeric quality (allowed: 0-5)
            if(i == parameters.size()-1)
            {
                out.printerr("no quality specified!\n");
                return CR_WRONG_USAGE;
            }
            string & q = parameters[i+1];
            // meh. should use a stringstream instead. but it's only 6 numbers
            if(q == "0")
                new_quality = 0;
            else if(q == "1")
                new_quality = 1;
            else if(q == "2")
                new_quality = 2;
            else if(q == "3")
                new_quality = 3;
            else if(q == "4")
                new_quality = 4;
            else if(q == "5")
                new_quality = 5;
            else
            {
                out << "Invalid quality specified!" << endl;
                return CR_WRONG_USAGE;
            }
            out << "change to quality: " << describeQuality(new_quality) << endl;
            change_quality = true;
            i++;
        }
        else if (p == "subtype" || p == "s" )
        {
            // must be followed by subtype RAW id
            // (string like 'ITEM_GLOVES_GAUNTLETS', 'ITEM_WEAPON_DAGGER_LARGE', 'ITEM_TOOL_KNIFE_MEAT_CLEAVER', ...)
            if(i == parameters.size()-1)
            {
                out.printerr("no subtype specified!\n");
                return CR_WRONG_USAGE;
            }
            change_subtype = true;
            new_subtype = parameters[i+1];
            i++;
        }
        else
        {
            out << p << ": Unknown command!" << endl;
            return CR_WRONG_USAGE;
        }
    }

    if (!Maps::IsValid())
    {
        out.printerr("Map is not available!\n");
        return CR_FAILURE;
    }

    MaterialInfo mat_new;
    if (change_material && !mat_new.find(new_material))
    {
        out.printerr("No such material!\n");
        return CR_FAILURE;
    }

    if (here)
    {
        int processed_total = 0;
        int cx, cy, cz;
        DFCoord pos_cursor;

        // needs a cursor
        if (!Gui::getCursorCoords(cx,cy,cz))
        {
            out.printerr("Cursor position not found. Please enable the cursor.\n");
            return CR_FAILURE;
        }
        pos_cursor = DFCoord(cx,cy,cz);

        // uh. is this check necessary?
        // changeitem doesn't do stuff with map blocks...
        {
            MapCache MC;
            Block * b = MC.BlockAt(pos_cursor / 16);
            if(!b)
            {
                out.printerr("Cursor is in an invalid/uninitialized area. Place it over a floor.\n");
                return CR_FAILURE;
            }
            // when only changing material it doesn't matter if cursor is over a tile
            //df::tiletype ttype = MC.tiletypeAt(pos_cursor);
            //if(!DFHack::isFloorTerrain(ttype))
            //{
            //    out.printerr("Cursor should be placed over a floor.\n");
            //    return CR_FAILURE;
            //}
        }

        // iterate over all items, process those where pos = pos_cursor
        size_t numItems = world->items.all.size();
        for(size_t i=0; i< numItems; i++)
        {
            df::item * item = world->items.all[i];
            DFCoord pos_item(item->pos.x, item->pos.y, item->pos.z);

            if (pos_item != pos_cursor)
                continue;

            changeitem_execute(out, item, info, force, change_material, new_material, change_quality, new_quality, change_subtype, new_subtype);
            processed_total++;
        }
        out.print("Done. %d items processed.\n", processed_total);
    }
    else
    {
        // needs a selected item
        df::item *item = Gui::getSelectedItem(out);
        if (!item)
        {
            out.printerr("No item selected.\n");
            return CR_FAILURE;
        }
        changeitem_execute(out, item, info, force, change_material, new_material, change_quality, new_quality, change_subtype, new_subtype);
    }
    return CR_OK;
}

command_result changeitem_execute(
    color_ostream &out, df::item * item,
    bool info, bool force,
    bool change_material, string new_material,
    bool change_quality, int new_quality,
    bool change_subtype, string new_subtype )
{
    MaterialInfo mat_new;
    MaterialInfo mat_old;

    ItemTypeInfo sub_old;
    ItemTypeInfo sub_new;
    int new_subtype_id = -1;

    if(change_material)
        mat_new.find(new_material);
    if(change_material || info)
        mat_old.decode(item);

    if(change_subtype || info)
        sub_old.decode(item);
    if(change_subtype)
    {
        string new_type = ENUM_KEY_STR(item_type, item->getType()) + ":" + new_subtype;
        if (new_subtype == "NONE")
            new_subtype_id = -1;
        else if (sub_new.find(new_type))
            new_subtype_id = sub_new.subtype;
        else
        {
//          out.printerr("Invalid subtype for selected item, skipping\n");
            return CR_FAILURE;
        }
    }

    // print some info, don't change stuff
    if(info)
    {
        out << "Item info: " << endl;
        out << "  type:    " << ENUM_KEY_STR(item_type, item->getType()) << endl;
        out << "  subtype: " << (sub_old.custom ? sub_old.custom->id : "NONE") << endl;
        out << "  quality: " << describeQuality(item->getQuality()) << endl;
        //if(item->isImproved())
        //    out << "  imp.quality: " << describeQuality(item->getImprovementQuality()) << endl;
        out << "  material: " << mat_old.getToken() << endl;
        return CR_OK;
    }

    if(change_quality)
    {
        item->setQuality(new_quality);
        // it would be nice to be able to change the improved quality, too
        // (only allowed if the item is already improved)
        // but there is no method in item.h which supports that
        // ok: hints from _Q/angavrilov: improvent is a vector, an item can have more than one improvement
        // -> virtual_cast to item_constructedst
    }

    if(change_material)
    {
        // subtype and mode should match to avoid doing dumb stuff like changing boulders into meat whatever
        // changing a stone cabinet to wood is fine, though. as well as lots of other combinations.
        // still, it's better to make the user activate 'force' if he really wants to.

        // fixme: changing material of cloth items needs more work...
        // <_Q> cloth items have a "CLOTH" improvement which tells you about the cloth that was used to make it

        if(force||(mat_old.subtype == mat_new.subtype && mat_old.mode==mat_new.mode))
        {
            item->setMaterial(mat_new.type);
            item->setMaterialIndex(mat_new.index);
        }
        else
        {
            out.printerr("change denied: subtype doesn't match. use 'force' to override.\n");
        }

        item->flags.bits.temps_computed = 0;              // recalc temperatures next time touched
        item->flags.bits.weight_computed = 0;   // recalc weight next time touched
    }

    if(change_subtype)
    {
        item->setSubtype(new_subtype_id);
        item->flags.bits.weight_computed = 0;   // recalc weight next time touched
    }
    return CR_OK;
}