/**
 * @file
 * @brief Player related debugging functions.
**/

#include "AppHdr.h"

#include "wiz-you.h"

#include <algorithm>
#include <functional>

#include "abyss.h"
#include "dbg-util.h"
#include "food.h"
#include "god-abil.h"
#include "god-wrath.h"
#include "item-use.h"
#include "jobs.h"
#include "libutil.h"
#include "macro.h"
#include "message.h"
#include "misc.h" // frombool
#include "mutation.h"
#include "output.h"
#include "playable.h"
#include "player-stats.h"
#include "prompt.h"
#include "religion.h"
#include "skills.h"
#include "species.h"
#include "spl-book.h"
#include "spl-util.h"
#include "state.h"
#include "status.h"
#include "stringutil.h"
#include "transform.h"
#include "unicode.h"
#include "view.h"
#include "xom.h"

#ifdef WIZARD

job_type find_job_from_string(const string &str)
{
    const string spec = lowercase_string(str);

    job_type partial_match = JOB_UNKNOWN;

    for (const auto job : playable_jobs())
    {
        const auto name = lowercase_string(get_job_name(job));
        const auto pos = name.find(spec);

        if (pos == 0)
            return job;  // We prefer prefixes over partial matches.
        else if (pos != string::npos)
            partial_match = job;
    }

    return partial_match;
}

static xom_event_type _find_xom_event_from_string(const string &event_name)
{
    string spec = lowercase_string(event_name);

    xom_event_type x = XOM_DID_NOTHING;

    for (int i = 0; i <= XOM_LAST_REAL_ACT; ++i)
    {
        const xom_event_type xi = static_cast<xom_event_type>(i);
        const string x_name = lowercase_string(xom_effect_to_name(xi));

        string::size_type pos = x_name.find(spec);
        if (pos != string::npos)
        {
            if (pos == 0)
            {
                // We prefer prefixes over partial matches.
                x = xi;
                break;
            }
            else
                x = xi;
        }
    }

    return x;
}

void wizard_suppress()
{
    you.wizard = false;
    you.suppress_wizard = true;
    redraw_screen();
}

void wizard_change_job_to(job_type job)
{
    you.char_class = job;
    you.chr_class_name = get_job_name(job);
}

void wizard_change_species()
{
    char specs[80];

    msgwin_get_line("What species would you like to be now? " ,
                    specs, sizeof(specs));

    if (specs[0] == '\0')
    {
        canned_msg(MSG_OK);
        return;
    }

    const species_type sp = find_species_from_string(specs);

    // Means find_species_from_string couldn't interpret `specs`.
    if (sp == SP_UNKNOWN)
    {
        mpr("That species isn't available.");
        return;
    }

    change_species_to(sp);
}

// Casts a specific spell by number or name.
void wizard_cast_spec_spell()
{
    char specs[80], *end;
    int spell;

    mprf(MSGCH_PROMPT, "Cast which spell? ");
    if (cancellable_get_line_autohist(specs, sizeof(specs))
        || specs[0] == '\0')
    {
        canned_msg(MSG_OK);
        crawl_state.cancel_cmd_repeat();
        return;
    }

    spell = strtol(specs, &end, 10);

    if (spell < 0 || end == specs)
    {
        if ((spell = spell_by_name(specs, true)) == SPELL_NO_SPELL)
        {
            mpr("Cannot find that spell.");
            crawl_state.cancel_cmd_repeat();
            return;
        }
    }

    if (your_spells(static_cast<spell_type>(spell), 0, false) == spret::abort)
        crawl_state.cancel_cmd_repeat();
}

