// cursecheck plugin
//
// check single tile or whole map/world for cursed creatures by checking if a valid curse date (!=-1) is set
// if a cursor is active only the selected tile will be observed
// without cursor the whole map will be checked
// by default cursed creatures will be only counted
//
// the tool was intended to help finding vampires but it will also list necromancers, werebeasts and zombies
//
// Options:
//   detail  - print full name, date of birth, date of curse (vamp might use fake identity, though)
//             show if the creature is active or dead, missing, ghostly (ghost vampires should not exist in 34.05)
//             identify type of curse (works fine for vanilla ghosts, vamps, werebeasts, zombies and necromancers)
//   nick    - set nickname to type of curse(does not always show up ingame, some vamps don't like nicknames)
//   all     - don't ignore dead and inactive creatures (former ghosts, dead necromancers, ...)
//   verbose - acts like detail but also lists all curse tags (if you want to know it all).

#include <iostream>
#include <iomanip>
#include <climits>
#include <vector>
#include <string>
#include <sstream>
#include <ctime>
#include <cstdio>
using namespace std;

#include "Core.h"
#include "Console.h"
#include "PluginManager.h"
#include "modules/Units.h"
#include <modules/Translation.h>
#include "modules/Gui.h"
#include "MiscUtils.h"

#include "df/unit.h"
#include "df/unit_soul.h"
#include "df/unit_syndrome.h"
#include "df/historical_entity.h"
#include "df/historical_figure.h"
#include "df/historical_figure_info.h"
#include "df/identity.h"
#include "df/language_name.h"
#include "df/syndrome.h"
#include "df/world.h"
#include "df/world_raws.h"
#include "df/incident.h"

using std::vector;
using std::string;
using namespace DFHack;
using namespace df::enums;

DFHACK_PLUGIN("cursecheck");
REQUIRE_GLOBAL(world);
REQUIRE_GLOBAL(cursor);

enum class curses : int8_t {
    None,
    Unknown,
    Ghost,
    Zombie,
    Necromancer,
    Werebeast,
    Vampire
};

std::vector<string> curse_names = {
    "none",
    "unknown",
    "ghost",
    "zombie",
    "necromancer",
    "werebeast",
    "vampire"
};

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

DFhackCExport command_result plugin_init ( color_ostream &out, std::vector <PluginCommand> &commands)
{
    commands.push_back(PluginCommand(
        "cursecheck",
        "Check for cursed creatures (undead, necromancers...)",
        cursecheck));
    return CR_OK;
}

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

curses determineCurse(df::unit * unit)
{
    curses cursetype = curses::None;

    if (unit->curse_year != -1)
    {
        cursetype = curses::Unknown;
    }

    // ghosts: ghostly, duh
    // as of DF 34.05 and higher vampire ghosts and the like should not be possible
    // if they get reintroduced later it will become necessary to watch 'ghostly' seperately
    if(unit->flags3.bits.ghostly)
        cursetype = curses::Ghost;

    // zombies: undead or hate life (according to ag), not bloodsuckers
    if( (unit->curse.add_tags1.bits.OPPOSED_TO_LIFE || unit->curse.add_tags1.bits.NOT_LIVING)
        && !unit->curse.add_tags1.bits.BLOODSUCKER )
        cursetype = curses::Zombie;

    // necromancers: alive, don't eat, don't drink, don't age
    if(!unit->curse.add_tags1.bits.NOT_LIVING
        && unit->curse.add_tags1.bits.NO_EAT
        && unit->curse.add_tags1.bits.NO_DRINK
        && unit->curse.add_tags2.bits.NO_AGING
        )
        cursetype = curses::Necromancer;

    // werecreatures: subjected to a were syndrome. The curse effects are active only when
    // in were form.
    for (size_t i = 0; i < unit->syndromes.active.size(); i++)
    {
        for (size_t k = 0; k < world->raws.syndromes.all[unit->syndromes.active[i]->type]->syn_class.size(); k++)
        {
            if (strcmp (world->raws.syndromes.all[unit->syndromes.active[i]->type]->syn_class[k]->c_str(), "WERECURSE") == 0)
            {
                cursetype = curses::Werebeast;
                break;
            }
        }
    }

    // vampires: bloodsucker (obvious enough)
    if(unit->curse.add_tags1.bits.BLOODSUCKER)
        cursetype = curses::Vampire;

    return cursetype;
}

