/*** Monster information.
 * @module monster
 */
#include "AppHdr.h"

#include "l-libs.h"

#include <algorithm>

#include "cluautil.h"
#include "coord.h"
#include "env.h"
#include "l-defs.h"
#include "libutil.h" // map_find
#include "mon-book.h"
#include "mon-pick.h"
#include "spl-util.h"
#include "stringutil.h"
#include "transform.h"
#include "math.h" // ceil
#include "spl-zap.h" // calc_spell_power
#include "evoke.h" // wand_mp_cost
#if TAG_MAJOR_VERSION == 34
#include "god-abil.h" // pakellas_effective_hex_power
#endif
#include "describe.h" // describe_info, get_monster_db_desc

#define MONINF_METATABLE "monster.info"

void lua_push_moninf(lua_State *ls, monster_info *mi)
{
    monster_info **miref =
        clua_new_userdata<monster_info *>(ls, MONINF_METATABLE);
    *miref = new monster_info(*mi);
}

#define MONINF(ls, n, var) \
    monster_info *var = *(monster_info **) \
        luaL_checkudata(ls, n, MONINF_METATABLE); \
    if (!var) \
        return 0

#define MIRET1(type, field, cfield) \
    static int moninf_get_##field(lua_State *ls) \
    { \
        MONINF(ls, 1, mi); \
        lua_push##type(ls, mi->cfield); \
        return 1; \
    }

#define MIREG(field) { #field, moninf_get_##field }

/*** Monster information class.
 * @type monster.info
 */
/*** How hurt is this monster?
 * Numeric representation of the level of damage sustained by the monster.
 * Return value ranges from 0 (full HP) to 6 (dead).
 * @treturn boolean
 * @function damage_level
 */
MIRET1(number, damage_level, dam)
/*** Is this monster safe by default?
 * Check if this monster is thought of as safe by crawl internally. Does not
 * check @{Hooks.ch_mon_is_safe}, so this can be used there without causing an
 * infinite loop.
 * @treturn boolean
 * @function is_safe
 */
MIRET1(boolean, is_safe, is(MB_SAFE))
/*** Is this monster firewood.
 * Plants and fungi. Immobile things that give no exp.
 * @treturn boolean
 * @function is_firewood
 */
MIRET1(boolean, is_firewood, is(MB_FIREWOOD))
/*** The monster's current attitude.
 * A numerical value representing the monster's attitude. Possible values:
 *
 * - 0 hostile
 * - 1 neutral
 * - 2 strict neutral (neutral but won't attack the player)
 * - 3 good neutral (neutral but won't attack friendlies)
 * - 4 friendly (created friendly, not enslavement)
 *
 * @treturn int
 * @function attitude
 */
MIRET1(number, attitude, attitude)
/*** The monster's threat level.
 * A numeric representation of the the threat level in the monster list.
 *
 * - 0 dark grey threat (trivial)
 * - 1 light grey threat (easy)
 * - 2 yellow threat (dangerous)
 * - 3 red threat (extremely dangerous)
 *
 * @treturn int
 * @function threat
 */
MIRET1(number, threat, threat)
/*** Simple monster name.
 * Returns the name of the monster.
 * @treturn string
 * @see name
 * @function mname
 */
MIRET1(string, mname, mname.c_str())
/*** Monster type enum value as in monster_type.h.
 * @treturn int
 * @function type
 */
MIRET1(number, type, type)
/*** Monster base type as in monster_type.h.
 * @treturn int
 * @function base_type
 */
MIRET1(number, base_type, base_type)
/*** Monster number field.
 * Contains hydra heads or slime size. Meaningless for all others.
 * @treturn int
 * @function number
 */
MIRET1(number, number, number)
/*** Does this monster have a ranged attack we know about?
 * This refers to ranged weapons. Ranged spells and abilities are not included
 * in this check.
 * @treturn boolean
 * @function has_ranged_attack
 */
MIRET1(boolean, has_known_ranged_attack, is(MB_RANGED_ATTACK))
/*** A string describing monster speed.
 * Possible values are: "very slow", "slow", "normal", "fast", "very fast", and
 * "extremely fast".
 * @treturn string
 * @function speed_description
 */