void wizard_memorise_spec_spell()
{
    char specs[80], *end;
    int spell;

    mprf(MSGCH_PROMPT, "Memorise which spell? ");
    if (cancellable_get_line_autohist(specs, sizeof(specs))
        || specs[0] == '\0')
    {
        canned_msg(MSG_OK);
        crawl_state.cancel_cmd_repeat();
        return;
    }

    spell = strtol(specs, &end, 10);

    if (spell < 0 || end == specs)
    {
        if ((spell = spell_by_name(specs, true)) == SPELL_NO_SPELL)
        {
            mpr("Cannot find that spell.");
            crawl_state.cancel_cmd_repeat();
            return;
        }
    }

    if (get_spell_flags(static_cast<spell_type>(spell)) & spflag::monster)
        mpr("Spell is monster-only - unpredictable behaviour may result.");
    if (!learn_spell(static_cast<spell_type>(spell), true))
        crawl_state.cancel_cmd_repeat();
}

void wizard_heal(bool super_heal)
{
    if (super_heal)
    {
        mpr("Super healing.");
        // Clear more stuff.
        unrot_hp(9999);
        you.magic_contamination = 0;
        you.duration[DUR_LIQUID_FLAMES] = 0;
        you.clear_beholders();
        you.attribute[ATTR_XP_DRAIN] = 0;
        you.duration[DUR_PETRIFIED] = 0;
        you.duration[DUR_PETRIFYING] = 0;
        you.duration[DUR_CORROSION] = 0;
        you.duration[DUR_DOOM_HOWL] = 0;
        you.duration[DUR_WEAK] = 0;
        you.duration[DUR_NO_HOP] = 0;
        you.props["corrosion_amount"] = 0;
        you.duration[DUR_BREATH_WEAPON] = 0;
        delete_all_temp_mutations("Super heal");
        you.stat_loss.init(0);
        you.attribute[ATTR_STAT_LOSS_XP] = 0;
        you.redraw_stats = true;
    }
    else
        mpr("Healing.");

    // Clear most status ailments.
    you.disease = 0;
    you.duration[DUR_CONF]      = 0;
    you.duration[DUR_POISONING] = 0;
    you.duration[DUR_EXHAUSTED] = 0;
    set_hp(you.hp_max);
    set_mp(you.max_magic_points);
    set_hunger(HUNGER_VERY_FULL + 100, true);
    you.redraw_hit_points = true;
    you.redraw_armour_class = true;
    you.redraw_evasion = true;

    for (int stat = 0; stat < NUM_STATS; stat++)
        you.duration[stat_zero_duration(static_cast<stat_type> (stat))] = 0;
}

void wizard_set_hunger_state()
{
    string hunger_prompt = "Set hunger state to ";
    if (you.species != SP_VAMPIRE && you.species != SP_GHOUL)
        hunger_prompt += "f(A)inting, ";
    hunger_prompt += "s(T)arving, (N)ear starving, (H)ungry";
    if (you.species == SP_GHOUL)
        hunger_prompt += " or (S)atiated";
    else
        hunger_prompt += ", (S)atiated, (F)ull or (E)ngorged";
    hunger_prompt += "? ";

    mprf(MSGCH_PROMPT, "%s", hunger_prompt.c_str());

    const int c = toalower(getch_ck());

    // Values taken from food.cc.
    switch (c)
    {
    case 'a': you.hunger = HUNGER_FAINTING / 2; break;
    case 't': you.hunger = (HUNGER_STARVING + HUNGER_FAINTING) / 2; break;
    case 'n': you.hunger = 1100;  break;
    case 'h': you.hunger = 2300;  break;
    case 's': you.hunger = 4900;  break;
    case 'f': you.hunger = 7900;  break;
    case 'e': you.hunger = HUNGER_MAXIMUM; break;
    default:  canned_msg(MSG_OK); break;
    }

    food_change();

    if (you.species == SP_GHOUL && you.hunger_state >= HS_SATIATED)
        mpr("Ghouls can never be full or above!");
}

