/*
 *  File:       tilereg.cc
 *  Summary:    Region system implementations
 *
 *  Created by: ennewalker on Sat Jan 5 01:33:53 2008 UTC
 *
 *  Modified for Crawl Reference by $Author$ on $Date$
 */

#include "AppHdr.h"
REVISION("$Rev$");

#include "cio.h"
#include "debug.h"
#include "describe.h"
#include "files.h"
#include "food.h"
#include "itemname.h"
#include "it_use2.h"
#include "item_use.h"
#include "items.h"
#include "macro.h"
#include "message.h"
#include "misc.h"
#include "menu.h"
#include "newgame.h"
#include "mon-util.h"
#include "player.h"
#include "religion.h"
#include "spells3.h"
#include "stash.h"
#include "stuff.h"
#include "terrain.h"
#include "transfor.h"
#include "travel.h"
#include "view.h"

#include "tilereg.h"
#include "tiles.h"
#include "tilefont.h"
#include "tilesdl.h"
#include "tilemcache.h"

#include <SDL_opengl.h>

coord_def Region::NO_CURSOR(-1, -1);

int TextRegion::print_x;
int TextRegion::print_y;
TextRegion *TextRegion::text_mode = NULL;
int TextRegion::text_col = 0;

TextRegion *TextRegion::cursor_region= NULL;
int TextRegion::cursor_flag = 0;
int TextRegion::cursor_x;
int TextRegion::cursor_y;

const VColour map_colours[MAX_MAP_COL] =
{
    VColour(  0,   0,   0, 255), // BLACK
    VColour(128, 128, 128, 255), // DKGREY
    VColour(160, 160, 160, 255), // MDGREY
    VColour(192, 192, 192, 255), // LTGREY
    VColour(255, 255, 255, 255), // WHITE

    VColour(  0,  64, 255, 255), // BLUE  (actually cyan-blue)
    VColour(128, 128, 255, 255), // LTBLUE
    VColour(  0,  32, 128, 255), // DKBLUE (maybe too dark)

    VColour(  0, 255,   0, 255), // GREEN
    VColour(128, 255, 128, 255), // LTGREEN
    VColour(  0, 128,   0, 255), // DKGREEN

    VColour(  0, 255, 255, 255), // CYAN
    VColour( 64, 255, 255, 255), // LTCYAN (maybe too pale)
    VColour(  0, 128, 128, 255), // DKCYAN

    VColour(255,   0,   0, 255), // RED
    VColour(255, 128, 128, 255), // LTRED (actually pink)
    VColour(128,   0,   0, 255), // DKRED

    VColour(192,   0, 255, 255), // MAGENTA (actually blue-magenta)
    VColour(255, 128, 255, 255), // LTMAGENTA
    VColour( 96,   0, 128, 255), // DKMAGENTA

    VColour(255, 255,   0, 255), // YELLOW
    VColour(255, 255,  64, 255), // LTYELLOW (maybe too pale)
    VColour(128, 128,   0, 255), // DKYELLOW

    VColour(165,  91,   0, 255), // BROWN
};

const int dir_dx[9] = {-1, 0, 1, -1, 0, 1, -1, 0, 1};
const int dir_dy[9] = {1, 1, 1, 0, 0, 0, -1, -1, -1};

const int cmd_normal[9] = {'b', 'j', 'n', 'h', '.', 'l', 'y', 'k', 'u'};
const int cmd_shift[9]  = {'B', 'J', 'N', 'H', '5', 'L', 'Y', 'K', 'U'};
const int cmd_ctrl[9]   = {CONTROL('B'), CONTROL('J'), CONTROL('N'),
                           CONTROL('H'), 'X', CONTROL('L'),
                           CONTROL('Y'), CONTROL('K'), CONTROL('U')};
const int cmd_dir[9]    = {'1', '2', '3', '4', '5', '6', '7', '8', '9'};

Region::Region() :
    ox(0),
    oy(0),
    dx(0),
    dy(0),
    mx(0),
    my(0),
    wx(0),
    wy(0),
    sx(0),
    sy(0),
    ex(0),
    ey(0)
{
}

void Region::resize(int _mx, int _my)
{
    mx = _mx;
    my = _my;

    recalculate();
}

void Region::place(int _sx, int _sy, int margin)
{
    sx = _sx;
    sy = _sy;
    ox = margin;
    oy = margin;

    recalculate();
}

void Region::resize_to_fit(int _wx, int _wy)
{
    if (_wx < 0 || _wy < 0)
    {
        mx = wx = my = wy = 0;
        ey = sy;
        ex = sy;
        return;
    }

    int inner_x = _wx - 2 * ox;
    int inner_y = _wy - 2 * oy;

    mx = dx ? inner_x / dx : 0;
    my = dy ? inner_y / dy : 0;

    recalculate();

    ex = sx + _wx;
    ey = sy + _wy;
}

void Region::recalculate()
{
    wx = ox * 2 + mx * dx;
    wy = oy * 2 + my * dy;
    ex = sx + wx;
    ey = sy + wy;

    on_resize();
}

Region::~Region()
{
}

bool Region::inside(int x, int y)
{
    return (x >= sx && y >= sy && x <= ex && y <= ey);
}

bool Region::mouse_pos(int mouse_x, int mouse_y, int &cx, int &cy)
{
    int x = mouse_x - ox - sx;
    int y = mouse_y - oy - sy;

    bool valid = (x >= 0 && y >= 0);

    ASSERT(dx > 0);
    ASSERT(dy > 0);
    x /= dx;
    y /= dy;
    valid &= (x < mx && y < my);

    cx = x;
    cy = y;

    return valid;
}

void Region::set_transform()
{
    glLoadIdentity();
    glTranslatef(sx + ox, sy + oy, 0);
    glScalef(dx, dy, 1);
}

TileRegion::TileRegion(ImageManager* im, FTFont *tag_font, int tile_x, int tile_y)
{
    ASSERT(im);
    ASSERT(tag_font);

    m_image = im;
    dx = tile_x;
    dy = tile_y;
    m_tag_font = tag_font;
#if 0
    // Not needed? (jpeg)
    m_dirty = true;
#endif
}

TileRegion::~TileRegion()
{
}

DungeonRegion::DungeonRegion(ImageManager* im, FTFont *tag_font,
                             int tile_x, int tile_y) :
    TileRegion(im, tag_font, tile_x, tile_y),
    m_cx_to_gx(0),
    m_cy_to_gy(0),
    m_buf_dngn(&im->m_textures[TEX_DUNGEON]),
    m_buf_doll(&im->m_textures[TEX_PLAYER]),
    m_buf_main(&im->m_textures[TEX_DEFAULT])
{
    for (int i = 0; i < CURSOR_MAX; i++)
        m_cursor[i] = NO_CURSOR;
}

DungeonRegion::~DungeonRegion()
{
}

void DungeonRegion::load_dungeon(unsigned int* tileb, int cx_to_gx, int cy_to_gy)
{
    m_tileb.clear();
    m_dirty = true;

    if (!tileb)
        return;

    int len = 2 * crawl_view.viewsz.x * crawl_view.viewsz.y;
    m_tileb.resize(len);
    // TODO enne - move this function into dungeonregion
    tile_finish_dngn(tileb, cx_to_gx + mx/2, cy_to_gy + my/2);
    memcpy(&m_tileb[0], tileb, sizeof(unsigned int) * len);

    m_cx_to_gx = cx_to_gx;
    m_cy_to_gy = cy_to_gy;

    place_cursor(CURSOR_TUTORIAL, m_cursor[CURSOR_TUTORIAL]);
}

void DungeonRegion::pack_background(unsigned int bg, int x, int y)
{
    unsigned int bg_idx = bg & TILE_FLAG_MASK;
    if (bg_idx >= TILE_DNGN_WAX_WALL)
    {
        tile_flavour &flv = env.tile_flv[x + m_cx_to_gx][y + m_cy_to_gy];
        m_buf_dngn.add(flv.floor, x, y);
    }

    m_buf_dngn.add(bg_idx, x, y);

    if ((bg & TILE_FLAG_BLOOD) && bg_idx > TILE_DNGN_UNSEEN)
    {
        tile_flavour &flv = env.tile_flv[x + m_cx_to_gx][y + m_cy_to_gy];
        int offset = flv.special % tile_dngn_count(TILE_BLOOD);
        m_buf_dngn.add(TILE_BLOOD + offset, x, y);
    }

    if (bg & TILE_FLAG_HALO)
        m_buf_dngn.add(TILE_HALO, x, y);

    if (bg & TILE_FLAG_SANCTUARY && !(bg & TILE_FLAG_UNSEEN))
        m_buf_dngn.add(TILE_SANCTUARY, x, y);

    // Apply the travel exclusion under the foreground if the cell is
    // visible.  It will be applied later if the cell is unseen.
    if (bg & TILE_FLAG_EXCL_CTR && !(bg & TILE_FLAG_UNSEEN))
        m_buf_dngn.add(TILE_TRAVEL_EXCLUSION_CENTRE_BG, x, y);
    else if (bg & TILE_FLAG_TRAV_EXCL && !(bg & TILE_FLAG_UNSEEN))
        m_buf_dngn.add(TILE_TRAVEL_EXCLUSION_BG, x, y);

    if (bg & TILE_FLAG_RAY)
        m_buf_dngn.add(TILE_RAY, x, y);
    else if (bg & TILE_FLAG_RAY_OOR)
        m_buf_dngn.add(TILE_RAY_OUT_OF_RANGE, x, y);
}

#define NUM_MAX_DOLLS   10
static dolls_data player_doll;
static int gender = -1;

enum tile_doll_mode
{
    TILEP_MODE_EQUIP   = 0,  // draw doll based on equipment
    TILEP_MODE_LOADING = 1,  // draw doll based on dolls.txt definitions
    TILEP_MODE_DEFAULT = 2   // draw doll based on job specific definitions
};

// Loads player doll definitions from (by default) dolls.txt.
// Returns true if file found, else false.
static bool _load_doll_data(const char *fn, dolls_data *dolls, int max,
                            int *mode, int *cur)
{
    char fbuf[1024];
    FILE *fp  = NULL;

    std::string dollsTxtString = datafile_path(fn, false, true);
    const char *dollsTxt = (dollsTxtString.c_str()[0] == 0) ?
                            "dolls.txt" : dollsTxtString.c_str();

    if ( (fp = fopen(dollsTxt, "r")) == NULL )
    {
        // File doesn't exist. By default, use equipment settings.
        *mode = TILEP_MODE_EQUIP;
        return (false);
    }
    else
    {
        memset(fbuf, 0, sizeof(fbuf));
        // Read mode from file.
        if (fscanf(fp, "%s", fbuf) != EOF)
        {
            if (strcmp(fbuf, "MODE=DEFAULT") == 0)
                *mode = (int) TILEP_MODE_DEFAULT;
            else if (strcmp(fbuf, "MODE=EQUIP") == 0)
                *mode = TILEP_MODE_EQUIP; // Nothing else to be done.
        }
        // Read current doll from file.
        if (fscanf(fp, "%s", fbuf) != EOF)
        {
            if (strncmp(fbuf, "NUM=", 4) == 0)
            {
                sscanf(fbuf, "NUM=%d", cur);
                if (*cur < 0 || *cur >= NUM_MAX_DOLLS)
                    *cur = 0;
            }
        }

        if (max == 1)
        {
            // Load only one doll, either the one defined by NUM or
            // use the default/equipment setting.
            if (*mode != TILEP_MODE_LOADING)
            {
                if (gender == -1)
                    gender = coinflip();

                if (*mode == TILEP_MODE_DEFAULT)
                    tilep_job_default(you.char_class, gender, dolls[0].parts);

                // If we don't need to load a doll, return now.
                fclose(fp);
                return (true);
            }

            int count = 0;
            while (fscanf(fp, "%s", fbuf) != EOF)
            {
                if (fbuf[0] == '#') // Skip comment lines.
                    continue;

                if (*cur == count++)
                {
                    tilep_scan_parts(fbuf, dolls[0].parts);
                    gender = get_gender_from_tile(dolls[0].parts);
                    break;
                }
            }
            if (*cur >= count)
            {
                mprf(MSGCH_WARN, "Doll %d could not be found in '%s'.",
                                 *cur, dollsTxt);
            }
        }
        else // Load up to max dolls from file.
        {
            for (int count = 0; count < max && fscanf(fp, "%s", fbuf) != EOF; )
            {
                if (fbuf[0] == '#') // Skip comment lines.
                    continue;

                tilep_scan_parts(fbuf, dolls[count++].parts);
            }
        }

        fclose(fp);
        return (true);
    }
}

void init_player_doll()
{
    dolls_data default_doll[1];

    for (unsigned int i = 0; i < TILEP_PART_MAX; ++i)
        default_doll[0].parts[i] = TILEP_SHOW_EQUIP;

    default_doll[0].parts[TILEP_PART_BASE]
        = tilep_species_to_base_tile(you.species, you.experience_level);

    int mode = TILEP_MODE_LOADING;
    int cur  = 0;
    _load_doll_data("dolls.txt", default_doll, 1, &mode, &cur);

    if (gender == -1)
        gender = coinflip();

    tilep_race_default(you.species, gender, you.experience_level,
                       default_doll[0].parts);

    player_doll = default_doll[0];
}