MIRET1(string, speed_description, speed_description().c_str())
/*** The monster's x coordinate in player centered coordinates.
 * @treturn int
 * @function x_pos
 */
MIRET1(number, x_pos, pos.x - you.pos().x)
/*** The monster's y coordinate in player centered coordinates.
 * @treturn int
 * @function y_pos
 */
MIRET1(number, y_pos, pos.y - you.pos().y)

/*** Monster glyph colour.
 * Return is a crawl colour number.
 * @treturn int
 * @function colour
 */
static int moninf_get_colour(lua_State *ls)
{
    MONINF(ls, 1, mi);
    lua_pushnumber(ls, mi->colour());
    return 1;
}

/*** The x,y coordinates of the monster in player centered coordinates.
 * @treturn int
 * @treturn int
 * @function pos
 */
static int moninf_get_pos(lua_State *ls)
{
    MONINF(ls, 1, mi);
    lua_pushnumber(ls, mi->pos.x - you.pos().x);
    lua_pushnumber(ls, mi->pos.y - you.pos().y);
    return 2;
}

#define MIRES1(field, resist) \
    static int moninf_get_##field(lua_State *ls) \
    { \
        MONINF(ls, 1, mi); \
        lua_pushnumber(ls, get_resist(mi->resists(), resist)); \
        return 1; \
    }

// Named for consistency with the player resists.
/*** Does the monster resist poison?
 * Returns a value ranging from -1 (vulnerable) to 3 (immune).
 * @treturn int resistance level
 * @function res_poison
 */
MIRES1(res_poison, MR_RES_POISON)
/*** Does the monster resist fire?
 * Returns a value ranging from -1 (vulnerable) to 3 (immune).
 * @treturn int resistance level
 * @function res_fire
 */
MIRES1(res_fire, MR_RES_FIRE)
/*** Does the monster resist cold?
 * Returns a value ranging from -1 (vulnerable) to 3 (immune).
 * @treturn int resistance level
 * @function res_cold
 */
MIRES1(res_cold, MR_RES_COLD)
/*** Does the monster resist negative energy?
 * Returns a value ranging from -1 (vulnerable) to 3 (immune).
 * @treturn int resistance level
 * @function res_draining
 */
MIRES1(res_draining, MR_RES_NEG)
/*** Does the monster resist electricity?
 * Returns a value ranging from -1 (vulnerable) to 3 (immune).
 * @treturn int resistance level
 * @function res_shock
 */
MIRES1(res_shock, MR_RES_ELEC)
/*** Does the monster resist corrosion?
 * Returns a value ranging from -1 (vulnerable) to 3 (immune).
 * @treturn int resistance level
 * @function res_corr
 */
MIRES1(res_corr, MR_RES_ACID)
/*** Can the monster currently be frenzied?
 * Is it possible to affect the monster with the discord spell or a datura
 * dart?
 * @treturn boolean
 * @function can_go_frenzy
 */
MIRET1(boolean, can_go_frenzy, can_go_frenzy)

/*** The monster's max HP given in its description.
 * @treturn string describing the max HP (usually "about X").
 * @function max_hp
 */
static int moninf_get_max_hp(lua_State *ls)
{
    MONINF(ls, 1, mi);
    lua_pushstring(ls, mi->get_max_hp_desc().c_str());
    return 1;
}

/*** The monster's MR level, in "pips" (number of +'s shown on its description).
 * Returns a value ranging from 0 to 125 (immune).
 * @treturn int MR level
 * @function mr
 */
static int moninf_get_mr(lua_State *ls)
{
    MONINF(ls, 1, mi);
    lua_pushnumber(ls, ceil(1.0*mi->res_magic()/MR_PIP));
    return 1;
}

/*** Your probability of defeating the monster's MR with a given spell or zap.
 * Returns a value ranging from 0 (no chance) to 100 (guaranteed success).
 *    Returns nil if MR does not apply or the spell can't be cast.
 * @tparam string spell name
 * @tparam[opt] boolean true if this spell is evoked rather than cast;
 *    defaults to false
 * @treturn int|string|nil percent chance of success (0-100);
 *     returns "magic immune" if monster is immune;
 *     returns nil if MR does not apply.
 * @function defeat_mr
 */