void wizard_set_piety_to(int newpiety, bool force)
{
    if (newpiety < 0 || newpiety > MAX_PIETY)
    {
        mprf("Piety needs to be between 0 and %d.", MAX_PIETY);
        return;
    }
    if (newpiety > piety_breakpoint(5) && you_worship(GOD_RU))
    {
        mprf("Ru piety can't be greater than %d.", piety_breakpoint(5));
        return;
    }

    if (you_worship(GOD_XOM))
    {
        you.piety = newpiety;
        you.redraw_title = true; // redraw piety display

        int newinterest;
        if (!force)
        {
            char buf[30];

            // For Xom, also allow setting interest.
            mprf(MSGCH_PROMPT,
                 "Enter new interest (current = %d, Enter for 0): ",
                 you.gift_timeout);

            if (cancellable_get_line_autohist(buf, sizeof buf))
            {
                canned_msg(MSG_OK);
                return;
            }

            newinterest = atoi(buf);
        }
        else
            newinterest = newpiety;

        if (newinterest >= 0 && newinterest < 256)
            you.gift_timeout = newinterest;
        else
            mpr("Interest must be between 0 and 255.");

        mprf("Set piety to %d, interest to %d.", you.piety, newinterest);

        const string new_xom_favour = describe_xom_favour();
        const string msg = "You are now " + new_xom_favour;
        god_speaks(you.religion, msg.c_str());
        return;
    }

    if (newpiety < 1 && !force)
    {
        if (yesno("Are you sure you want to be excommunicated?", false, 'n'))
        {
            you.piety = 0;
            excommunication();
        }
        else
            canned_msg(MSG_OK);
        return;
    }
    mprf("Setting piety to %d.", newpiety);
    set_piety(newpiety);

    // Automatically reduce penance to 0.
    if (player_under_penance())
        dec_penance(you.penance[you.religion]);
}

void wizard_set_gold()
{
    const int default_gold = you.gold + 1000;
    mprf(MSGCH_PROMPT, "Enter new gold value (current = %d, Enter for %d): ",
         you.gold, default_gold);

    char buf[30];
    if (cancellable_get_line_autohist(buf, sizeof buf))
    {
        canned_msg(MSG_OK);
        return;
    }

    if (buf[0] == '\0')
        you.set_gold(default_gold);
    else
        you.set_gold(max(atoi(buf), 0));

    mprf("You now have %d gold piece%s.", you.gold, you.gold != 1 ? "s" : "");
}

void wizard_set_piety()
{
    if (you_worship(GOD_NO_GOD))
    {
        mpr("You are not religious!");
        return;
    }

    if (you_worship(GOD_RU))
    {
        mprf("Current progress to next sacrifice: %d  Progress needed: %d",
            you.props[RU_SACRIFICE_PROGRESS_KEY].get_int(),
            you.props[RU_SACRIFICE_DELAY_KEY].get_int());
    }

    mprf(MSGCH_PROMPT, "Enter new piety value (current = %d, Enter for 0): ",
         you.piety);
    char buf[30];
    if (cancellable_get_line_autohist(buf, sizeof buf))
    {
        canned_msg(MSG_OK);
        return;
    }

    wizard_set_piety_to(atoi(buf));
}

void wizard_exercise_skill()
{
    skill_type skill = debug_prompt_for_skill("Which skill (by name)? ");

    if (skill == SK_NONE)
        mpr("That skill doesn't seem to exist.");
    else
    {
        mpr("Exercising...");
        exercise(skill, 10);
    }
}

void wizard_set_skill_level(skill_type skill)
{
    if (skill == SK_NONE)
        skill = debug_prompt_for_skill("Which skill (by name)? ");

    if (skill == SK_NONE)
    {
        mpr("That skill doesn't seem to exist.");
        return;
    }

    mpr(skill_name(skill));
    double amount = prompt_for_float("To what level? ");

    if (amount < 0 || amount > 27)
    {
        canned_msg(MSG_OK);
        return;
    }

    const int old_amount = you.skills[skill];

    set_skill_level(skill, amount);

    if (amount == 27)
    {
        you.train[skill] = TRAINING_DISABLED;
        you.train_alt[skill] = TRAINING_DISABLED;
        reset_training();
        check_selected_skills();
    }

    redraw_skill(skill);

    mprf("%s %s to skill level %.1f.", (old_amount < amount ? "Increased" :
                                      old_amount > amount ? "Lowered"
                                                          : "Reset"),
         skill_name(skill), amount);
}