static int _get_random_doll_part(int p)
{
    ASSERT(p >= 0 && p <= TILEP_PART_MAX);
    return (tile_player_part_start[p]
            + random2(tile_player_part_count[p]));
}

static void _fill_doll_part(dolls_data &doll, int p)
{
    ASSERT(p >= 0 && p <= TILEP_PART_MAX);
    doll.parts[p] = _get_random_doll_part(p);
}

static void _create_random_doll(dolls_data &rdoll)
{
    // All dolls roll for these.
    _fill_doll_part(rdoll, TILEP_PART_BODY);
    _fill_doll_part(rdoll, TILEP_PART_HAND1);
    _fill_doll_part(rdoll, TILEP_PART_LEG);
    _fill_doll_part(rdoll, TILEP_PART_BOOTS);
    _fill_doll_part(rdoll, TILEP_PART_HAIR);

    // The following only are rolled with 50% chance.
    if (coinflip())
        _fill_doll_part(rdoll, TILEP_PART_CLOAK);
    if (coinflip())
        _fill_doll_part(rdoll, TILEP_PART_ARM);
    if (coinflip())
        _fill_doll_part(rdoll, TILEP_PART_HAND2);
    if (coinflip())
        _fill_doll_part(rdoll, TILEP_PART_HELM);

    // Only male dolls get a chance at a beard.
    if (rdoll.parts[TILEP_PART_BASE] % 2 == 1 && one_chance_in(4))
        _fill_doll_part(rdoll, TILEP_PART_BEARD);
}

void _fill_doll_equipment(dolls_data &result)
{
    // Main hand.
    if (result.parts[TILEP_PART_HAND1] == TILEP_SHOW_EQUIP)
    {
        const int item = you.equip[EQ_WEAPON];
        if (you.attribute[ATTR_TRANSFORMATION] == TRAN_BLADE_HANDS)
            result.parts[TILEP_PART_HAND1] = TILEP_HAND1_BLADEHAND;
        else if (item == -1)
            result.parts[TILEP_PART_HAND1] = 0;
        else
            result.parts[TILEP_PART_HAND1] = tilep_equ_weapon(you.inv[item]);
    }
    // Off hand.
    if (result.parts[TILEP_PART_HAND2] == TILEP_SHOW_EQUIP)
    {
        const int item = you.equip[EQ_SHIELD];
        if (you.attribute[ATTR_TRANSFORMATION] == TRAN_BLADE_HANDS)
            result.parts[TILEP_PART_HAND2] = TILEP_HAND2_BLADEHAND;
        else if (item == -1)
            result.parts[TILEP_PART_HAND2] = 0;
        else
            result.parts[TILEP_PART_HAND2] = tilep_equ_shield(you.inv[item]);
    }
    // Body armour.
    if (result.parts[TILEP_PART_BODY] == TILEP_SHOW_EQUIP)
    {
        const int item = you.equip[EQ_BODY_ARMOUR];
        if (item == -1)
            result.parts[TILEP_PART_BODY] = 0;
        else
            result.parts[TILEP_PART_BODY] = tilep_equ_armour(you.inv[item]);
    }
    // Cloak.
    if (result.parts[TILEP_PART_CLOAK] == TILEP_SHOW_EQUIP)
    {
        const int item = you.equip[EQ_CLOAK];
        if (item == -1)
            result.parts[TILEP_PART_CLOAK] = 0;
        else
            result.parts[TILEP_PART_CLOAK] = tilep_equ_cloak(you.inv[item]);
    }
    // Helmet.
    if (result.parts[TILEP_PART_HELM] == TILEP_SHOW_EQUIP)
    {
        const int item = you.equip[EQ_HELMET];
        if (item != -1)
        {
            result.parts[TILEP_PART_HELM] = tilep_equ_helm(you.inv[item]);
        }
        else if (player_mutation_level(MUT_HORNS) > 0)
        {
            switch (player_mutation_level(MUT_HORNS))
            {
                case 1:
                    result.parts[TILEP_PART_HELM] = TILEP_HELM_HORNS1;
                    break;
                case 2:
                    result.parts[TILEP_PART_HELM] = TILEP_HELM_HORNS2;
                    break;
                case 3:
                    result.parts[TILEP_PART_HELM] = TILEP_HELM_HORNS3;
                    break;
            }
        }
        else
        {
            result.parts[TILEP_PART_HELM] = 0;
        }
    }
    // Boots.
    if (result.parts[TILEP_PART_BOOTS] == TILEP_SHOW_EQUIP)
    {
        const int item = you.equip[EQ_BOOTS];
        if (item != -1)
            result.parts[TILEP_PART_BOOTS] = tilep_equ_boots(you.inv[item]);
        else if (player_mutation_level(MUT_HOOVES))
            result.parts[TILEP_PART_BOOTS] = TILEP_BOOTS_HOOVES;
        else
            result.parts[TILEP_PART_BOOTS] = 0;
    }
    // Gloves.
    if (result.parts[TILEP_PART_ARM] == TILEP_SHOW_EQUIP)
    {
        const int item = you.equip[EQ_GLOVES];
        if (item != -1)
            result.parts[TILEP_PART_ARM] = tilep_equ_gloves(you.inv[item]);
        else if (you.has_claws(false) >= 3)
            result.parts[TILEP_PART_ARM] = TILEP_ARM_CLAWS;
        else
            result.parts[TILEP_PART_ARM] = 0;
    }

    // Various other slots.
    if (result.parts[TILEP_PART_LEG] == TILEP_SHOW_EQUIP)
        result.parts[TILEP_PART_LEG] = 0;
    if (result.parts[TILEP_PART_DRCWING] == TILEP_SHOW_EQUIP)
        result.parts[TILEP_PART_DRCWING] = 0;
    if (result.parts[TILEP_PART_DRCHEAD] == TILEP_SHOW_EQUIP)
        result.parts[TILEP_PART_DRCHEAD] = 0;
}

// Allow player to choose between up to 10 premade dolls, or replace them
// with the job default, using current equipment, or a random doll.
// If the player makes any changes while browsing the selections, i.e.
// overwrites a slot or changes the mode, these changes are saved back
// into dolls.txt.
// TODO: Change to proper menu.
void TilePlayerEdit()
{
    // Initialize equipment setting.
    dolls_data equip_doll;
    for (unsigned int i = 0; i < TILEP_PART_MAX; ++i)
         equip_doll.parts[i] = TILEP_SHOW_EQUIP;

    tilep_race_default(you.species, gender,
                       you.experience_level, equip_doll.parts);

    // Initialize job default.
    dolls_data default_doll = equip_doll;
    tilep_job_default(you.char_class, gender, default_doll.parts);

    // Read predefined dolls from file.
    dolls_data dolls[NUM_MAX_DOLLS];
    for (unsigned int i = 0; i < NUM_MAX_DOLLS; ++i)
         dolls[i] = equip_doll;

    int old_mode = TILEP_MODE_LOADING;
    int old_num  = -1;
    const bool found_file = _load_doll_data("dolls.txt", dolls, NUM_MAX_DOLLS,
                                            &old_mode, &old_num);
    int mode = old_mode;
    int num  = old_num;

    bool display_num   = (found_file && mode == TILEP_MODE_LOADING);
    bool dolls_changed = false;
    bool did_random    = false;

    while (true)
    {
        // Offer a list of choices.
        std::vector<std::string> prompt;
        prompt.push_back("Pick (d)efault doll for job, use (e)quipment settings");
        prompt.push_back("roll a (r)andom doll");
        if (found_file)
            prompt.push_back("load doll (0-9) from file");
        if (did_random || mode != TILEP_MODE_LOADING)
            prompt.push_back("(s)ave current doll");

        std::string current = "";
        bool need_braces = false;
        if (did_random || mode != TILEP_MODE_LOADING)
        {
            current = (did_random                 ? "random" :
                       mode == TILEP_MODE_EQUIP   ? "equipment" :
                       mode == TILEP_MODE_DEFAULT ? "default" : "buggy");

            need_braces = true;
        }

        if (display_num)
        {
            snprintf(info, INFO_SIZE, "doll %d", num);
            if (need_braces)
                current += " [";
            current += info;
            if (need_braces)
                current += "]";
        }

        mprf(MSGCH_PROMPT, "%s? (Current: %s)",
             comma_separated_line(prompt.begin(),
                                  prompt.end(), ", or ", ", ").c_str(),
             current.c_str());

        char ch = (char) getchm(KMC_DEFAULT);
             ch = tolower(ch);

        bool finish = false;
        did_random  = false;
        if (ch == 'd')
        {
            player_doll = default_doll;
            mode        = TILEP_MODE_DEFAULT;
        }
        else if (ch == 'e')
        {
            player_doll = equip_doll;
            mode        = TILEP_MODE_EQUIP;
        }
        else if (ch == 'r')
        {
            dolls_data random_doll = equip_doll;
            _create_random_doll(random_doll);
            player_doll = random_doll;
            did_random  = true;
        }
        else if (ch == 's')
        {
            mpr("Save current doll to what slot? (0-9)", MSGCH_PROMPT);
            const char ch2 = (char) getchm(KMC_DEFAULT);
            if (ch2 >= '0' && ch2 <= '9')
            {
                const int slot = ch2 - '0';
                ASSERT(slot >= 0 && slot <= 9);

                // Translate TILEP_SHOW_EQUIP according to equipment.
                _fill_doll_equipment(player_doll);
                dolls[slot]   = player_doll;
                num           = slot;
                did_random    = false;
                dolls_changed = true;
                display_num   = true;
                mprf("Doll saved to slot %d.", slot);
            }
            else
                mpr("Doll not saved.");
            more();
        }
        else if (ch >= '0' && ch <= '9')
        {
            // HACK until I can find the proper function.
            num = ch - '0';
            ASSERT(num >= 0 && num <= 9);
            ASSERT(num < NUM_MAX_DOLLS);

            player_doll = dolls[num];
            mode        = TILEP_MODE_LOADING;
            display_num = true;
        }
        else // All other input exits the loop.
            finish = true;

        gender = get_gender_from_tile(player_doll.parts);

        mesclr();

        if (finish)
        {
            // No changes, nothing to be saved.
            if (!dolls_changed && mode == old_mode && num == old_num)
                break;

            // Save mode, num, and all dolls into dolls.txt.
            std::string dollsTxtString = datafile_path("dolls.txt", false, true);
            const char *dollsTxt = (dollsTxtString.c_str()[0] == 0) ?
                                    "dolls.txt" : dollsTxtString.c_str();

            FILE *fp = NULL;
            if ( (fp = fopen(dollsTxt, "w+")) != NULL )
            {
                fprintf(fp, "MODE=%s\n",
                            (mode == TILEP_MODE_EQUIP)   ? "EQUIP" :
                            (mode == TILEP_MODE_LOADING) ? "LOADING"
                                                         : "DEFAULT");

                fprintf(fp, "NUM=%02d\n", num == -1 ? 0 : num);

                // Print some explanatory comments. May contain no spaces!
                fprintf(fp, "#Legend:\n");
                fprintf(fp, "#***:equipment/123:index/000:none\n");
                fprintf(fp, "#Shadow/Gender/Cloak/Boots/Legs/Body/Gloves/Weapon/Shield/Hair/Beard/Helmet\n");
                fprintf(fp, "#--:Sex:Clk:Bts:Leg:Bdy:Glv:Wpn:Shd:Hai:Brd:Hlm\n");

                char fbuf[80];
                for (unsigned int i = 0; i < NUM_MAX_DOLLS; ++i)
                {
                    tilep_print_parts(fbuf, dolls[i].parts);
                    fprintf(fp, "%s\n", fbuf);
                }
                fclose(fp);
            }
            else
                mprf(MSGCH_ERROR, "Could not write into file '%s'.", dollsTxt);

            break;
        }

        viewwindow(true, false);
    }
}

void DungeonRegion::pack_player(int x, int y)
{
    dolls_data result = player_doll;

    const bool halo = inside_halo(you.pos());
    result.parts[TILEP_PART_HALO] = halo ? TILEP_HALO_TSO : 0;
    result.parts[TILEP_PART_ENCH] =
        (you.duration[DUR_LIQUID_FLAMES] ? TILEP_ENCH_STICKY_FLAME : 0);

    _fill_doll_equipment(result);
    pack_doll(result, x, y);
}

void DungeonRegion::pack_doll(const dolls_data &doll, int x, int y)
{
    int p_order[TILEP_PART_MAX] =
    {
        TILEP_PART_SHADOW,  //  0
        TILEP_PART_HALO,
        TILEP_PART_ENCH,
        TILEP_PART_DRCWING,
        TILEP_PART_CLOAK,
        TILEP_PART_BASE,    //  5
        TILEP_PART_BOOTS,
        TILEP_PART_LEG,
        TILEP_PART_BODY,
        TILEP_PART_ARM,
        TILEP_PART_HAND1,   // 10
        TILEP_PART_HAND2,
        TILEP_PART_HAIR,
        TILEP_PART_BEARD,
        TILEP_PART_HELM,
        TILEP_PART_DRCHEAD  // 15
    };

    int flags[TILEP_PART_MAX];
    tilep_calc_flags(doll.parts, flags);

    // For skirts, boots go under the leg armour.  For pants, they go over.
    if (doll.parts[TILEP_PART_LEG] < TILEP_LEG_SKIRT_OFS)
    {
        p_order[6] = TILEP_PART_BOOTS;
        p_order[7] = TILEP_PART_LEG;
    }

    for (int i = 0; i < TILEP_PART_MAX; i++)
    {
        int p = p_order[i];
        if (!doll.parts[p] || flags[p] == TILEP_FLAG_HIDE)
            continue;

        int ymax = TILE_Y;

        if (flags[p] == TILEP_FLAG_CUT_CENTAUR
            || flags[p] == TILEP_FLAG_CUT_NAGA)
        {
            ymax = 18;
        }

        m_buf_doll.add(doll.parts[p], x, y, 0, 0, true, ymax);
    }
}