static int moninf_get_defeat_mr(lua_State *ls)
{
    MONINF(ls, 1, mi);
    spell_type spell = spell_by_name(luaL_checkstring(ls, 2), false);
    bool is_evoked = lua_isboolean(ls, 3) ? lua_toboolean(ls, 3) : false;
    int power = is_evoked ?
        (15 + you.skill(SK_EVOCATIONS, 7) / 2) * (wand_mp_cost() + 9) / 9 :
        calc_spell_power(spell, true);
    spell_flags flags = get_spell_flags(spell);
    bool mr_check = testbits(flags, spflag::MR_check)
        && testbits(flags, spflag::dir_or_target)
        && !testbits(flags, spflag::helpful);
    if (power <= 0 || !mr_check)
    {
        lua_pushnil(ls);
        return 1;
    }
    int mr = mi->res_magic();
    if (mr == MAG_IMMUNE)
    {
        lua_pushstring(ls, "magic immune");
        return 1;
    }
    zap_type zap = spell_to_zap(spell);
    int eff_power = zap == NUM_ZAPS ? power : zap_ench_power(zap, power, false);
#if TAG_MAJOR_VERSION == 34
    int adj_power = is_evoked ? pakellas_effective_hex_power(eff_power) : eff_power;
    int success = hex_success_chance(mr, adj_power, 100);
#else
    int success = hex_success_chance(mr, eff_power, 100);
#endif
    lua_pushnumber(ls, success);
    return 1;
}

/*** The monster's AC value, in "pips" (number of +'s shown on its description).
 * Returns a value ranging from 0 to 5 (highest).
 * @treturn int AC level
 * @function ac
 */
static int moninf_get_ac(lua_State *ls)
{
    MONINF(ls, 1, mi);
    lua_pushnumber(ls, ceil(mi->ac/5.0));
    return 1;
}
/*** The monster's EV value, in "pips" (number of +'s shown on its description).
 * Returns a value ranging from 0 to 5 (highest).
 * @treturn int evasion level
 * @function ev
 */
static int moninf_get_ev(lua_State *ls)
{
    MONINF(ls, 1, mi);
    int value = mi->ev;
    if (!value && mi->base_ev != INT_MAX)
        value = mi->base_ev;
    lua_pushnumber(ls, ceil(value/5.0));
    return 1;
}

/*** Get the monster's holiness.
 * If passed a holiness, returns a boolean test of whether the monster has the
 * given holiness. Otherwise returns a string describing the monster's
 * holiness.
 *
 * Possible holinesses: "holy", "natural", "undead", "demonic",
 * "nonliving", "plant", "evil". Evil is a pseudo-holiness given to natural,
 * nonliving, or plant monsters that are hated by the good gods for the spells
 * they cast or gods they worship.
 * @tparam[opt] string holiness
 * @treturn string|boolean
 * @function holiness
 */
LUAFN(moninf_get_holiness)
{
    MONINF(ls, 1, mi);

    string holi = luaL_checkstring(ls, 2);
    lowercase(holi);
    mon_holy_type arg = holiness_by_name(holi);
    if (!holi.empty() && arg == MH_NONE)
    {
        luaL_argerror(ls, 2, (string("no such holiness: '")
                              + holi + "'").c_str());
        return 0;
    }

    if (!holi.empty())
        PLUARET(boolean, bool(mi->holi & arg));
    else
        PLUARET(string, holiness_description(mi->holi).c_str());
}

/*** Get the monster's average depth of (random) generation in the current branch
 * Returns -1 if the monster is not generated in this branch. Does not handle
 * fish or zombies.
 * @treturn number
 * @function avg_local_depth
 */
LUAFN(moninf_get_avg_local_depth)
{
    MONINF(ls, 1, mi);
    PLUARET(number, monster_pop_depth_avg(you.where_are_you, mi->type));
}