void wizard_set_all_skills()
{
    double amount = prompt_for_float("Set all skills to what level? ");

    if (amount < 0)             // cancel returns -1 -- bwr
        canned_msg(MSG_OK);
    else
    {
        if (amount > 27)
            amount = 27;

        for (skill_type sk = SK_FIRST_SKILL; sk < NUM_SKILLS; ++sk)
        {
            if (is_invalid_skill(sk) || is_useless_skill(sk))
                continue;

            set_skill_level(sk, amount);

            if (amount == 27)
            {
                you.train[sk] = TRAINING_DISABLED;
                you.training[sk] = 0;
            }
        }

        you.redraw_title = true;

        // We're not updating skill cost here since XP hasn't changed.

        calc_hp(true, false);
        calc_mp();

        you.redraw_armour_class = true;
        you.redraw_evasion = true;
    }
}

bool wizard_add_mutation()
{
    bool success = false;
    char specs[80];

    if (you.has_mutation(MUT_MUTATION_RESISTANCE))
        mpr("Ignoring mut resistance to apply mutation.");

    msgwin_get_line("Which mutation (name, 'good', 'bad', 'any', "
                    "'xom', 'slime', 'qazlal')? ",
                    specs, sizeof(specs));

    if (specs[0] == '\0')
    {
        canned_msg(MSG_OK);
        return true;
    }

    vector<mutation_type> partial_matches;
    mutation_type mutat = mutation_from_name(specs, true, &partial_matches);

    if (mutat >= CATEGORY_MUTATIONS)
         return mutate(mutat, "wizard power", true, true);

    if (mutat == NUM_MUTATIONS)
    {
        crawl_state.cancel_cmd_repeat();

        if (partial_matches.empty())
            mpr("No matching mutation names.");
        else
        {
            vector<string> matches;

            for (mutation_type mut : partial_matches)
            {
                const char *mutname = mutation_name(mut, true);
                ASSERT(mutname); // `mutation_name` returns nullptr if something went wrong getting the desc for `mut`.
                matches.emplace_back(mutname);
            }

            string prefix = make_stringf("No exact match for mutation '%s', possible matches are: ", specs);

            // Use mpr_comma_separated_list() because the list
            // might be *LONG*.
            mpr_comma_separated_list(prefix, matches, " and ", ", ",
                                     MSGCH_DIAGNOSTICS);
        }

        return false;
    }
    else
    {
        mprf("Found #%d: %s (\"%s\")", (int) mutat,
             mutation_name(mutat),
             mutation_desc(mutat, 1, false).c_str());

        const int levels =
            prompt_for_int("How many levels to increase or decrease? ",
                                  false);

        if (levels == 0)
        {
            canned_msg(MSG_OK);
            success = false;
        }
        else if (levels > 0)
        {
            for (int i = 0; i < levels; ++i)
                if (mutate(mutat, "wizard power", true, true))
                    success = true;
        }
        else
        {
            for (int i = 0; i < -levels; ++i)
                if (delete_mutation(mutat, "wizard power", true, true))
                    success = true;
        }
    }

    return success;
}

void wizard_set_abyss()
{
    char buf[80];
    mprf(MSGCH_PROMPT, "Enter values for X, Y, Z (space separated) or return: ");
    if (!cancellable_get_line_autohist(buf, sizeof buf))
        abyss_teleport();

    int x = 0, y = 0, z = 0;
    sscanf(buf, "%d %d %d", &x, &y, &z);
    set_abyss_state(coord_def(x,y), z);
}

void wizard_set_stats()
{
    char buf[80];
    mprf(MSGCH_PROMPT, "Enter values for Str, Int, Dex (space separated): ");
    if (cancellable_get_line_autohist(buf, sizeof buf) || buf[0] == '\0')
    {
        canned_msg(MSG_OK);
        return;
    }

    int sstr = you.strength(false),
        sdex = you.dex(false),
        sint = you.intel(false);

    sscanf(buf, "%d %d %d", &sstr, &sint, &sdex);

    mprf("Setting attributes (Str, Int, Dex) to: %i, %i, %i", sstr, sint, sdex);
    you.base_stats[STAT_STR] = debug_cap_stat(sstr);
    you.base_stats[STAT_INT] = debug_cap_stat(sint);
    you.base_stats[STAT_DEX] = debug_cap_stat(sdex);
    you.stat_loss.init(0);
    you.attribute[ATTR_STAT_LOSS_XP] = 0;
    you.redraw_stats.init(true);
    you.redraw_evasion = true;
}