void DungeonRegion::pack_mcache(mcache_entry *entry, int x, int y)
{
    ASSERT(entry);

    const dolls_data *doll = entry->doll();
    if (doll)
        pack_doll(*doll, x, y);

    tile_draw_info dinfo[3];
    unsigned int draw_info_count = entry->info(&dinfo[0]);
    ASSERT(draw_info_count <= sizeof(dinfo) / (sizeof(dinfo[0])));

    for (unsigned int i = 0; i < draw_info_count; i++)
        m_buf_doll.add(dinfo[i].idx, x, y, dinfo[i].ofs_x, dinfo[i].ofs_y);
}

void DungeonRegion::pack_foreground(unsigned int bg, unsigned int fg, int x, int y)
{
    unsigned int fg_idx = fg & TILE_FLAG_MASK;
    unsigned int bg_idx = bg & TILE_FLAG_MASK;

    if (fg_idx && fg_idx <= TILE_MAIN_MAX)
        m_buf_main.add(fg_idx, x, y);

    if (fg_idx && !(fg & TILE_FLAG_FLYING))
    {
        if (tile_dngn_equal(TILE_DNGN_LAVA, bg_idx))
            m_buf_main.add(TILE_MASK_LAVA, x, y);
        else if (tile_dngn_equal(TILE_DNGN_SHALLOW_WATER, bg_idx))
            m_buf_main.add(TILE_MASK_SHALLOW_WATER, x, y);
        else if (tile_dngn_equal(TILE_DNGN_SHALLOW_WATER_MURKY, bg_idx))
            m_buf_main.add(TILE_MASK_SHALLOW_WATER_MURKY, x, y);
        else if (tile_dngn_equal(TILE_DNGN_DEEP_WATER, bg_idx))
            m_buf_main.add(TILE_MASK_DEEP_WATER, x, y);
        else if (tile_dngn_equal(TILE_DNGN_DEEP_WATER_MURKY, bg_idx))
            m_buf_main.add(TILE_MASK_DEEP_WATER_MURKY, x, y);
    }

    if (fg & TILE_FLAG_NET)
        m_buf_main.add(TILE_TRAP_NET, x, y);

    if (fg & TILE_FLAG_S_UNDER)
        m_buf_main.add(TILE_SOMETHING_UNDER, x, y);

    // Pet mark
    int status_shift = 0;
    if (fg & TILE_FLAG_PET)
    {
        m_buf_main.add(TILE_HEART, x, y);
        status_shift += 10;
    }
    else if ((fg & TILE_FLAG_MAY_STAB) == TILE_FLAG_NEUTRAL)
    {
        m_buf_main.add(TILE_NEUTRAL, x, y);
        status_shift += 8;
    }
    else if ((fg & TILE_FLAG_MAY_STAB) == TILE_FLAG_STAB)
    {
        m_buf_main.add(TILE_STAB_BRAND, x, y);
        status_shift += 8;
    }
    else if ((fg & TILE_FLAG_MAY_STAB) == TILE_FLAG_MAY_STAB)
    {
        m_buf_main.add(TILE_MAY_STAB_BRAND, x, y);
        status_shift += 5;
    }

    if (fg & TILE_FLAG_POISON)
    {
        m_buf_main.add(TILE_POISON, x, y, -status_shift, 0);
        status_shift += 5;
    }
    if (fg & TILE_FLAG_FLAME)
    {
        m_buf_main.add(TILE_FLAME, x, y, -status_shift, 0);
        status_shift += 5;
    }

    if (fg & TILE_FLAG_ANIM_WEP)
        m_buf_main.add(TILE_ANIMATED_WEAPON, x, y);

    if (bg & TILE_FLAG_UNSEEN && (bg != TILE_FLAG_UNSEEN || fg))
        m_buf_main.add(TILE_MESH, x, y);

    if (bg & TILE_FLAG_OOR && (bg != TILE_FLAG_OOR || fg))
        m_buf_main.add(TILE_OOR_MESH, x, y);

    if (bg & TILE_FLAG_MM_UNSEEN && (bg != TILE_FLAG_MM_UNSEEN || fg))
        m_buf_main.add(TILE_MAGIC_MAP_MESH, x, y);

    // Don't let the "new stair" icon cover up any existing icons, but
    // draw it otherwise.
    if (bg & TILE_FLAG_NEW_STAIR && status_shift == 0)
        m_buf_main.add(TILE_NEW_STAIR, x, y);

    if (bg & TILE_FLAG_EXCL_CTR && (bg & TILE_FLAG_UNSEEN))
        m_buf_main.add(TILE_TRAVEL_EXCLUSION_CENTRE_FG, x, y);
    else if (bg & TILE_FLAG_TRAV_EXCL && (bg & TILE_FLAG_UNSEEN))
        m_buf_main.add(TILE_TRAVEL_EXCLUSION_FG, x, y);

    // Tutorial cursor takes precedence over other cursors.
    if (bg & TILE_FLAG_TUT_CURSOR)
    {
        m_buf_main.add(TILE_TUTORIAL_CURSOR, x, y);
    }
    else if (bg & TILE_FLAG_CURSOR)
    {
        int type = ((bg & TILE_FLAG_CURSOR) == TILE_FLAG_CURSOR1) ?
            TILE_CURSOR : TILE_CURSOR2;

        if ((bg & TILE_FLAG_CURSOR) == TILE_FLAG_CURSOR3)
           type = TILE_CURSOR3;

        m_buf_main.add(type, x, y);
    }

    if (fg & TILE_FLAG_MDAM_MASK)
    {
        unsigned int mdam_flag = fg & TILE_FLAG_MDAM_MASK;
        if (mdam_flag == TILE_FLAG_MDAM_LIGHT)
            m_buf_main.add(TILE_MDAM_LIGHTLY_DAMAGED, x, y);
        else if (mdam_flag == TILE_FLAG_MDAM_MOD)
            m_buf_main.add(TILE_MDAM_MODERATELY_DAMAGED, x, y);
        else if (mdam_flag == TILE_FLAG_MDAM_HEAVY)
            m_buf_main.add(TILE_MDAM_HEAVILY_DAMAGED, x, y);
        else if (mdam_flag == TILE_FLAG_MDAM_SEV)
            m_buf_main.add(TILE_MDAM_SEVERELY_DAMAGED, x, y);
        else if (mdam_flag == TILE_FLAG_MDAM_ADEAD)
            m_buf_main.add(TILE_MDAM_ALMOST_DEAD, x, y);
    }
}

void DungeonRegion::pack_cursor(cursor_type type, unsigned int tile)
{
    const coord_def &gc = m_cursor[type];
    if (gc == NO_CURSOR || !on_screen(gc))
        return;

    m_buf_main.add(tile, gc.x - m_cx_to_gx, gc.y - m_cy_to_gy);
}

void DungeonRegion::pack_buffers()
{
    m_buf_dngn.clear();
    m_buf_doll.clear();
    m_buf_main.clear();

    if (m_tileb.empty())
        return;

    int tile = 0;
    for (int y = 0; y < crawl_view.viewsz.y; y++)
        for (int x = 0; x < crawl_view.viewsz.x; x++)
        {
            unsigned int bg = m_tileb[tile + 1];
            unsigned int fg = m_tileb[tile];
            unsigned int fg_idx = fg & TILE_FLAG_MASK;

            pack_background(bg, x, y);

            if (fg_idx >= TILEP_MCACHE_START)
            {
                mcache_entry *entry = mcache.get(fg_idx);
                if (entry)
                    pack_mcache(entry, x, y);
                else
                    m_buf_doll.add(TILEP_MONS_UNKNOWN, x, y);
            }
            else if (fg_idx == TILEP_PLAYER)
            {
                pack_player(x, y);
            }
            else if (fg_idx >= TILE_MAIN_MAX)
            {
                m_buf_doll.add(fg_idx, x, y);
            }

            pack_foreground(bg, fg, x, y);

            tile += 2;
        }

    pack_cursor(CURSOR_TUTORIAL, TILE_TUTORIAL_CURSOR);
    pack_cursor(CURSOR_MOUSE, see_grid(m_cursor[CURSOR_MOUSE]) ? TILE_CURSOR
                                                               : TILE_CURSOR2);

    if (m_cursor[CURSOR_TUTORIAL] != NO_CURSOR
        && on_screen(m_cursor[CURSOR_TUTORIAL]))
    {
        m_buf_main.add(TILE_TUTORIAL_CURSOR, m_cursor[CURSOR_TUTORIAL].x,
                                             m_cursor[CURSOR_TUTORIAL].y);
    }

    for (unsigned int i = 0; i < m_overlays.size(); i++)
    {
        // overlays must be from the main image and must be in LOS.
        if (!crawl_view.in_grid_los(m_overlays[i].gc))
            continue;

        int idx = m_overlays[i].idx;
        if (idx >= TILE_MAIN_MAX)
            continue;

        int x = m_overlays[i].gc.x - m_cx_to_gx;
        int y = m_overlays[i].gc.y - m_cy_to_gy;
        m_buf_main.add(idx, x, y);
    }
}

struct tag_def
{
    tag_def() { text = NULL; left = right = 0; }

    const char* text;
    char left, right;
    char type;
};

// #define DEBUG_TILES_REDRAW
void DungeonRegion::render()
{
#ifdef DEBUG_TILES_REDRAW
    cprintf("rendering DungeonRegion\n");
#endif
    if (m_dirty)
    {
        pack_buffers();
        m_dirty = false;
    }

    set_transform();
    m_buf_dngn.draw();
    m_buf_doll.draw();
    m_buf_main.draw();

    if (you.duration[DUR_BERSERKER])
    {
        ShapeBuffer buff;
        VColour red_film(130, 0, 0, 100);
        buff.add(0, 0, mx, my, red_film);
        buff.draw();
    }

    FixedArray<tag_def, ENV_SHOW_DIAMETER, ENV_SHOW_DIAMETER> tag_show;

    int total_tags = 0;

    for (int t = TAG_MAX - 1; t >= 0; t--)
    {
        for (unsigned int i = 0; i < m_tags[t].size(); i++)
        {
            if (!crawl_view.in_grid_los(m_tags[t][i].gc))
                continue;

            const coord_def ep = grid2show(m_tags[t][i].gc);

            if (tag_show(ep).text)
                continue;

            const char *str = m_tags[t][i].tag.c_str();

            int width    = m_tag_font->string_width(str);
            tag_def &def = tag_show(ep);

            const int buffer = 2;

            def.left  = -width / 2 - buffer;
            def.right =  width / 2 + buffer;
            def.text  = str;
            def.type  = t;

            total_tags++;
        }

        if (total_tags)
            break;
    }

    if (!total_tags)
        return;

    // Draw text tags.
    // TODO enne - be more intelligent about not covering stuff up
    for (int y = 0; y < ENV_SHOW_DIAMETER; y++)
        for (int x = 0; x < ENV_SHOW_DIAMETER; x++)
        {
            coord_def ep(x, y);
            tag_def &def = tag_show(ep);

            if (!def.text)
                continue;

            const coord_def gc = show2grid(ep);
            coord_def pc;
            to_screen_coords(gc, pc);
            // center this coord, which is at the top left of gc's cell
            pc.x += dx / 2;

            const coord_def min_pos(sx, sy);
            const coord_def max_pos(ex, ey);
            m_tag_font->render_string(pc.x, pc.y, def.text,
                                      min_pos, max_pos, WHITE, false);
        }
}

void DungeonRegion::clear()
{
    m_tileb.clear();
}

void DungeonRegion::on_resize()
{
    // TODO enne
}

static int _adjacent_cmd(const coord_def &gc, const MouseEvent &event)
{
    coord_def dir = gc - you.pos();
    for (int i = 0; i < 9; i++)
    {
        if (dir_dx[i] != dir.x || dir_dy[i] != dir.y)
            continue;

        if (event.mod & MOD_SHIFT)
            return cmd_shift[i];
        else if (event.mod & MOD_CTRL)
            return cmd_ctrl[i];
        else
            return cmd_normal[i];
    }

    return 0;
}

static int _click_travel(const coord_def &gc, MouseEvent &event)
{
    if (!in_bounds(gc))
        return 0;

    int cmd = _adjacent_cmd(gc, event);
    if (cmd)
        return cmd;

    if (i_feel_safe())
    {
        start_travel(gc);
        return CK_MOUSE_CMD;
    }

    // If not safe, then take one step towards the click.
    travel_pathfind tp;
    tp.set_src_dst(you.pos(), gc);
    const coord_def dest = tp.pathfind(RMODE_TRAVEL);

    if (!dest.x && !dest.y)
        return 0;

    return _adjacent_cmd(dest, event);
}