/*** Get the monster's probability of randomly generating on the current floor
 * This can be used to estimate difficulty, but keep in mind that it is highly
 * dependent on the branch's generation table.
 * Returns -1 if the monster is not generated in this branch. Does not handle
 * fish or zombies.
 * @treturn number
 * @function avg_local_prob
 */
LUAFN(moninf_get_avg_local_prob)
{
    MONINF(ls, 1, mi);
    PLUARET(number, monster_probability(level_id::current(), mi->type));
}


// const char* here would save a tiny bit of memory, but every map
// for an unique pair of types costs 35KB of code. We have
// map<string, int> elsewhere.
static map<string, int> mi_flags;
static void _init_mi_flags()
{
    int f = 0;
#define MI_FLAG(x) mi_flags[x] = f++;
#include "mi-enum.h"
#undef MI_FLAG
}

/*** Test a monster flag.
 * Check if a monster has a flag set. See `mi-flag.h` for a list of flags.
 * @tparam string flagname
 * @treturn boolean
 * @function is
 */
LUAFN(moninf_get_is)
{
    MONINF(ls, 1, mi);
    int num = -1;
    if (lua_isnumber(ls, 2)) // legacy scripts
        num = lua_tonumber(ls, 2);
    else
    {
        if (mi_flags.empty())
            _init_mi_flags();
        string flag = luaL_checkstring(ls, 2);
        if (int *flagnum = map_find(mi_flags, lowercase(flag)))
            num = *flagnum;
        else
        {
            luaL_argerror(ls, 2, (string("no such moninf flag: '")
                                  + flag + "'").c_str());
            return 0;
        }
    }
    if (num < 0 || num >= NUM_MB_FLAGS)
    {
        luaL_argerror(ls, 2, "mb:is() out of bounds");
        return 0;
    }
    lua_pushboolean(ls, mi->is(num));
    return 1;
}

/*** Get the monster's possible spells.
 * Returns a list of the monster's possible spellbooks. Each spellbook is given
 * as a list of spell names.
 * @treturn array
 * @function spells
 */
LUAFN(moninf_get_spells)
{
    MONINF(ls, 1, mi);

    lua_newtable(ls);

    if (!mi->has_spells())
        return 1;

    unique_books books = get_unique_spells(*mi);
    const size_t num_books = books.size();

    for (size_t i = 0; i < num_books; ++i)
    {
        const vector<mon_spell_slot> &unique_slots = books[i];
        vector<string> spell_titles;

        for (const auto& slot : unique_slots)
            spell_titles.emplace_back(spell_title(slot.spell));

        clua_stringtable(ls, spell_titles);
        lua_rawseti(ls, -2, i+1);
    }

    return 1;
}

static bool cant_see_you(const monster_info *mi)
{
    if (mons_class_flag(mi->type, M_SEE_INVIS))
        return false;
    if (you.in_water())
        return false;
    return you.invisible() || mi->is(MB_BLIND);
}

/*** What quality of stab can you get on this monster?
 * The return value is a number representing the percentage of a top-tier stab
 * you can currently get by attacking the monster. Possible values are:
 *
 * - 1.0 Sleep and paralysis stabs.
 * - 0.5 Net, web, and petrification stabs.
 * - 0.25 Confusion, fear, and invisibility stabs.
 * - 0.166666666 Distraction stabs.
 * - 0.0 No stab bonus.
 *
 * @treturn number
 * @function stabbability
 */
LUAFN(moninf_get_stabbability)
{
    MONINF(ls, 1, mi);
    if (mi->is(MB_DORMANT) || mi->is(MB_SLEEPING) || mi->is(MB_PARALYSED))
        lua_pushnumber(ls, 1.0);
    else if (mi->is(MB_CAUGHT) || mi->is(MB_WEBBED) || mi->is(MB_PETRIFYING)
             || mi->is(MB_PETRIFIED))
    {
        lua_pushnumber(ls, 0.5);
    }
    else if (mi->is(MB_CONFUSED) || mi->is(MB_FLEEING) || cant_see_you(mi))
        lua_pushnumber(ls, 0.25);
    else if (mi->is(MB_DISTRACTED))
        lua_pushnumber(ls, 0.16666666);
    else
        lua_pushnumber(ls, 0);

    return 1;
}