void wizard_edit_durations()
{
    vector<duration_type> durs;
    size_t max_len = 0;

    for (int i = 0; i < NUM_DURATIONS; ++i)
    {
        const duration_type dur = static_cast<duration_type>(i);

        if (!you.duration[i])
            continue;

        max_len = max(strlen(duration_name(dur)), max_len);
        durs.push_back(dur);
    }

    if (!durs.empty())
    {
        for (size_t i = 0; i < durs.size(); ++i)
        {
            const duration_type dur = durs[i];
            const char ch = i >= 26 ? ' ' : 'a' + i;
            mprf_nocap(MSGCH_PROMPT, "%c%c %-*s : %d",
                 ch, ch == ' ' ? ' ' : ')',
                 (int)max_len, duration_name(dur), you.duration[dur]);
        }
        mprf(MSGCH_PROMPT, "\nEdit which duration (letter or name)? ");
    }
    else
        mprf(MSGCH_PROMPT, "Edit which duration (name)? ");

    char buf[80];

    if (cancellable_get_line_autohist(buf, sizeof buf) || !*buf)
    {
        canned_msg(MSG_OK);
        return;
    }

    if (!strlcpy(buf, lowercase_string(trimmed_string(buf)).c_str(), sizeof(buf)))
    {
        canned_msg(MSG_OK);
        return;
    }

    duration_type choice = NUM_DURATIONS;

    if (strlen(buf) == 1 && isalower(buf[0]))
    {
        if (durs.empty())
        {
            mprf(MSGCH_PROMPT, "No existing durations to choose from.");
            return;
        }
        const int dchoice = buf[0] - 'a';

        if (dchoice < 0 || dchoice >= (int) durs.size())
        {
            mprf(MSGCH_PROMPT, "Invalid choice.");
            return;
        }
        choice = durs[dchoice];
    }
    else
    {
        vector<duration_type> matches;
        vector<string> match_names;

        for (int i = 0; i < NUM_DURATIONS; ++i)
        {
            const duration_type dur = static_cast<duration_type>(i);
            if (strcmp(duration_name(dur), buf) == 0)
            {
                choice = dur;
                break;
            }
            if (strstr(duration_name(dur), buf) != nullptr)
            {
                matches.push_back(dur);
                match_names.emplace_back(duration_name(dur));
            }
        }
        if (choice != NUM_DURATIONS)
            ;
        else if (matches.size() == 1)
            choice = matches[0];
        else if (matches.empty())
        {
            mprf(MSGCH_PROMPT, "No durations matching '%s'.", buf);
            return;
        }
        else
        {
            string prefix = "No exact match for duration '";
            prefix += buf;
            prefix += "', possible matches are: ";

            mpr_comma_separated_list(prefix, match_names, " and ", ", ",
                                     MSGCH_DIAGNOSTICS);
            return;
        }
    }

    snprintf(buf, sizeof(buf), "Set '%s' to: ", duration_name(choice));
    int num = prompt_for_int(buf, false);

    if (num == 0)
    {
        mprf(MSGCH_PROMPT, "Can't set duration directly to 0, setting it to 1 instead.");
        num = 1;
    }
    you.duration[choice] = num;
}

void wizard_list_props()
{
    mprf(MSGCH_DIAGNOSTICS, "props: %s",
         you.describe_props().c_str());
}

/*
 * Hard reset Ds mutations for `xl` based on the trait schedule.
 */