int DungeonRegion::handle_mouse(MouseEvent &event)
{
    tiles.clear_text_tags(TAG_CELL_DESC);

    if (!inside(event.px, event.py))
        return 0;

    if (mouse_control::current_mode() == MOUSE_MODE_NORMAL
        && event.event == MouseEvent::PRESS
        && event.button == MouseEvent::LEFT)
    {
        you.last_clicked_grid = m_cursor[CURSOR_MOUSE];
        return CK_MOUSE_CLICK;
    }

    if (mouse_control::current_mode() == MOUSE_MODE_NORMAL
        || mouse_control::current_mode() == MOUSE_MODE_MACRO
        || mouse_control::current_mode() == MOUSE_MODE_MORE)
    {
        return 0;
    }

    int cx;
    int cy;

    bool on_map = mouse_pos(event.px, event.py, cx, cy);

    const coord_def gc(cx + m_cx_to_gx, cy + m_cy_to_gy);
    tiles.place_cursor(CURSOR_MOUSE, gc);

    if (event.event == MouseEvent::MOVE)
    {
        const std::string &desc = get_terse_square_desc(gc);
        // Suppress floor description
        if (desc != "floor")
            tiles.add_text_tag(TAG_CELL_DESC, desc, gc);
    }

    if (!on_map)
        return 0;

    if (mouse_control::current_mode() == MOUSE_MODE_TARGET
        || mouse_control::current_mode() == MOUSE_MODE_TARGET_PATH
        || mouse_control::current_mode() == MOUSE_MODE_TARGET_DIR)
    {
        if (event.event == MouseEvent::MOVE)
        {
            return CK_MOUSE_MOVE;
        }
        else if (event.event == MouseEvent::PRESS
                 && event.button == MouseEvent::LEFT && on_screen(gc))
        {
            you.last_clicked_grid = m_cursor[CURSOR_MOUSE];
            return CK_MOUSE_CLICK;
        }

        return 0;
    }

    if (event.event != MouseEvent::PRESS)
        return 0;

    you.last_clicked_grid = m_cursor[CURSOR_MOUSE];

    if (you.pos() == gc)
    {
        switch (event.button)
        {
        case MouseEvent::LEFT:
        {
            if (!(event.mod & MOD_SHIFT))
                return 'g';

            const dungeon_feature_type grid = grd(gc);
            switch (grid_stair_direction(grid))
            {
            case CMD_GO_DOWNSTAIRS:
                return ('>');
            case CMD_GO_UPSTAIRS:
                return ('<');
            default:
                if (is_altar(grid)
                    && player_can_join_god(grid_altar_god(grid)))
                {
                    return ('p');
                }
                return 0;
            }
        }
        case MouseEvent::RIGHT:
            if (!(event.mod & MOD_SHIFT))
                return '%'; // Character overview.
            if (you.religion != GOD_NO_GOD)
                return '^'; // Religion screen.

            // fall through...
        default:
            return 0;
        }

    }
    // else not on player...
    if (event.button == MouseEvent::RIGHT)
    {
        full_describe_square(gc);
        return CK_MOUSE_CMD;
    }

    if (event.button != MouseEvent::LEFT)
        return 0;

    return _click_travel(gc, event);
}

void DungeonRegion::to_screen_coords(const coord_def &gc, coord_def &pc) const
{
    int cx = gc.x - m_cx_to_gx;
    int cy = gc.y - m_cy_to_gy;

    pc.x = sx + ox + cx * dx;
    pc.y = sy + oy + cy * dy;
}

bool DungeonRegion::on_screen(const coord_def &gc) const
{
    int x = gc.x - m_cx_to_gx;
    int y = gc.y - m_cy_to_gy;

    return (x >= 0 && x < mx && y >= 0 && y < my);
}

// Returns the index into m_tileb for the foreground tile.
// This value may not be valid.  Check on_screen() first.
// Add one to the return value to get the background tile idx.
int DungeonRegion::get_buffer_index(const coord_def &gc)
{
    int x = gc.x - m_cx_to_gx;
    int y = gc.y - m_cy_to_gy;
    return 2 * (x + y * mx);
}

void DungeonRegion::place_cursor(cursor_type type, const coord_def &gc)
{
    coord_def result = gc;

    // If we're only looking for a direction, put the mouse
    // cursor next to the player to let them know that their
    // spell/wand will only go one square.
    if (mouse_control::current_mode() == MOUSE_MODE_TARGET_DIR
        && type == CURSOR_MOUSE && gc != NO_CURSOR)
    {
        coord_def delta = gc - you.pos();

        int ax = abs(delta.x);
        int ay = abs(delta.y);

        result = you.pos();
        if (1000 * ay < 414 * ax)
            result += (delta.x > 0) ? coord_def(1, 0) : coord_def(-1, 0);
        else if (1000 * ax < 414 * ay)
            result += (delta.y > 0) ? coord_def(0, 1) : coord_def(0, -1);
        else if (delta.x > 0)
            result += (delta.y > 0) ? coord_def(1, 1) : coord_def(1, -1);
        else if (delta.x < 0)
            result += (delta.y > 0) ? coord_def(-1, 1) : coord_def(-1, -1);
    }

    if (m_cursor[type] != result)
    {
        m_dirty = true;
        m_cursor[type] = result;
        if (type == CURSOR_MOUSE)
            you.last_clicked_grid = coord_def();
    }
}

bool DungeonRegion::update_tip_text(std::string& tip)
{
    // TODO enne - it would be really nice to use the tutorial
    // descriptions here for features, monsters, etc...
    // Unfortunately, that would require quite a bit of rewriting
    // and some parsing of formatting to get that to work.

    if (mouse_control::current_mode() != MOUSE_MODE_COMMAND)
        return (false);

    if (m_cursor[CURSOR_MOUSE] == NO_CURSOR)
        return (false);
    if (!map_bounds(m_cursor[CURSOR_MOUSE]))
        return (false);

    if (m_cursor[CURSOR_MOUSE] == you.pos())
    {
        tip = you.your_name;
        tip += " (";
        tip += get_species_abbrev(you.species);
        tip += get_class_abbrev(you.char_class);
        tip += ")";

        if (igrd(m_cursor[CURSOR_MOUSE]) != NON_ITEM)
            tip += "\n[L-Click] Pick up items (g)";

        const dungeon_feature_type grid = grd(m_cursor[CURSOR_MOUSE]);
        const command_type dir = grid_stair_direction(grid);
        if (dir != CMD_NO_CMD)
        {
            tip += "\n[Shift-L-Click] ";
            if (grid == DNGN_ENTER_SHOP)
                tip += "enter shop";
            else if (is_gate(grid))
                tip += "enter gate";
            else
                tip += "use stairs";

            if (dir == CMD_GO_DOWNSTAIRS)
                tip += " (>)";
            else
                tip += " (<)";
        }
        else if (is_altar(grid) && player_can_join_god(grid_altar_god(grid)))
            tip += "\n[Shift-L-Click] pray on altar (p)";

        // Character overview.
        tip += "\n[R-Click] Overview (%)";

        // Religion.
        if (you.religion != GOD_NO_GOD)
            tip += "\n[Shift-R-Click] Religion (^)";
    }
    else if (abs(m_cursor[CURSOR_MOUSE].x - you.pos().x) <= 1
             && abs(m_cursor[CURSOR_MOUSE].y - you.pos().y) <= 1)
    {
        tip = "";

        if (!grid_is_solid(m_cursor[CURSOR_MOUSE]))
        {
            int mon_num = mgrd(m_cursor[CURSOR_MOUSE]);
            if (mon_num == NON_MONSTER || mons_friendly(&menv[mon_num]))
            {
                tip = "[L-Click] Move\n";
            }
            else if (mon_num != NON_MONSTER)
            {
                tip = menv[mon_num].name(DESC_CAP_A);
                tip += "\n[L-Click] Attack\n";
            }
        }

        tip += "[R-Click] Describe";
    }
    else
    {
        if (i_feel_safe() && !grid_is_solid(m_cursor[CURSOR_MOUSE]))
            tip = "[L-Click] Travel\n";

        tip += "[R-Click] Describe";
    }

    return (true);
}

class alt_desc_proc
{
public:
    alt_desc_proc(int _w, int _h) { w = _w; h = _h; }

    int width() { return w; }
    int height() { return h; }
    void nextline()
    {
        ostr << "\n";
    }
    void print(const std::string &str)
    {
        ostr << str;
    }

    static int count_newlines(const std::string &str)
    {
        int count = 0;
        for (size_t i = 0; i < str.size(); i++)
        {
            if (str[i] == '\n')
                count++;
        }
        return count;
    }

    // Remove trailing newlines.
    static void trim(std::string &str)
    {
        int idx = str.size();
        while (--idx >= 0)
        {
            if (str[idx] != '\n')
                break;
        }
        str.resize(idx + 1);
    }

    // rfind consecutive newlines and truncate.
    static bool chop(std::string &str)
    {
        int loc = -1;
        for (size_t i = 1; i < str.size(); i++)
            if (str[i] == '\n' && str[i-1] == '\n')
                loc = i;

        if (loc == -1)
            return (false);

        str.resize(loc);
        return (true);
    }

    void get_string(std::string &str)
    {
        str = replace_all(ostr.str(), "\n\n\n\n", "\n\n");
        str = replace_all(str, "\n\n\n", "\n\n");

        trim(str);
        while (count_newlines(str) > h)
        {
            if (!chop(str))
                break;
        }
    }

protected:
    int w;
    int h;
    std::ostringstream ostr;
};

bool DungeonRegion::update_alt_text(std::string &alt)
{
    if (mouse_control::current_mode() != MOUSE_MODE_COMMAND)
        return (false);

    const coord_def &gc = m_cursor[CURSOR_MOUSE];

    if (gc == NO_CURSOR)
        return (false);
    if (!map_bounds(gc))
        return (false);
    if (!is_terrain_seen(gc))
        return (false);
    if (you.last_clicked_grid == gc)
        return (false);

    describe_info inf;
    if (see_grid(gc))
        get_square_desc(gc, inf, true);
    else if (grd(gc) != DNGN_FLOOR)
        get_feature_desc(gc, inf);
    else
    {
        // For plain floor, output the stash description.
        std::string stash = get_stash_desc(gc.x, gc.y);
        if (!stash.empty())
            inf.body << "$" << stash;
    }

    alt_desc_proc proc(crawl_view.msgsz.x, crawl_view.msgsz.y);
    process_description<alt_desc_proc>(proc, inf);

    proc.get_string(alt);

    // Suppress floor description
    if (alt == "Floor.")
    {
        alt.clear();
        return (false);
    }

    return (true);
}

void DungeonRegion::clear_text_tags(text_tag_type type)
{
    m_tags[type].clear();
}

void DungeonRegion::add_text_tag(text_tag_type type, const std::string &tag,
                                 const coord_def &gc)
{
    TextTag t;
    t.tag = tag;
    t.gc  = gc;

    m_tags[type].push_back(t);
}

void DungeonRegion::add_overlay(const coord_def &gc, int idx)
{
    tile_overlay over;
    over.gc  = gc;
    over.idx = idx;

    m_overlays.push_back(over);
    m_dirty = true;
}

void DungeonRegion::clear_overlays()
{
    m_overlays.clear();
    m_dirty = true;
}

InventoryTile::InventoryTile()
{
    tile     = 0;
    idx      = -1;
    quantity = -1;
    key      = 0;
    flag     = 0;
    special  = 0;
}

bool InventoryTile::empty() const
{
    return (idx == -1);
}

InventoryRegion::InventoryRegion(ImageManager* im, FTFont *tag_font,
                                 int tile_x, int tile_y) :
    TileRegion(im, tag_font, tile_x, tile_y),
    m_flavour(NULL),
    m_buf_dngn(&im->m_textures[TEX_DUNGEON]),
    m_buf_main(&im->m_textures[TEX_DEFAULT]),
    m_cursor(NO_CURSOR)
{
}

InventoryRegion::~InventoryRegion()
{
    delete[] m_flavour;
    m_flavour = NULL;
}

void InventoryRegion::clear()
{
    m_items.clear();
    m_buf_dngn.clear();
    m_buf_main.clear();
}

void InventoryRegion::on_resize()
{
    delete[] m_flavour;
    if (mx * my <= 0)
        return;

    m_flavour = new unsigned char[mx * my];
    for (int i = 0; i < mx * my; ++i)
        m_flavour[i] = random2((unsigned char)~0);
}

void InventoryRegion::update(int num, InventoryTile *items)
{
    m_items.clear();
    for (int i = 0; i < num; i++)
        m_items.push_back(items[i]);

    m_dirty = true;
}

void InventoryRegion::update_slot(int slot, InventoryTile &desc)
{
    while (m_items.size() <= (unsigned int)slot)
    {
        InventoryTile temp;
        m_items.push_back(temp);
    }

    m_items[slot] = desc;
#if 0
    // Not needed? (jpeg)
    m_dirty = true;
#endif
}