/*** Is the monster caught in something?
 * Tests for nets or webs.
 * @treturn boolean
 * @function is_caught
 */
LUAFN(moninf_get_is_caught)
{
    MONINF(ls, 1, mi);
    lua_pushboolean(ls, mi->is(MB_CAUGHT) || mi->is(MB_WEBBED));
    return 1;
}

/*** Is the monster constricted?
 * @treturn boolean
 * @function is_constricted
 */
LUAFN(moninf_get_is_constricted)
{
    MONINF(ls, 1, mi);
    lua_pushboolean(ls, (mi->constrictor_name.find("constricted by") == 0)
                     || (mi->constrictor_name.find("held by") == 0));
    return 1;
}

/*** Is the monster constricting something?
 * @treturn boolean
 * @function is_constricting
 */
LUAFN(moninf_get_is_constricting)
{
    MONINF(ls, 1, mi);
    lua_pushboolean(ls, !mi->constricting_name.empty());
    return 1;
}

/*** Is the monster constricting you in particular?
 * @treturn boolean
 * @function is_constricting_you
 */
LUAFN(moninf_get_is_constricting_you)
{
    MONINF(ls, 1, mi);
    if (!you.is_constricted())
    {
        lua_pushboolean(ls, false);
        return 1;
    }

    // yay the interface
    lua_pushboolean(ls, (find(mi->constricting_name.begin(),
                              mi->constricting_name.end(), "constricting you")
                         != mi->constricting_name.end())
                     || (find(mi->constricting_name.begin(),
                              mi->constricting_name.end(), "holding you")
                         != mi->constricting_name.end()));
    return 1;
}

/*** Can this monster be constricted?
 * @treturn boolean
 * @function can_be_constricted
 */
LUAFN(moninf_get_can_be_constricted)
{
    MONINF(ls, 1, mi);
    if (!mi->constrictor_name.empty()
        || !form_keeps_mutations()
        || (you.species != SP_NAGA
            || you.experience_level <= 12
            || you.is_constricting())
         && (you.species != SP_OCTOPODE || !you.has_usable_tentacle()))
    {
        lua_pushboolean(ls, false);
    }
    else
    {
        monster dummy;
        dummy.type = mi->type;
        dummy.base_monster = mi->base_type;
        lua_pushboolean(ls, dummy.res_constrict() < 3);
    }
    return 1;
}

/*** How far can the monster reach with their melee weapon?
 * @treturn int
 * @function reach_range
 */
LUAFN(moninf_get_reach_range)
{
    MONINF(ls, 1, mi);

    lua_pushnumber(ls, mi->reach_range());
    return 1;
}

/*** Is this monster a unique?
 * @treturn boolean
 * @function is_unique
 */
LUAFN(moninf_get_is_unique)
{
    MONINF(ls, 1, mi);
    lua_pushboolean(ls, mons_is_unique(mi->type));
    return 1;
}

/*** Can this monster move?
 * @treturn boolean
 * @function is_stationary
 */
LUAFN(moninf_get_is_stationary)
{
    MONINF(ls, 1, mi);
    lua_pushboolean(ls, mons_class_is_stationary(mi->type));
    return 1;
}

/*** Get a string describing how injured this monster is.
 * @treturn string
 * @function damage_desc
 */
LUAFN(moninf_get_damage_desc)
{
    MONINF(ls, 1, mi);
    string s = mi->damage_desc();
    lua_pushstring(ls, s.c_str());
    return 1;
}

/*** A description of this monster.
 * @tparam[opt] boolean set true to get the description information body
 *     displayed when examining the monster; if false (default) returns
 *     a short description.
 * @treturn string
 * @function desc
 */
LUAFN(moninf_get_desc)
{
    MONINF(ls, 1, mi);
    if (lua_isboolean(ls, 2) && lua_toboolean(ls, 2))
    {
        // full description
        describe_info inf;
        bool has_stat_desc;
        get_monster_db_desc(*mi, inf, has_stat_desc);
        lua_pushstring(ls, inf.body.str().c_str());
    }
    else
    {
        // short description
        string desc;
        int col;
        mi->to_string(1, desc, col);
        lua_pushstring(ls, desc.c_str());
    }
    return 1;
}