static void reset_ds_muts_from_schedule(int xl)
{
    if (you.species != SP_DEMONSPAWN)
        return;

    for (int i = 0; i < NUM_MUTATIONS; i++)
    {
        mutation_type mut = static_cast<mutation_type>(i);
        int innate_levels = 0;
        bool is_trait = false;
        for (player::demon_trait trait : you.demonic_traits)
        {
            if (trait.mutation == mut)
            {
                is_trait = true;
                if (xl >= trait.level_gained)
                    innate_levels += 1;
            }
        }
        if (is_trait)
        {
            while (you.innate_mutation[mut] > innate_levels)
            {
                // First set it as non-innate, then delete the mutation from
                // there. delete_mutation won't delete mutations otherwise.
                // This step doesn't affect temporary mutations.
                you.innate_mutation[mut]--;
                delete_mutation(mut, "level change", false, true, false, false);
            }
            if (you.innate_mutation[mut] < innate_levels)
                perma_mutate(mut, innate_levels - you.innate_mutation[mut], "level change");
        }
    }
}


static void debug_uptick_xl(int newxl, bool train)
{
    if (train)
    {
        you.exp_available += 10 * (exp_needed(newxl) - you.experience);
        train_skills();
    }
    you.experience = exp_needed(newxl);
    if (you.experience_level < you.max_level)
    {
        // Fixing up Ds muts needs to happen before the level change, so that the mutations validate correctly
        if (newxl < you.max_level)
            reset_ds_muts_from_schedule(newxl);
        else
            reset_ds_muts_from_schedule(you.max_level); // let level change handle the rest
    }
    level_change(true);
#ifdef DEBUG
    validate_mutations();
#endif
}

/**
 * Set the player's XL to a new value.
 * @param newxl  The new experience level.
 */
static void debug_downtick_xl(int newxl)
{
    set_hp(you.hp_max);
    // boost maxhp so we don't die if heavily rotted
    you.hp_max_adj_perm += 1000;
    you.experience = exp_needed(newxl);
    level_change();
    reset_ds_muts_from_schedule(newxl); // needs to happen after the level change
    you.skill_cost_level = 0;
    check_skill_cost_change();
    // restore maxhp loss
    you.hp_max_adj_perm -= 1000;
    calc_hp();
    if (you.hp_max <= 0)
    {
        // ... but remove it completely if unviable
        you.hp_max_adj_temp = max(you.hp_max_adj_temp, 0);
        you.hp_max_adj_perm = max(you.hp_max_adj_perm, 0);
        calc_hp();
    }

    set_hp(max(1, you.hp));
#ifdef DEBUG
    validate_mutations();
#endif
}

void wizard_set_xl(bool change_skills)
{
    mprf(MSGCH_PROMPT, "Enter new experience level: ");
    char buf[30];
    if (cancellable_get_line_autohist(buf, sizeof buf))
    {
        canned_msg(MSG_OK);
        return;
    }

    const int newxl = atoi(buf);
    if (newxl < 1 || newxl > you.get_max_xl() || newxl == you.experience_level)
    {
        canned_msg(MSG_OK);
        return;
    }

    set_xl(newxl, change_skills);
    mprf("Experience level set to %d.", newxl);
}

void set_xl(const int newxl, const bool train, const bool silent)
{
    no_messages mx(silent);

    if (newxl < you.experience_level)
        debug_downtick_xl(newxl);
    else
        debug_uptick_xl(newxl, train);
}

void wizard_get_god_gift()
{
    if (you_worship(GOD_NO_GOD))
    {
        mpr("You are not religious!");
        return;
    }

    if (you_worship(GOD_RU))
    {
        ru_offer_new_sacrifices();
        return;
    }

    if (!do_god_gift(true))
        mpr("Nothing happens.");
}

void wizard_toggle_xray_vision()
{
    you.xray_vision = !you.xray_vision;
    mprf("X-ray vision %s.", you.xray_vision ? "enabled" : "disabled");
    viewwindow(true);
}

void wizard_freeze_time()
{
    auto& props = you.props;
    // this property is never false: either true or unset
    if (props.exists(FREEZE_TIME_KEY))
    {
        props.erase(FREEZE_TIME_KEY);
        mpr("You allow the flow of time to resume.");
    }
    else
    {
        props[FREEZE_TIME_KEY] = true;
        mpr("You bring the flow of time to a stop.");
    }
}