void InventoryRegion::render()
{
    if (m_dirty)
    {
        pack_buffers();
        m_dirty = false;
    }

    if (m_buf_dngn.empty() && m_buf_main.empty())
        return;

#ifdef DEBUG_TILES_REDRAW
    cprintf("rendering InventoryRegion\n");
#endif
    set_transform();
    m_buf_dngn.draw();
    m_buf_main.draw();

    if (m_cursor != NO_CURSOR)
    {
        unsigned int curs_index = cursor_index();
        if (curs_index >= m_items.size())
            return;
        int idx = m_items[curs_index].idx;
        if (idx == -1)
            return;

        bool floor = m_items[curs_index].flag & TILEI_FLAG_FLOOR;

        int x = m_cursor.x * dx + sx + ox + dx / 2;
        int y = m_cursor.y * dy + sy + oy;

        const coord_def min_pos(sx, sy - dy);
        const coord_def max_pos(ex, ey);

        std::string desc = "";
        if (floor && is_valid_item(mitm[idx]))
            desc = mitm[idx].name(DESC_PLAIN);
        else if (!floor && is_valid_item(you.inv[idx]))
            desc = you.inv[idx].name(DESC_INVENTORY_EQUIP);

        if (!desc.empty())
        {
            m_tag_font->render_string(x, y, desc.c_str(),
                                      min_pos, max_pos, WHITE, false, 200);
        }
    }
}

void InventoryRegion::add_quad_char(char c, int x, int y, int ofs_x, int ofs_y)
{
    int num = c - '0';
    assert(num >= 0 && num <= 9);
    int idx = TILE_NUM0 + num;

    m_buf_main.add(idx, x, y, ofs_x, ofs_y, false);
}

void InventoryRegion::pack_buffers()
{
    m_buf_dngn.clear();
    m_buf_main.clear();

    // Ensure the cursor has been placed.
    place_cursor(m_cursor);

    // Pack base separately, as it comes from a different texture...
    unsigned int i = 0;
    for (int y = 0; y < my; y++)
    {
        if (i >= m_items.size())
            break;

        for (int x = 0; x < mx; x++)
        {
            if (i >= m_items.size())
                break;

            InventoryTile &item = m_items[i++];

            if (item.flag & TILEI_FLAG_FLOOR)
            {
                if (i >= (unsigned int) mx * my)
                    break;

                int num_floor = tile_dngn_count(env.tile_default.floor);
                m_buf_dngn.add(env.tile_default.floor
                                + m_flavour[i] % num_floor, x, y);
            }
            else
                m_buf_dngn.add(TILE_ITEM_SLOT, x, y);
        }
    }

    i = 0;
    for (int y = 0; y < my; y++)
    {
        if (i >= m_items.size())
            break;

        for (int x = 0; x < mx; x++)
        {
            if (i >= m_items.size())
                break;

            InventoryTile &item = m_items[i++];

            if (item.flag & TILEI_FLAG_EQUIP)
            {
                if (item.flag & TILEI_FLAG_CURSE)
                    m_buf_main.add(TILE_ITEM_SLOT_EQUIP_CURSED, x, y);
                else
                    m_buf_main.add(TILE_ITEM_SLOT_EQUIP, x, y);

                if (item.flag & TILEI_FLAG_MELDED)
                    m_buf_main.add(TILE_MESH, x, y);
            }
            else if (item.flag & TILEI_FLAG_CURSE)
                m_buf_main.add(TILE_ITEM_SLOT_CURSED, x, y);

            // TODO enne - need better graphic here
            if (item.flag & TILEI_FLAG_SELECT)
                m_buf_main.add(TILE_ITEM_SLOT_SELECTED, x, y);

            if (item.flag & TILEI_FLAG_CURSOR)
                m_buf_main.add(TILE_CURSOR, x, y);

            if (item.tile)
                m_buf_main.add(item.tile, x, y);

            if (item.quantity != -1)
            {
                int num = item.quantity;
                // If you have that many, who cares.
                if (num > 999)
                    num = 999;

                const int offset_amount = TILE_X/4;
                int offset_x = 3;
                int offset_y = 1;

                int help = num;
                int c100 = help/100;
                help -= c100*100;

                if (c100)
                {
                    add_quad_char('0' + c100, x, y, offset_x, offset_y);
                    offset_x += offset_amount;
                }

                int c10 = help/10;
                if (c10 || c100)
                {
                    add_quad_char('0' + c10, x, y, offset_x, offset_y);
                    offset_x += offset_amount;
                }

                int c1 = help % 10;
                add_quad_char('0' + c1, x, y, offset_x, offset_y);
            }

            if (item.special)
                m_buf_main.add(item.special, x, y, 0, 0, false);

            if (item.flag & TILEI_FLAG_TRIED)
                m_buf_main.add(TILE_TRIED, x, y, 0, TILE_Y / 2, false);

            if (item.flag & TILEI_FLAG_INVALID)
                m_buf_main.add(TILE_MESH, x, y);
        }
    }
}

unsigned int InventoryRegion::cursor_index() const
{
    ASSERT(m_cursor != NO_CURSOR);
    return (m_cursor.x + m_cursor.y * mx);
}

void InventoryRegion::place_cursor(const coord_def &cursor)
{
    if (m_cursor != NO_CURSOR && cursor_index() < m_items.size())
    {
        m_items[cursor_index()].flag &= ~TILEI_FLAG_CURSOR;
#if 0
        // Not needed? (jpeg)
        m_dirty = true;
#endif
    }

    if (m_cursor != cursor)
        you.last_clicked_item = -1;

    m_cursor = cursor;

    if (m_cursor == NO_CURSOR || cursor_index() >= m_items.size())
        return;

    // Add cursor to new location
    m_items[cursor_index()].flag |= TILEI_FLAG_CURSOR;
    m_dirty = true;
}

int InventoryRegion::handle_mouse(MouseEvent &event)
{
    int cx, cy;
    if (!mouse_pos(event.px, event.py, cx, cy))
    {
        place_cursor(NO_CURSOR);
        return 0;
    }

    const coord_def cursor(cx, cy);
    place_cursor(cursor);

    if (mouse_control::current_mode() != MOUSE_MODE_COMMAND)
        return 0;

    if (event.event != MouseEvent::PRESS)
        return 0;

    unsigned int item_idx = cursor_index();
    if (item_idx >= m_items.size() || m_items[item_idx].empty())
        return 0;

    int idx = m_items[item_idx].idx;
    bool on_floor = m_items[item_idx].flag & TILEI_FLAG_FLOOR;

    ASSERT(idx >= 0);

    // TODO enne - this is all really only valid for the on-screen inventory
    // Do we subclass inventoryregion for the onscreen and offscreen versions?
    char key = m_items[item_idx].key;
    if (key)
        return key;

    if (event.button == MouseEvent::LEFT)
    {
        you.last_clicked_item = item_idx;
        if (on_floor)
        {
            if (event.mod & MOD_SHIFT)
                tile_item_use_floor(idx);
            else
                tile_item_pickup(idx);
        }
        else
        {
            if (event.mod & MOD_SHIFT)
                tile_item_drop(idx);
            else if (event.mod & MOD_CTRL)
                tile_item_use_secondary(idx);
            else
                tile_item_use(idx);
        }
        // TODO enne - need to redraw inventory here?
        return CK_MOUSE_CMD;
    }
    else if (event.button == MouseEvent::RIGHT)
    {
        you.last_clicked_item = item_idx;
        if (on_floor)
        {
            if (event.mod & MOD_SHIFT)
            {
                tile_item_eat_floor(idx);
                you.last_clicked_item = item_idx;
            }
            else
            {
                describe_item(mitm[idx]);
                redraw_screen();
            }
        }
        else // in inventory
        {
            describe_item(you.inv[idx], true);
            redraw_screen();
        }
        return CK_MOUSE_CMD;
    }

    return 0;
}

// NOTE: Assumes the item is equipped in the first place!
static bool _is_true_equipped_item(item_def item)
{
    // Weapons and staves are only truly equipped if wielded.
    if (item.link == you.equip[EQ_WEAPON])
        return (item.base_type == OBJ_WEAPONS || item.base_type == OBJ_STAVES);

    // Cursed armour and rings are only truly equipped if *not* wielded.
    return (item.link != you.equip[EQ_WEAPON]);
}

// Returns whether there's any action you can take with an item in inventory
// apart from dropping it.
static bool _can_use_item(const item_def &item, bool equipped)
{
    // There's nothing you can do with an empty box if you can't unwield it.
    if (!equipped && item.sub_type == MISC_EMPTY_EBONY_CASKET)
        return (false);

    // Vampires can drain corpses.
    if (item.base_type == OBJ_CORPSES)
    {
        return (you.species == SP_VAMPIRE
                && item.sub_type != CORPSE_SKELETON
                && !food_is_rotten(item)
                && mons_has_blood(item.plus));
    }

    if (equipped && item_cursed(item))
    {
        // Misc. items/rods can always be evoked, cursed or not.
        if (item.base_type == OBJ_MISCELLANY || item_is_rod(item))
            return (true);

        // You can't unwield/fire a wielded cursed weapon/staff
        // but cursed armour and rings can be unwielded without problems.
        return (!_is_true_equipped_item(item));
    }

    // Mummies can't do anything with food or potions.
    if (you.species == SP_MUMMY)
        return (item.base_type != OBJ_POTIONS && item.base_type != OBJ_FOOD);

    // In all other cases you can use the item in some way.
    return (true);
}

bool InventoryRegion::update_tip_text(std::string& tip)
{
    if (m_cursor == NO_CURSOR)
        return (false);

    unsigned int item_idx = cursor_index();
    if (item_idx >= m_items.size() || m_items[item_idx].empty())
        return (false);

    int idx = m_items[item_idx].idx;

    // TODO enne - consider subclassing this class, rather than depending
    // on "key" to determine if this is the crt inventory or the on screen one.
    bool display_actions = (m_items[item_idx].key == 0
                    && mouse_control::current_mode() == MOUSE_MODE_COMMAND);

    // TODO enne - should the command keys here respect keymaps?

    if (m_items[item_idx].flag & TILEI_FLAG_FLOOR)
    {
        const item_def &item = mitm[idx];

        if (!is_valid_item(item))
            return (false);

        tip = "";
        if (m_items[item_idx].key)
        {
            tip = m_items[item_idx].key;
            tip += " - ";
        }

        tip += item.name(DESC_NOCAP_A);

        if (!display_actions)
            return (true);

        tip += "\n[L-Click] Pick up (g)";
        if (item.base_type == OBJ_CORPSES
            && item.sub_type != CORPSE_SKELETON
            && !food_is_rotten(item))
        {
            tip += "\n[Shift-L-Click] ";
            if (can_bottle_blood_from_corpse(item.plus))
                tip += "Bottle blood";
            else
                tip += "Chop up";
            tip += " (c)";

            if (you.species == SP_VAMPIRE)
                tip += "\n\n[Shift-R-Click] Drink blood (e)";
        }
        else if (item.base_type == OBJ_FOOD
                 && you.is_undead != US_UNDEAD
                 && you.species != SP_VAMPIRE)
        {
            tip += "\n[Shift-R-Click] Eat (e)";
        }
    }
    else
    {
        const item_def &item = you.inv[idx];
        if (!is_valid_item(item))
            return (false);

        tip = item.name(DESC_INVENTORY_EQUIP);

        if (!display_actions)
            return (true);

        int type = item.base_type;
        const bool equipped = m_items[item_idx].flag & TILEI_FLAG_EQUIP;
        bool wielded = (you.equip[EQ_WEAPON] == idx);

        const int EQUIP_OFFSET = NUM_OBJECT_CLASSES;

        if (_can_use_item(item, equipped))
        {
            tip += "\n[L-Click] ";
            if (equipped)
            {
                if (wielded && type != OBJ_MISCELLANY && !item_is_rod(item))
                {
                    if (type == OBJ_JEWELLERY || type == OBJ_ARMOUR
                        || type == OBJ_WEAPONS || type == OBJ_STAVES)
                    {
                        type = OBJ_WEAPONS + EQUIP_OFFSET;
                    }
                }
                else
                    type += EQUIP_OFFSET;
            }

            switch (type)
            {
            // first equipable categories
            case OBJ_WEAPONS:
            case OBJ_STAVES:
                tip += "Wield (w)";
                if (is_throwable(&you, item))
                    tip += "\n[Ctrl-L-Click] Fire (f)";
                break;
            case OBJ_WEAPONS + EQUIP_OFFSET:
                tip += "Unwield (w-)";
                if (is_throwable(&you, item))
                    tip += "\n[Ctrl-L-Click] Fire (f)";
                break;
            case OBJ_MISCELLANY:
                if (item.sub_type >= MISC_DECK_OF_ESCAPE
                    && item.sub_type <= MISC_DECK_OF_DEFENCE)
                {
                    tip += "Wield (w)";
                    break;
                }
                tip += "Evoke (V)";
                break;
            case OBJ_MISCELLANY + EQUIP_OFFSET:
                if (item.sub_type >= MISC_DECK_OF_ESCAPE
                    && item.sub_type <= MISC_DECK_OF_DEFENCE)
                {
                    tip += "Draw a card (v)\n";
                    tip += "[Ctrl-L-Click] Unwield (w-)";
                    break;
                }
                // else fall-through
            case OBJ_STAVES + EQUIP_OFFSET: // rods - other staves handled above
                tip += "Evoke (v)\n";
                tip += "[Ctrl-L-Click] Unwield (w-)";
                break;
            case OBJ_ARMOUR:
                tip += "Wear (W)";
                break;
            case OBJ_ARMOUR + EQUIP_OFFSET:
                tip += "Take off (T)";
                break;
            case OBJ_JEWELLERY:
                tip += "Put on (P)";
                break;
            case OBJ_JEWELLERY + EQUIP_OFFSET:
                tip += "Remove (R)";
                break;
            case OBJ_MISSILES:
                tip += "Fire (f)";

                if (wielded)
                    tip += "\n[Ctrl-L-Click] Unwield (w-)";
                else if (item.sub_type == MI_STONE
                            && player_knows_spell(SPELL_SANDBLAST)
                         || item.sub_type == MI_ARROW
                            && player_knows_spell(SPELL_STICKS_TO_SNAKES))
                {
                    // For Sandblast and Sticks to Snakes,
                    // respectively.
                    tip += "\n[Ctrl-L-Click] Wield (w)";
                }
                break;
            case OBJ_WANDS:
                tip += "Evoke (V)";
                if (wielded)
                    tip += "\n[Ctrl-L-Click] Unwield (w-)";
                break;
            case OBJ_BOOKS:
                if (item_type_known(item)
                    && item.sub_type != BOOK_MANUAL
                    && item.sub_type != BOOK_DESTRUCTION)
                {
                    tip += "Memorise (M)";
                    if (wielded)
                        tip += "\n[Ctrl-L-Click] Unwield (w-)";
                    break;
                }
                // else fall-through
            case OBJ_SCROLLS:
                tip += "Read (r)";
                if (wielded)
                    tip += "\n[Ctrl-L-Click] Unwield (w-)";
                break;
            case OBJ_POTIONS:
                tip += "Quaff (q)";
                // For Sublimation of Blood.
                if (wielded)
                    tip += "\n[Ctrl-L-Click] Unwield (w-)";
                else if (item_type_known(item)
                         && is_blood_potion(item)
                         && player_knows_spell(SPELL_SUBLIMATION_OF_BLOOD))
                {
                    tip += "\n[Ctrl-L-Click] Wield (w)";
                }
                break;
            case OBJ_FOOD:
                tip += "Eat (e)";
                // For Sublimation of Blood.
                if (wielded)
                    tip += "\n[Ctrl-L-Click] Unwield (w-)";
                else if (item.sub_type == FOOD_CHUNK
                         && player_knows_spell(
                                SPELL_SUBLIMATION_OF_BLOOD))
                {
                    tip += "\n[Ctrl-L-Click] Wield (w)";
                }
                break;
            case OBJ_CORPSES:
                if (you.species == SP_VAMPIRE)
                    tip += "Drink blood (e)";

                if (wielded)
                {
                    if (you.species == SP_VAMPIRE)
                        tip += EOL;
                    tip += "[Ctrl-L-Click] Unwield (w-)";
                }
                break;
            default:
                tip += "Use";
            }
        }

        // For Boneshards.
        // Special handling since skeletons have no primary action.
        if (item.base_type == OBJ_CORPSES
            && item.sub_type == CORPSE_SKELETON)
        {
            if (wielded)
                tip += "\n[Ctrl-L-Click] Unwield";
            else if (player_knows_spell(SPELL_BONE_SHARDS))
                tip += "\n[Ctrl-L-Click] Wield (w)";
        }

        tip += "\n[R-Click] Info";
        // Has to be non-equipped or non-cursed to drop.
        if (!equipped || !_is_true_equipped_item(you.inv[idx])
            || !item_cursed(you.inv[idx]))
        {
            tip += "\n[Shift-L-Click] Drop (d)";
        }
    }

    return (true);
}