/*** What statuses is this monster under?
 * If passed a string parameter, returns a boolean indicating if the monster
 * has that status. Otherwise returns a comma separated string with all the
 * statuses the monster has.
 * @tparam[opt] string statusname
 * @treturn string|boolean
 * @function status
 */
LUAFN(moninf_get_status)
{
    MONINF(ls, 1, mi);
    const char* which = nullptr;
    if (lua_gettop(ls) >= 2)
        which = luaL_checkstring(ls, 2);

    vector<string> status = mi->attributes();
    if (!which)
    {
        PLUARET(string, comma_separated_line(status.begin(),
                                             status.end(), ", ").c_str());
    }
    for (const auto &st : status)
        if (st == which)
            PLUARET(boolean, true);

    PLUARET(boolean, false);
}

/*** The monster's full name.
 * Includes any vault defined names, the uniques name, name changes induced by
 * polymorph, &c.
 * @treturn string
 * @see mname
 * @function name
 */
LUAFN(moninf_get_name)
{
    MONINF(ls, 1, mi);
    string s = mi->full_name();
    lua_pushstring(ls, s.c_str());
    return 1;
}

static const struct luaL_reg moninf_lib[] =
{
    MIREG(type),
    MIREG(base_type),
    MIREG(number),
    MIREG(colour),
    MIREG(mname),
    MIREG(is),
    MIREG(is_safe),
    MIREG(is_firewood),
    MIREG(stabbability),
    MIREG(holiness),
    MIREG(attitude),
    MIREG(threat),
    MIREG(is_caught),
    MIREG(is_constricted),
    MIREG(is_constricting),
    MIREG(is_constricting_you),
    MIREG(can_be_constricted),
    MIREG(reach_range),
    MIREG(is_unique),
    MIREG(is_stationary),
    MIREG(damage_level),
    MIREG(damage_desc),
    MIREG(desc),
    MIREG(status),
    MIREG(name),
    MIREG(has_known_ranged_attack),
    MIREG(speed_description),
    MIREG(spells),
    MIREG(res_poison),
    MIREG(res_fire),
    MIREG(res_cold),
    MIREG(res_draining),
    MIREG(res_shock),
    MIREG(res_corr),
    MIREG(can_go_frenzy),
    MIREG(max_hp),
    MIREG(mr),
    MIREG(defeat_mr),
    MIREG(ac),
    MIREG(ev),
    MIREG(x_pos),
    MIREG(y_pos),
    MIREG(pos),
    MIREG(avg_local_depth),
    MIREG(avg_local_prob),

    { nullptr, nullptr }
};
/*** @section end
 */

// XXX: unify with directn.cc/h
// This uses relative coordinates with origin the player.
bool in_show_bounds(const coord_def &s)
{
    return s.rdist() <= ENV_SHOW_OFFSET;
}

/*** Get information about a monster at a cell.
 * Returns a monster.info object of a monster at the specified coordinates.
 * Uses player coordinates
 * @tparam int x
 * @tparam int y
 * @treturn monster.info|nil
 * @function get_monster_at
 */
LUAFN(mi_get_monster_at)
{
    COORDSHOW(s, 1, 2)
    coord_def p = player2grid(s);
    if (!you.see_cell(p))
        return 0;
    if (env.mgrid(p) == NON_MONSTER)
        return 0;
    monster* m = &env.mons[env.mgrid(p)];
    if (!m->visible_to(&you))
        return 0;
    monster_info mi(m);
    lua_push_moninf(ls, &mi);
    return 1;
}

static const struct luaL_reg mon_lib[] =
{
    { "get_monster_at", mi_get_monster_at },

    { nullptr, nullptr }
};

void cluaopen_moninf(lua_State *ls)
{
    clua_register_metatable(ls, MONINF_METATABLE, moninf_lib,
                            lua_object_gc<monster_info>);
    luaL_openlib(ls, "monster", mon_lib, 0);
}