void wizard_god_wrath()
{
    const god_type god = choose_god(you.religion);
    if (god == NUM_GODS)
        mpr("That god doesn't seem to exist!");
    else if (god == GOD_NO_GOD)
        mpr("You suffer the terrible wrath of No God.");
    else if (!divine_retribution(god, true, true))
    {
        // Dead Jiyva, or Ru/Gozag/Ashenzari.
        mprf("%s is not feeling wrathful today.", god_name(god).c_str());
    }
}

void wizard_god_mollify()
{
    bool mollified = false;
    for (god_iterator it; it; ++it)
    {
        if (player_under_penance(*it))
        {
            dec_penance(*it, you.penance[*it]);
            mollified = true;
        }
    }
    if (!mollified)
        mpr("You are not under penance.");
}

void wizard_transform()
{
    transformation form;

    while (true)
    {
        string line;
        for (int i = 0; i < NUM_TRANSFORMS; i++)
        {
            const auto tr = static_cast<transformation>(i);
#if TAG_MAJOR_VERSION == 34
            if (tr == transformation::jelly || tr == transformation::porcupine)
                continue;
#endif
            line += make_stringf("[%c] %-10s ", i + 'a', transform_name(tr));
            if (i % 5 == 4 || i == NUM_TRANSFORMS - 1)
            {
                mprf(MSGCH_PROMPT, "%s", line.c_str());
                line.clear();
            }
        }
        mprf(MSGCH_PROMPT, "Which form (ESC to exit)? ");

        int keyin = toalower(get_ch());

        if (key_is_escape(keyin) || keyin == ' '
            || keyin == '\r' || keyin == '\n')
        {
            canned_msg(MSG_OK);
            return;
        }

        if (keyin < 'a' || keyin > 'a' + NUM_TRANSFORMS - 1)
            continue;

        const auto k_tr = static_cast<transformation>(keyin - 'a');
#if TAG_MAJOR_VERSION == 34
        if (k_tr == transformation::jelly || k_tr == transformation::porcupine)
            continue;
#endif
        form = k_tr;
        break;
    }

    you.transform_uncancellable = false;
    if (!transform(200, form) && you.form != form)
        mpr("Transformation failed.");
}

void wizard_join_religion()
{
    if (you.species == SP_DEMIGOD)
    {
        mpr("Not even in wizmode may Demigods worship a god!");
        return;
    }
    god_type god = choose_god();
    if (god == NUM_GODS)
        mpr("That god doesn't seem to exist!");
    else if (god == GOD_NO_GOD)
    {
        if (you_worship(GOD_NO_GOD))
            mpr("You already have no god!");
        else
            excommunication();
    }
    else if (you_worship(god))
        mpr("You already worship that god!");
    else
    {
        if (god == GOD_GOZAG)
            you.gold = max(you.gold, gozag_service_fee());
        join_religion(god);
    }
}

void wizard_xom_acts()
{
    char specs[80];

    msgwin_get_line("What action should Xom take? (Blank = any) " ,
                    specs, sizeof(specs));

    const int severity = you_worship(GOD_XOM) ? abs(you.piety - HALF_MAX_PIETY)
                                              : random_range(0, HALF_MAX_PIETY);

    if (specs[0] == '\0')
    {
        const maybe_bool nice = you_worship(GOD_XOM) ? MB_MAYBE :
                                frombool(coinflip());
        const xom_event_type result = xom_acts(severity, nice);
        dprf("Xom did '%s'.", xom_effect_to_name(result).c_str());
#ifndef DEBUG_DIAGNOSTICS
        UNUSED(result);
#endif
        return;
    }

    xom_event_type event = _find_xom_event_from_string(specs);
    if (event == XOM_DID_NOTHING)
    {
        dprf("That action doesn't seem to exist!");
        return;
    }

    dprf("Okay, Xom is doing '%s'.", xom_effect_to_name(event).c_str());
    xom_take_action(event, severity);
}
#endif