command_result cursecheck (color_ostream &out, vector <string> & parameters)
{
    CoreSuspender suspend;
    int32_t cursorX, cursorY, cursorZ;
    Gui::getCursorCoords(cursorX,cursorY,cursorZ);

    bool giveDetails = false;
    bool giveUnitID = false;
    bool giveNick = false;
    bool ignoreDead = true;
    bool verbose = false;
    size_t cursecount = 0;

    for(size_t i = 0; i < parameters.size();i++)
    {
        if(parameters[i] == "help" || parameters[i] == "?")
        {
            out.print(  "  Search for cursed creatures (ghosts, vampires, necromancers, zombies, werebeasts).\n"
                        "  With map cursor active only the current tile will be checked.\n"
                        "  Without an in-game cursor the whole map/world will be scanned.\n"
                        "  By default cursed creatures are only counted to make it more interesting.\n"
                        "  By default dead and passive creatures (aka really dead) are ignored.\n"
                        "Options:\n"
                        "  detail  - show details (name and age shown ingame might differ)\n"
                        "  ids     - add creature and race IDs to be show on the  output\n"
                        "  nick    - try to set cursetype as nickname (does not always work)\n"
                        "  all     - include dead and passive creatures\n"
                        "  verbose - show all curse tags (if you really want to know it all)\n"
                        );
            return CR_OK;
        }
        if(parameters[i] == "detail")
            giveDetails = true;
        if(parameters[i] == "ids")
            giveUnitID = true;
        if(parameters[i] == "nick")
            giveNick = true;
        if(parameters[i] == "all")
            ignoreDead = false;
        if(parameters[i] == "verbose")
        {
            // verbose makes no sense without enabling details
            giveDetails = true;
            verbose = true;
        }
    }

    // check whole map if no cursor is active
    bool checkWholeMap = false;
    if(cursorX == -30000)
    {
        out.print("No cursor; will check all units on the map.\n");
        checkWholeMap = true;
    }

    for(size_t i = 0; i < world->units.all.size(); i++)
    {
        df::unit * unit = world->units.all[i];

        // filter out all "living" units that are currently removed from play
        // don't spam all completely dead creatures if not explicitly wanted
        if((!Units::isActive(unit) && !Units::isKilled(unit)) || (Units::isKilled(unit) && ignoreDead))
        {
            continue;
        }

        // bail out if we have a map cursor and creature is not at that specific position
        if ( !checkWholeMap && (unit->pos.x != cursorX || unit->pos.y != cursorY || unit->pos.z != cursorZ) )
        {
            continue;
        }

        curses cursetype = determineCurse(unit);

        if (cursetype != curses::None)
        {
             cursecount++;

            if(giveNick)
            {
                Units::setNickname(unit, curse_names[static_cast<size_t>(cursetype)].c_str()); //"CURSED");
            }

            if (giveDetails)
            {
                if (unit->name.has_name)
                {
                    string firstname = unit->name.first_name;
                    string restofname = Translation::TranslateName(&unit->name, false);
                    firstname[0] = toupper(firstname[0]);

                    // if creature has no nickname, restofname will already contain firstname
                    // no need for double output
                    if (restofname.compare(0, firstname.length(), firstname) != 0)
                        out.print("%s ", firstname.c_str());
                    out.print("%s ", restofname.c_str());
                }
                else
                {
                    // happens with unnamed zombies and resurrected body parts
                    out.print("Unnamed creature ");
                }

                auto death = df::incident::find(unit->counters.death_id);
                bool missing = false;
                if (death && !death->flags.bits.discovered)
                {
                    missing = true;
                }

                out.print("born in %d, cursed in %d to be a %s. (%s%s%s)\n",
                    unit->birth_year,
                    unit->curse_year,
                    curse_names [static_cast<size_t>(cursetype)].c_str(),
                    // technically most cursed creatures are undead,
                    // therefore output 'active' if they are not completely dead
                    unit->flags2.bits.killed ? "deceased" : "active",
                    unit->flags3.bits.ghostly ? "-ghostly" : "",
                    missing ? "-missing" : ""
                );

                if (missing)
                {
                    out.print("- You can use 'tweak clear-missing' to allow engraving a memorial easier.\n");
                }

                // dump all curse flags on demand
                if (verbose)
                {
                    out << "Curse flags: "
                        << bitfield_to_string(unit->curse.add_tags1) << endl
                        << bitfield_to_string(unit->curse.add_tags2) << endl;
                }
            }

            if (giveUnitID)
            {
                out.print("Creature %d, race %d (%x)\n", unit->id, unit->race, unit->race);
            }
        }
    }

    if (checkWholeMap)
        out.print("Number of cursed creatures on map: %zd \n", cursecount);
    else
        out.print("Number of cursed creatures on tile: %zd \n", cursecount);

    return CR_OK;
}