bool InventoryRegion::update_alt_text(std::string &alt)
{
    if (m_cursor == NO_CURSOR)
        return (false);

    unsigned int item_idx = cursor_index();
    if (item_idx >= m_items.size() || m_items[item_idx].empty())
        return (false);

    if (you.last_clicked_item >= 0
        && item_idx == (unsigned int) you.last_clicked_item)
    {
        return (false);
    }
    int idx = m_items[item_idx].idx;

    const item_def *item;
    if (m_items[item_idx].flag & TILEI_FLAG_FLOOR)
        item = &mitm[idx];
    else
        item = &you.inv[idx];

    if (!is_valid_item(*item))
        return (false);

    describe_info inf;
    get_item_desc(*item, inf, true);

    alt_desc_proc proc(crawl_view.msgsz.x, crawl_view.msgsz.y);
    process_description<alt_desc_proc>(proc, inf);

    proc.get_string(alt);

    return (true);
}

MapRegion::MapRegion(int pixsz) :
    m_buf(NULL),
    m_dirty(true),
    m_far_view(false)
{
    ASSERT(pixsz > 0);

    dx = pixsz;
    dy = pixsz;
    clear();
    init_colours();
}

void MapRegion::on_resize()
{
    delete[] m_buf;

    int size = mx * my;
    m_buf = new unsigned char[size];
    memset(m_buf, 0, sizeof(unsigned char) * size);
}

void MapRegion::init_colours()
{
    // TODO enne - the options array for colours should be
    // tied to the map feature enumeration to avoid this function.
    m_colours[MF_UNSEEN]        = (map_colour)Options.tile_unseen_col;
    m_colours[MF_FLOOR]         = (map_colour)Options.tile_floor_col;
    m_colours[MF_WALL]          = (map_colour)Options.tile_wall_col;
    m_colours[MF_MAP_FLOOR]     = (map_colour)Options.tile_floor_col; // TODO enne
    m_colours[MF_MAP_WALL]      = (map_colour)Options.tile_mapped_wall_col;
    m_colours[MF_DOOR]          = (map_colour)Options.tile_door_col;
    m_colours[MF_ITEM]          = (map_colour)Options.tile_item_col;
    m_colours[MF_MONS_HOSTILE]  = (map_colour)Options.tile_monster_col;
    m_colours[MF_MONS_FRIENDLY] = (map_colour)Options.tile_friendly_col;
    m_colours[MF_MONS_NEUTRAL]  = (map_colour)Options.tile_neutral_col;
    m_colours[MF_MONS_NO_EXP]   = (map_colour)Options.tile_plant_col;
    m_colours[MF_STAIR_UP]      = (map_colour)Options.tile_upstairs_col;
    m_colours[MF_STAIR_DOWN]    = (map_colour)Options.tile_downstairs_col;
    m_colours[MF_STAIR_BRANCH]  = (map_colour)Options.tile_feature_col;
    m_colours[MF_FEATURE]       = (map_colour)Options.tile_feature_col;
    m_colours[MF_WATER]         = (map_colour)Options.tile_water_col;
    m_colours[MF_LAVA]          = (map_colour)Options.tile_lava_col;
    m_colours[MF_TRAP]          = (map_colour)Options.tile_trap_col;
    m_colours[MF_EXCL_ROOT]     = (map_colour)Options.tile_excl_centre_col;
    m_colours[MF_EXCL]          = (map_colour)Options.tile_excluded_col;
    m_colours[MF_PLAYER]        = (map_colour)Options.tile_player_col;
}

MapRegion::~MapRegion()
{
    delete[] m_buf;
}

void MapRegion::pack_buffers()
{
    m_buf_map.clear();
    m_buf_lines.clear();

    for (int x = m_min_gx; x <= m_max_gx; x++)
        for (int y = m_min_gy; y <= m_max_gy; y++)
        {
            map_feature f = (map_feature)m_buf[x + y * mx];
            map_colour c = m_colours[f];

            float pos_x = x - m_min_gx;
            float pos_y = y - m_min_gy;
            m_buf_map.add(pos_x, pos_y, pos_x + 1, pos_y + 1, map_colours[c]);
        }

    // Draw window box.
    if (m_win_start.x == -1 && m_win_end.x == -1)
        return;

    int c = (int)Options.tile_window_col;
    float pos_sx = (m_win_start.x - m_min_gx);
    float pos_sy = (m_win_start.y - m_min_gy);
    float pos_ex = (m_win_end.x - m_min_gx) + 1 / (float)dx;
    float pos_ey = (m_win_end.y - m_min_gy) + 1 / (float)dy;

    m_buf_lines.add_square(pos_sx, pos_sy, pos_ex, pos_ey, map_colours[c]);
}

void MapRegion::render()
{
    if (m_min_gx > m_max_gx || m_min_gy > m_max_gy)
        return;

#ifdef DEBUG_TILES_REDRAW
    cprintf("rendering MapRegion\n");
#endif
    if (m_dirty)
    {
        pack_buffers();
        m_dirty = false;
    }

    set_transform();
    m_buf_map.draw();
    m_buf_lines.draw();
}

void MapRegion::recenter()
{
    // adjust offsets to center map
    ox = (wx - dx * (m_max_gx - m_min_gx)) / 2;
    oy = (wy - dy * (m_max_gy - m_min_gy)) / 2;
#if 0
    // Not needed? (jpeg)
    m_dirty = true;
#endif
}

void MapRegion::set(int gx, int gy, map_feature f)
{
    ASSERT((unsigned int)f <= (unsigned char)~0);
    m_buf[gx + gy * mx] = f;

    if (f == MF_UNSEEN)
        return;

    // Get map extents
    m_min_gx = std::min(m_min_gx, gx);
    m_max_gx = std::max(m_max_gx, gx);
    m_min_gy = std::min(m_min_gy, gy);
    m_max_gy = std::max(m_max_gy, gy);

    recenter();
}

void MapRegion::update_bounds()
{
    int min_gx = m_min_gx;
    int max_gx = m_max_gx;
    int min_gy = m_min_gy;
    int max_gy = m_max_gy;

    m_min_gx = GXM;
    m_max_gx = 0;
    m_min_gy = GYM;
    m_max_gy = 0;

    for (int x = min_gx; x <= max_gx; x++)
        for (int y = min_gy; y <= max_gy; y++)
        {
            map_feature f = (map_feature)m_buf[x + y * mx];
            if (f == MF_UNSEEN)
                continue;

            m_min_gx = std::min(m_min_gx, x);
            m_max_gx = std::max(m_max_gx, x);
            m_min_gy = std::min(m_min_gy, y);
            m_max_gy = std::max(m_max_gy, y);
        }

    recenter();
#if 0
    // Not needed? (jpeg)
    m_dirty = true;
#endif
}

void MapRegion::set_window(const coord_def &start, const coord_def &end)
{
    m_win_start = start;
    m_win_end   = end;

    m_dirty = true;
}

void MapRegion::clear()
{
    m_min_gx = GXM;
    m_max_gx = 0;
    m_min_gy = GYM;
    m_max_gy = 0;

    m_win_start.x = -1;
    m_win_start.y = -1;
    m_win_end.x = -1;
    m_win_end.y = -1;

    recenter();

    if (m_buf)
        memset(m_buf, 0, sizeof(*m_buf) * mx * my);

    m_buf_map.clear();
    m_buf_lines.clear();
}

int MapRegion::handle_mouse(MouseEvent &event)
{
    if (mouse_control::current_mode() != MOUSE_MODE_COMMAND)
        return 0;

    int cx;
    int cy;
    if (!mouse_pos(event.px, event.py, cx, cy))
    {
        if (m_far_view)
        {
            m_far_view = false;
            tiles.load_dungeon(crawl_view.vgrdc);
            return 0;
        }
        return 0;
    }

    const coord_def gc(m_min_gx + cx, m_min_gy + cy);

    tiles.place_cursor(CURSOR_MOUSE, gc);

    switch (event.event)
    {
    case MouseEvent::MOVE:
        if (m_far_view)
            tiles.load_dungeon(gc);
        return 0;
    case MouseEvent::PRESS:
        if (event.button == MouseEvent::LEFT)
        {
            return _click_travel(gc, event);
        }
        else if (event.button == MouseEvent::RIGHT)
        {
            m_far_view = true;
            tiles.load_dungeon(gc);
        }
        return 0;
    case MouseEvent::RELEASE:
        if ((event.button == MouseEvent::RIGHT) && m_far_view)
        {
            m_far_view = false;
            tiles.load_dungeon(crawl_view.vgrdc);
        }
        return 0;
    default:
        return 0;
    }
}

bool MapRegion::update_tip_text(std::string& tip)
{
    if (mouse_control::current_mode() != MOUSE_MODE_COMMAND)
        return (false);

    tip = "[L-Click] Travel / [R-Click] View";
    return (true);
}

void TextRegion::scroll()
{
    for (int idx = 0; idx < mx*(my-1); idx++)
    {
        cbuf[idx] = cbuf[idx + mx];
        abuf[idx] = abuf[idx + mx];
    }

    for (int idx = mx*(my-1); idx < mx*my; idx++)
    {
        cbuf[idx] = ' ';
        abuf[idx] = 0;
    }

    if (print_y > 0)
        print_y -= 1;
    if (cursor_y > 0)
        cursor_y -= 1;
}

TextRegion::TextRegion(FTFont *font) :
    cbuf(NULL),
    abuf(NULL),
    cx_ofs(0),
    cy_ofs(0),
    m_font(font)
{
    ASSERT(font);

    dx = m_font->char_width();
    dy = m_font->char_height();
}

void TextRegion::on_resize()
{
    delete[] cbuf;
    delete[] abuf;

    int size = mx * my;
    cbuf = new unsigned char[size];
    abuf = new unsigned char[size];

    for (int i = 0; i < size; i++)
    {
        cbuf[i] = ' ';
        abuf[i] = 0;
    }
}

TextRegion::~TextRegion()
{
    delete[] cbuf;
    delete[] abuf;
}

void TextRegion::adjust_region(int *x1, int *x2, int y)
{
    *x2 = *x2 + 1;
}

void TextRegion::addstr(char *buffer)
{
    char buf2[1024];
    int len = strlen(buffer);

    int j = 0;

    for (int i = 0; i < len + 1; i++)
    {
        char c = buffer[i];
        bool newline = false;

        if (c == '\r')
            continue;

        if (c == '\n')
        {
            c = 0;
            newline = true;
        }
        buf2[j] = c;
        j++;

        if (c == 0)
        {
            if (j-1 != 0)
                addstr_aux(buf2, j - 1); // draw it
            if (newline)
            {
                print_x = cx_ofs;
                print_y++;
                j = 0;

                if (print_y - cy_ofs == my)
                    scroll();
            }
        }
    }
    if (cursor_flag)
        cgotoxy(print_x+1, print_y+1);
}

void TextRegion::addstr_aux(char *buffer, int len)
{
    int x = print_x - cx_ofs;
    int y = print_y - cy_ofs;
    int adrs = y * mx;
    int head = x;
    int tail = x + len - 1;

    adjust_region(&head, &tail, y);

    for (int i = 0; i < len && x + i < mx; i++)
    {
        cbuf[adrs+x+i] = buffer[i];
        abuf[adrs+x+i] = text_col;
    }
    print_x += len;
}

void TextRegion::clear_to_end_of_line()
{
    int cx = print_x - cx_ofs;
    int cy = print_y - cy_ofs;
    int col = text_col;
    int adrs = cy * mx;

    ASSERT(adrs + mx - 1 < mx * my);
    for (int i = cx; i < mx; i++)
    {
        cbuf[adrs+i] = ' ';
        abuf[adrs+i] = col;
    }
}

void TextRegion::putch(unsigned char ch)
{
    // special case: check for '0' char: map to space
    if (ch == 0)
        ch = ' ';
    addstr_aux((char *)&ch, 1);
}

void TextRegion::writeWChar(unsigned char *ch)
{
    addstr_aux((char *)ch, 2);
}

void TextRegion::textcolor(int color)
{
    text_col = color;
}

void TextRegion::textbackground(int col)
{
    textcolor(col*16 + (text_col & 0xf));
}

void TextRegion::cgotoxy(int x, int y)
{
    ASSERT(x >= 1);
    ASSERT(y >= 1);
    print_x = x-1;
    print_y = y-1;

#if 0
    if (cursor_region != NULL && cursor_flag)
    {
        cursor_x = -1;
        cursor_y = -1;
        cursor_region = NULL;
    }
#endif
    if (cursor_flag)
    {
        cursor_x = print_x;
        cursor_y = print_y;
        cursor_region = text_mode;
    }
}

int TextRegion::wherex()
{
    return print_x + 1;
}

int TextRegion::wherey()
{
    return print_y + 1;
}

void TextRegion::_setcursortype(int curstype)
{
    cursor_flag = curstype;
    if (cursor_region != NULL)
    {
        cursor_x = -1;
        cursor_y = -1;
    }

    if (curstype)
    {
        cursor_x = print_x;
        cursor_y = print_y;
        cursor_region = text_mode;
    }
}

void TextRegion::render()
{
#ifdef DEBUG_TILES_REDRAW
    cprintf("rendering TextRegion\n");
#endif
    if (this == TextRegion::cursor_region && cursor_x > 0 && cursor_y > 0)
    {
        int idx = cursor_x + mx * cursor_y;

        unsigned char char_back = cbuf[idx];
        unsigned char col_back  = abuf[idx];

        cbuf[idx] = '_';
        abuf[idx] = WHITE;

        m_font->render_textblock(sx + ox, sy + oy, cbuf, abuf, mx, my);

        cbuf[idx] = char_back;
        abuf[idx] = col_back;
    }
    else
    {
        m_font->render_textblock(sx + ox, sy + oy, cbuf, abuf, mx, my);
    }
}

void TextRegion::clear()
{
    for (int i = 0; i < mx * my; i++)
    {
        cbuf[i] = ' ';
        abuf[i] = 0;
    }
}

StatRegion::StatRegion(FTFont *font) : TextRegion(font)
{
}

int StatRegion::handle_mouse(MouseEvent &event)
{
    if (mouse_control::current_mode() != MOUSE_MODE_COMMAND)
        return 0;

    if (!inside(event.px, event.py))
        return 0;

    if (event.event != MouseEvent::PRESS || event.button != MouseEvent::LEFT)
        return 0;

    // Resting
    return '5';
}

bool StatRegion::update_tip_text(std::string& tip)
{
    if (mouse_control::current_mode() != MOUSE_MODE_COMMAND)
        return (false);

    tip = "[L-Click] Rest / Search for a while";
    return (true);
}

MessageRegion::MessageRegion(FTFont *font) : TextRegion(font), m_overlay(false)
{
}

int MessageRegion::handle_mouse(MouseEvent &event)
{
    // TODO enne - mouse scrolling here should mouse scroll up through
    // the message history in the message pane, without going to the CRT.

    if (!inside(event.px, event.py))
        return 0;

    if (event.event != MouseEvent::PRESS || event.button != MouseEvent::LEFT)
        return 0;

    if (mouse_control::current_mode() != MOUSE_MODE_COMMAND)
        return 0;

    return CONTROL('P');
}

bool MessageRegion::update_tip_text(std::string& tip)
{
    if (mouse_control::current_mode() != MOUSE_MODE_COMMAND)
        return (false);

    tip = "[L-Click] Browse message history";
    return (true);
}

void MessageRegion::set_overlay(bool is_overlay)
{
    m_overlay = is_overlay;
}

struct box_vert
{
    float x;
    float y;
    unsigned char r;
    unsigned char g;
    unsigned char b;
    unsigned char a;
};

void MessageRegion::render()
{
#ifdef DEBUG_TILES_REDRAW
    cprintf("rendering MessageRegion\n");
#endif
    int idx = -1;
    unsigned char char_back = 0;
    unsigned char col_back = 0;

    if (!m_overlay && !m_alt_text.empty())
    {
        coord_def min_pos(sx, sy);
        coord_def max_pos(ex, ey);
        m_font->render_string(sx + ox, sy + oy, m_alt_text.c_str(),
                              min_pos, max_pos, WHITE, false);
        return;
    }

    if (this == TextRegion::cursor_region && cursor_x > 0 && cursor_y > 0)
    {
        idx = cursor_x + mx * cursor_y;
        char_back = cbuf[idx];
        col_back  = abuf[idx];

        cbuf[idx] = '_';
        abuf[idx] = WHITE;
    }

    if (m_overlay)
    {
        int height;
        bool found = false;
        for (height = my; height > 0; height--)
        {
            unsigned char *buf = &cbuf[mx * (height - 1)];
            for (int x = 0; x < mx; x++)
            {
                if (buf[x] != ' ')
                {
                    found = true;
                    break;
                }
            }

            if (found)
                break;
        }

        if (height > 0)
        {
            height *= m_font->char_height();

            glLoadIdentity();

            ShapeBuffer buff;
            VColour col(100, 100, 100, 100);
            buff.add(sx, sy, ex, sy + height, col);
            buff.draw();
        }
    }

    m_font->render_textblock(sx + ox, sy + oy, cbuf, abuf, mx, my, m_overlay);

    if (idx >= 0)
    {
        cbuf[idx] = char_back;
        abuf[idx] = col_back;
    }
}

CRTRegion::CRTRegion(FTFont *font) : TextRegion(font)
{
}

int CRTRegion::handle_mouse(MouseEvent &event)
{
    if (event.event != MouseEvent::PRESS || event.button != MouseEvent::LEFT)
        return 0;

    return CK_MOUSE_CLICK;
}

MenuRegion::MenuRegion(ImageManager *im, FTFont *entry) :
    m_image(im), m_font_entry(entry), m_mouse_idx(-1),
    m_max_columns(1), m_dirty(false), m_font_buf(entry)
{
    ASSERT(m_image);
    ASSERT(m_font_entry);

    dx = 1;
    dy = 1;

    m_entries.resize(128);

    for (int i = 0; i < TEX_MAX; i++)
        m_tile_buf[i].set_tex(&m_image->m_textures[i]);
}

void MenuRegion::set_num_columns(int columns)
{
    m_max_columns = std::max(1, columns);
}

int MenuRegion::mouse_entry(int x, int y)
{
    if (m_dirty)
        place_entries();

    for (unsigned int i = 0; i < m_entries.size(); i++)
    {
        if (!m_entries[i].valid)
            continue;

        if (x >= m_entries[i].sx && x <= m_entries[i].ex
            && y >= m_entries[i].sy && y <= m_entries[i].ey)
        {
            return i;
        }
    }

    return -1;
}

int MenuRegion::handle_mouse(MouseEvent &event)
{
    m_mouse_idx = -1;

    int x, y;
    if (!mouse_pos(event.px, event.py, x, y))
        return 0;

    if (event.event == MouseEvent::MOVE)
    {
        int old_idx = m_mouse_idx;
        m_mouse_idx = mouse_entry(x, y);
        if (old_idx == m_mouse_idx)
            return 0;

        const VColour mouse_colour(160, 160, 160, 255);

        m_line_buf.clear();
        if (!m_entries[m_mouse_idx].heading && m_entries[m_mouse_idx].key)
        {
            m_line_buf.add_square(m_entries[m_mouse_idx].sx-1,
                                  m_entries[m_mouse_idx].sy,
                                  m_entries[m_mouse_idx].ex+1,
                                  m_entries[m_mouse_idx].ey+1,
                                  mouse_colour);
        }

        return 0;
    }

    if (event.event == MouseEvent::PRESS)
    {
        switch (event.button)
        {
        case MouseEvent::LEFT:
        {
            int entry = mouse_entry(x, y);
            if (entry == -1)
                return 0;
            return m_entries[entry].key;
        }
#if 0
        // TODO enne - these events are wonky on OS X.
        // TODO enne - fix menus so that mouse wheeling doesn't easy exit
        case MouseEvent::SCROLL_UP:
            return CK_UP;
        case MouseEvent::SCROLL_DOWN:
            return CK_DOWN;
#endif
        default:
            return 0;
        }
    }

    return 0;
}

void MenuRegion::place_entries()
{
    m_dirty = false;

    const int heading_indent  = 10;
    const int tile_indent     = 20;
    const int text_indent     = (Options.tile_menu_icons ? 58 : 20);
    const int max_tile_height = (Options.tile_menu_icons ? 32 : 0);
    const int entry_buffer    = 1;
    const VColour selected_colour(50, 50, 10, 255);

    m_font_buf.clear();
    m_shape_buf.clear();
    m_line_buf.clear();
    for (int t = 0; t < TEX_MAX; t++)
        m_tile_buf[t].clear();

    int column = 0;
    if (!Options.tile_menu_icons)
        set_num_columns(1);
    const int max_columns = std::min(2, m_max_columns);
    const int column_width = mx / max_columns;

    int lines = count_linebreaks(m_more);
    int more_height = (lines + 1) * m_font_entry->char_height();
    m_font_buf.add(m_more, sx + ox, ey - oy - more_height);

    int height = 0;
    int end_height = my - more_height;

    const int max_entry_height = std::max((int)m_font_entry->char_height() * 2,
                                          max_tile_height);

    for (unsigned int i = 0; i < m_entries.size(); i++)
    {
        if (!m_entries[i].valid)
        {
            m_entries[i].sx = 0;
            m_entries[i].sy = 0;
            m_entries[i].ex = 0;
            m_entries[i].ey = 0;
            continue;
        }

        if (height + max_entry_height > end_height && column <= max_columns)
        {
            height = 0;
            column++;
        }

        int text_width  = m_font_entry->string_width(m_entries[i].text);
        int text_height = m_font_entry->char_height();

        if (m_entries[i].heading)
        {
            m_entries[i].sx = heading_indent + column * column_width;
            m_entries[i].ex = m_entries[i].sx + text_width;
            m_entries[i].sy = height;
            m_entries[i].ey = m_entries[i].sy + text_height;

            m_font_buf.add(m_entries[i].text, m_entries[i].sx, m_entries[i].sy);

            height += text_height;
        }
        else
        {
            m_entries[i].sy = height;
            int entry_start = column * column_width;
            int text_sx = text_indent + entry_start;

            int entry_height;

            if (m_entries[i].tiles.size() > 0)
            {
                m_entries[i].sx = entry_start + tile_indent;
                entry_height = std::max(max_tile_height, text_height);

                for (unsigned int t = 0; t < m_entries[i].tiles.size(); t++)
                {
                    // NOTE: This is not perfect. Tiles will be drawn
                    // sorted by texture first, e.g. you can never draw
                    // a dungeon tile over a monster tile.
                    int tile      = m_entries[i].tiles[t].tile;
                    TextureID tex = m_entries[i].tiles[t].tex;
                    m_tile_buf[tex].add_unscaled(tile, m_entries[i].sx,
                                                 m_entries[i].sy);
                }
            }
            else
            {
                m_entries[i].sx = text_sx;
                entry_height = text_height;
            }

            int text_sy = m_entries[i].sy;
            text_sy += (entry_height - m_font_entry->char_height()) / 2;
            // Split menu entries that don't fit into a single lines into
            // two lines.
            if (Options.tile_menu_icons
                && text_sx + text_width > entry_start + column_width)
            {
                // [enne] - Ugh, hack.  Maybe MenuEntry could specify the
                // presence and length of this substring?
                std::string unfm = m_entries[i].text.tostring();
                bool let = (unfm[1] >= 'a' && unfm[1] <= 'z'
                            || unfm[1] >= 'A' && unfm[1] <= 'Z');
                bool plus = (unfm[3] == '-' || unfm[3] == '+');

                formatted_string text;
                if (let && plus && unfm[0] == ' ' && unfm[2] == ' '
                    && unfm[4] == ' ')
                {
                    formatted_string header = m_entries[i].text.substr(0, 5);
                    m_font_buf.add(header, text_sx, text_sy);
                    text_sx += m_font_entry->string_width(header);
                    text = m_entries[i].text.substr(5);
                }
                else
                {
                    text += m_entries[i].text;
                }

                int w = entry_start + column_width - text_sx;
                int h = m_font_entry->char_height() * 2;
                formatted_string split = m_font_entry->split(text, w, h);

                int string_height = m_font_entry->string_height(split);
                if (string_height > entry_height)
                    text_sy = m_entries[i].sy;

                m_font_buf.add(split, text_sx, text_sy);

                entry_height = std::max(entry_height, string_height);
                m_entries[i].ex = entry_start + column_width;
            }
            else
            {
                if (max_columns == 1)
                    m_entries[i].ex = text_sx + text_width;
                else
                    m_entries[i].ex = entry_start + column_width;
                m_font_buf.add(m_entries[i].text, text_sx, text_sy);
            }

            m_entries[i].ey = m_entries[i].sy + entry_height;
            height = m_entries[i].ey;
        }

        if (m_entries[i].selected)
        {
            m_shape_buf.add(m_entries[i].sx-1, m_entries[i].sy-1,
                            m_entries[i].ex+1, m_entries[i].ey+1,
                            selected_colour);
        }

        height += entry_buffer;
    }
}

void MenuRegion::render()
{
#ifdef DEBUG_TILES_REDRAW
    cprintf("rendering MenuRegion\n");
#endif
    if (m_dirty)
        place_entries();

    set_transform();
    m_shape_buf.draw();
    m_line_buf.draw();
    for (int i = 0; i < TEX_MAX; i++)
        m_tile_buf[i].draw();
    m_font_buf.draw();
}

void MenuRegion::clear()
{
    m_shape_buf.clear();
    m_line_buf.clear();
    for (int i = 0; i < TEX_MAX; i++)
        m_tile_buf[i].clear();
    m_font_buf.clear();

    m_more.clear();

    for (unsigned int i = 0; i < m_entries.size(); i++)
        m_entries[i].valid = false;

    m_mouse_idx = -1;
}

void MenuRegion::set_entry(int idx, const std::string &str, int colour,
                           const MenuEntry *me)
{
    if (idx >= (int)m_entries.size())
    {
        int new_size = m_entries.size();
        while (idx >= new_size)
            new_size *= 2;
        m_entries.resize(new_size);
    }

    MenuRegionEntry &e = m_entries[idx];
    e.valid = true;
    e.text.clear();
    e.text.textcolor(colour);
    e.text += formatted_string::parse_string(str);

    e.heading  = (me->level == MEL_TITLE || me->level == MEL_SUBTITLE);
    e.selected = me->selected();
    e.key      = me->hotkeys.size() > 0 ? me->hotkeys[0] : 0;
    e.sx = e.sy = e.ex = e.ey = 0;
    e.tiles.clear();
    me->get_tiles(e.tiles);

    m_dirty = true;
}

void MenuRegion::on_resize()
{
    // Probably needed, even though for me nothing went wrong when
    // I commented it out. (jpeg)
    m_dirty = true;
}

int MenuRegion::maxpagesize() const
{
    // TODO enne - this is a conservative guess.
    // It would be better to make menus use a dynamic number of items per page,
    // but it'd require a lot more refactoring of menu.cc to handle that.

    const int lines = count_linebreaks(m_more);
    const int more_height = (lines + 1) * m_font_entry->char_height();

    // Similar to the definition of max_entry_height in place_entries().
    // HACK: Increasing height by 1 to make sure all items actually fit
    //       on the page, though this introduces a few too many empty lines.
    const int div = (Options.tile_menu_icons ? 32
                                             : m_font_entry->char_height() + 1);

    const int pagesize = ((my - more_height) / div) * m_max_columns;

    // Upper limit for inventory menus. (jpeg)
    // Non-inventory menus only have one column and need
    // *really* big screens to cover more than 52 lines.
    if (pagesize > 52)
        return (52);
    return (pagesize);
}

void MenuRegion::set_offset(int lines)
{
    oy = (lines - 1) * m_font_entry->char_height() + 4;
    my = wy - oy;
}

void MenuRegion::set_more(const formatted_string &more)
{
    m_more.clear();
    m_more += more;
#if 0
    // Not needed? (jpeg)
    m_dirty = true;
#endif
}

TitleRegion::TitleRegion(int width, int height) :
    m_buf(&m_img, GL_QUADS)
{
    sx = sy = 0;
    dx = dy = 1;

    if (!m_img.load_texture("title.png", GenericTexture::MIPMAP_NONE))
        return;

    // Center
    wx = width;
    wy = height;
    ox = (wx - m_img.orig_width()) / 2;
    oy = (wy - m_img.orig_height()) / 2;

    {
        PTVert &v = m_buf.get_next();
        v.pos_x = 0;
        v.pos_y = 0;
        v.tex_x = 0;
        v.tex_y = 0;
    }
    {
        PTVert &v = m_buf.get_next();
        v.pos_x = 0;
        v.pos_y = m_img.height();
        v.tex_x = 0;
        v.tex_y = 1;
    }
    {
        PTVert &v = m_buf.get_next();
        v.pos_x = m_img.width();
        v.pos_y = m_img.height();
        v.tex_x = 1;
        v.tex_y = 1;
    }
    {
        PTVert &v = m_buf.get_next();
        v.pos_x = m_img.width();
        v.pos_y = 0;
        v.tex_x = 1;
        v.tex_y = 0;
    }
}

void TitleRegion::render()
{
#ifdef DEBUG_TILES_REDRAW
    cprintf("rendering TitleRegion\n");
#endif
    set_transform();
    m_buf.draw();
}

ImageManager::ImageManager()
{
}

ImageManager::~ImageManager()
{
    unload_textures();
}

bool ImageManager::load_textures()
{
    GenericTexture::MipMapOptions mip = GenericTexture::MIPMAP_CREATE;
    if (!m_textures[TEX_DUNGEON].load_texture("dngn.png", mip))
        return (false);

    if (!m_textures[TEX_PLAYER].load_texture("player.png", mip))
        return (false);

    m_textures[TEX_DUNGEON].set_info(TILE_DNGN_MAX, &tile_dngn_info);
    m_textures[TEX_PLAYER].set_info(TILEP_PLAYER_MAX, &tile_player_info);

    return (true);
}

static void _copy_onto(unsigned char *pixels, int width,
                       int height, unsigned char *src,
                       const tile_info &inf, bool blend)
{
    unsigned char *dest = &pixels[4 * (inf.sy * width + inf.sx)];

    size_t dest_row_size = width * 4;
    size_t src_row_size = inf.width * 4;

    if (blend)
    {
        for (int r = 0; r < inf.height; r++)
        {
            for (int c = 0; c < inf.width; c++)
            {
                unsigned char a = src[3];
                unsigned char inv_a = 255 - src[3];
                dest[0] = (src[0] * a + dest[0] * inv_a) / 255;
                dest[1] = (src[1] * a + dest[1] * inv_a) / 255;
                dest[2] = (src[2] * a + dest[2] * inv_a) / 255;
                dest[3] = (src[3] * a + dest[3] * inv_a) / 255;

                dest += 4;
                src += 4;
            }
            dest += dest_row_size - src_row_size;
        }
    }
    else
    {
        for (int r = 0; r < inf.height; r++)
        {
            memcpy(dest, src, src_row_size);

            dest += dest_row_size;
            src += src_row_size;
        }
    }
}

// Copy an image at inf from pixels into dest.
static void _copy_into(unsigned char *dest, unsigned char *pixels,
                       int width, int height, const tile_info &inf,
                       int ofs_x = 0, int ofs_y = 0)
{
    unsigned char *src = &pixels[4 * (inf.sy * width + inf.sx)];

    size_t src_row_size = width * 4;
    size_t dest_row_size = inf.width * 4;

    memset(dest, 0, 4 * inf.width * inf.height);

    int total_ofs_x = inf.offset_x + ofs_x;
    int total_ofs_y = inf.offset_y + ofs_y;
    int src_height = inf.ey - inf.sy;
    int src_width = inf.ex - inf.sx;

    if (total_ofs_x < 0)
    {
        src_width += total_ofs_x;
        src -= 4 * total_ofs_x;
        total_ofs_x = 0;
    }
    if (total_ofs_y < 0)
    {
        src_height += total_ofs_y;
        src -= 4 * width * total_ofs_y;
        total_ofs_y = 0;
    }

    dest += total_ofs_x * 4 + total_ofs_y * dest_row_size;

    for (int r = 0; r < src_height; r++)
    {
        memcpy(dest, src, src_width * 4);

        dest += dest_row_size;
        src += src_row_size;
    }
}

// Stores "over" on top of "under" in the location of "over".
static bool _copy_under(unsigned char *pixels, int width,
                        int height, int idx_under, int idx_over,
                        int uofs_x = 0, int uofs_y = 0)
{
    const tile_info &under = tile_main_info(idx_under);
    const tile_info &over  = tile_main_info(idx_over);

    if (over.width != under.width || over.height != under.height)
        return (false);

    if (over.ex - over.sx != over.width || over.ey - over.sy != over.height)
        return (false);

    if (over.offset_x != 0 || over.offset_y != 0)
        return (false);

    size_t image_size = under.width * under.height * 4;

    // Make a copy of the original images.
    unsigned char *under_pixels = new unsigned char[image_size];
    _copy_into(under_pixels, pixels, width, height, under, uofs_x, uofs_y);
    unsigned char *over_pixels = new unsigned char[image_size];
    _copy_into(over_pixels, pixels, width, height, over);

    // Replace the over image with the under image
    _copy_onto(pixels, width, height, under_pixels, over, false);
    // Blend the over image over top.
    _copy_onto(pixels, width, height, over_pixels, over, true);

    delete[] under_pixels;
    delete[] over_pixels;

    return (true);
}

static bool _process_item_image(unsigned char *pixels, unsigned int width,
                                unsigned int height)
{
    bool success = true;
    for (int i = 0; i < NUM_POTIONS; i++)
    {
        int special = you.item_description[IDESC_POTIONS][i];
        int tile0 = TILE_POTION_OFFSET + special % 14;
        int tile1 = TILE_POT_HEALING + i;
        success &= _copy_under(pixels, width, height, tile0, tile1);
    }

    for (int i = 0; i < NUM_WANDS; i++)
    {
        int special = you.item_description[IDESC_WANDS][i];
        int tile0 = TILE_WAND_OFFSET + special % 12;
        int tile1 = TILE_WAND_FLAME + i;
        success &= _copy_under(pixels, width, height, tile0, tile1);
    }

    for (int i = 0; i < STAFF_SMITING; i++)
    {
        int special = you.item_description[IDESC_STAVES][i];
        int tile0 = TILE_STAFF_OFFSET + (special / 4) % 10;
        int tile1 = TILE_STAFF_WIZARDRY + i;
        success &= _copy_under(pixels, width, height, tile0, tile1);
    }
    for (int i = STAFF_SMITING; i < NUM_STAVES; i++)
    {
        int special = you.item_description[IDESC_STAVES][i];
        int tile0 = TILE_ROD_OFFSET + (special / 4) % 10;
        int tile1 = TILE_ROD_SMITING + i - STAFF_SMITING;
        success &= _copy_under(pixels, width, height, tile0, tile1);
    }
    for (int i = RING_FIRST_RING; i < NUM_RINGS; i++)
    {
        int special = you.item_description[IDESC_RINGS][i];
        int tile0 = TILE_RING_NORMAL_OFFSET + special % 13;
        int tile1 = TILE_RING_REGENERATION + i - RING_FIRST_RING;
        success &= _copy_under(pixels, width, height, tile0, tile1, -5, -6);
    }
    for (int i = AMU_FIRST_AMULET; i < NUM_JEWELLERY; i++)
    {
        int special = you.item_description[IDESC_RINGS][i];
        int tile0 = TILE_AMU_NORMAL_OFFSET + special % 13;
        int tile1 = TILE_AMU_RAGE + i - AMU_FIRST_AMULET;
        success &= _copy_under(pixels, width, height, tile0, tile1);
    }

    return (true);
}

bool ImageManager::load_item_texture()
{
    // We need to load images in two passes: one for the title and one
    // for the items.  To handle identifiable items, the texture itself
    // is modified.  So, it cannot be loaded until after the item
    // description table has been initialised.
    GenericTexture::MipMapOptions mip = GenericTexture::MIPMAP_CREATE;
    bool success = m_textures[TEX_DEFAULT].load_texture("main.png", mip,
                                                        &_process_item_image);
    m_textures[TEX_DEFAULT].set_info(TILE_MAIN_MAX, tile_main_info);

    return success;
}

void ImageManager::unload_textures()
{
    for (int i = 0; i < TEX_MAX; i++)
        m_textures[i].unload_texture();
}
