/* MegaZeux
 *
 * idops.c - The new set of board manipulation primitives.
 *
 * Copyright (c) 2008 Patrick Walton <pcwalton@uchicago.edu>
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License as
 * published by the Free Software Foundation; either version 2 of
 * the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 */

#include <stdio.h>
#include <stdlib.h>

#include "idops.h"
#include "idput.h"
#include "util.h"

/*
 *  The new primitive fetch function. Given a point, returns the object on the
 *  given layer that represents that point.
 */
Ctp yank(World *world, Board *src_board, Layer layer, Point pt)
{
  Ctp ctp = { 0, 0, 0 };
  int off;

  switch(layer)
  {
    case LAYER_BOARD:
      if(pt.x < 0 || pt.x >= src_board->board_width
          || pt.y < 0 || pt.y >= src_board->board_height)
        return ctp;

      off = pt.x + pt.y * src_board->board_width;

      switch(pt.z) {
        case 2: /* The third layer, which only exists under the player. */
          if(pt.x == world->player_x && pt.y == world->player_y
              && src_board == world->current_board) {
            ctp.color = world->under_player_color;
            ctp.thing = (mzx_thing)world->under_player_id;
            ctp.param = world->under_player_param;
          }
          break;

        case 1: /* The second layer, accessible in Robotic via BENEATH. */
          ctp.color = ((Uint8 *)src_board->level_under_color)[off];
          ctp.thing = (mzx_thing)BOARD_UNDER_THING(src_board, off);
          ctp.param = ((Uint8 *)src_board->level_under_param)[off];
          break;

        case 0: /* The top layer. */
          ctp.color = ((Uint8 *)src_board->level_color)[off];
          ctp.thing = (mzx_thing)BOARD_THING(src_board, off);
          ctp.param = ((Uint8 *)src_board->level_param)[off];
      }
      break;

    case LAYER_OVERLAY:
      if(pt.x < 0 || pt.x >= src_board->board_width
          || pt.y < 0 || pt.y >= src_board->board_height)
        return ctp;

      off = pt.x + pt.y * src_board->board_width;

      ctp.color = ((Uint8 *)src_board->overlay_color)[off];
      ctp.param = ((Uint8 *)src_board->overlay)[off];
      ctp.thing = ctp.param ? CUSTOM_BLOCK : SPACE;
      break;

    case LAYER_VLAYER:
      if(pt.x < 0 || pt.x >= world->vlayer_width
          || pt.y < 0 || pt.y >= world->vlayer_height)
        return ctp;

      off = pt.x + pt.y * world->vlayer_width;

      ctp.color = ((Uint8 *)world->vlayer_colors)[off];
      ctp.param = ((Uint8 *)world->vlayer_chars)[off];
      ctp.thing = ctp.param ? CUSTOM_BLOCK : SPACE;
  };

  return ctp;
}

/*
 *  This function must be called before a special is deleted. If records is
 *  non-NULL, it's updated to reflect the fact that the special was deleted.
 *  Otherwise, this function performs the operation assuming that no more
 *  references to the special can now exist.
 *
 *  It's fine to call this function when ctp does not describe a special;
 *  in that case, nothing happens.
 *
 *  Returns 1 to indicate success and 0 to indicate failure. The caller should
 *  immediately abort and should not delete the object from the board if this
 *  function returns 0.
 */
static int delete_special(World *world, Board *src_board, Ctp ctp,
 Point pt, SpecialRecordTable *records)
{
  if(!IS_SPECIAL(ctp.thing))
    return 1;

  if(records)
  {
    int key;
    HashEntry *ent;
    SpecialRecord *rec;
    key = RECORD_KEY(ctp);
    ent = lookup_in_hash_table(records, &key);
    if(ent)
    {
      rec = (SpecialRecord *)ent->val;
      ASSERT(!rec->deleted);
      rec->deleted = 1;
      rec->deleted_at = pt;
    }
    else
    {
      int *kp;

      rec = malloc(sizeof(SpecialRecord));
      rec->ctp        = ctp;
      rec->deleted    = 1;
      rec->deleted_at = pt;
      rec->inserted   = 0;

      kp = malloc(sizeof(int));
      *kp = key;
      insert_into_hash_table(records, kp, rec);
    }
  }
  else
  {
    switch(ctp.thing)
    {
      case SENSOR:
        clear_sensor_id(src_board, ctp.param);
        break;
      case ROBOT:
      case ROBOT_PUSHABLE:
        clear_robot_id(src_board, ctp.param);
        break;
      case SIGN:
      case SCROLL:
        clear_scroll_id(src_board, ctp.param);
        break;
      case PLAYER:
        return 0;
      default:
        break;    /* shut up the compiler */
    }
  }

  return 1;
}

/*
 *  Same as delete_special. Keep in mind that you must call this function
 *  *before* you insert the possibly-special thing, because it may modify the
 *  ctp. For instance, if you're inserting a robot (and records is NULL), then
 *  this function will need to modify the param to point to the new robot
 *  param.
 *
 *  This function can return 0 for a variety of reasons, most commonly to
 *  indicate that the board is out of space for more robots/sensors/scrolls. In
 *  that case, the caller must back out of the operation and refuse to place
 *  the object.
 */
static int insert_special(World *world, Board *src_board, Ctp *ctp,
 Point pt, SpecialRecordTable *records)
{
  if(!IS_SPECIAL(ctp->thing))
    return 1;

  if(records)
  {
    int key;
    HashEntry *ent;
    SpecialRecord *rec;
    key = RECORD_KEY(*ctp);
    ent = lookup_in_hash_table(records, &key);
    if(ent)
    {
      rec = (SpecialRecord *)ent->val;
      ASSERT(!rec->inserted);
      rec->inserted = 1;
      rec->inserted_at = pt;
    }
    else
    {
      int *kp;

      rec = malloc(sizeof(SpecialRecord));
      rec->ctp          = *ctp;
      rec->inserted     = 1;
      rec->inserted_at  = pt;
      rec->deleted      = 0;

      kp = malloc(sizeof(int));
      *kp = key;
      insert_into_hash_table(records, kp, rec);
    }
  }
  else
  {
    unsigned int new_param;
    switch(ctp->thing)
    {
      case SENSOR:
        new_param = duplicate_sensor(src_board,
            src_board->sensor_list[ctp->param]);
        break;
      case ROBOT:
      case ROBOT_PUSHABLE:
        new_param = duplicate_robot(src_board,
            src_board->robot_list[ctp->param], pt.x, pt.y);
        break;
      case SIGN:
      case SCROLL:
        new_param = duplicate_scroll(src_board,
            src_board->scroll_list[ctp->param]);
        break;
      case PLAYER:
        return 0;
      default:
        return 1;
    }

    if(new_param == ctp->param)
      return 0; /* Out of sensor/robot/scroll space. */

    ctp->param = new_param;
  }

  return 1;
}

static int move_special(World *world, Board *src_board, Ctp ctp,
 Point src_pt, Point dest_pt, SpecialRecordTable *records)
{
  if(!IS_SPECIAL(ctp.thing))
    return 1;
  if(src_pt.x == dest_pt.x && src_pt.y == dest_pt.y)
    return 1; /* Hurr */

  if(records)
  {
    int key;
    HashEntry *ent;
    SpecialRecord *rec;
    key = RECORD_KEY(ctp);
    ent = lookup_in_hash_table(records, &key);
    if(ent)
    {
      /* This shouldn't happen but it's technically legal... */
      rec = (SpecialRecord *)ent->val;
      ASSERT(!rec->inserted && !rec->deleted);
      rec->inserted = 1;
      rec->inserted_at = dest_pt;
      rec->deleted = 1;
      rec->deleted_at = src_pt;
    }
    else
    {
      int *kp;

      rec = malloc(sizeof(SpecialRecord));
      rec->ctp          = ctp;
      rec->inserted     = 1;
      rec->inserted_at  = dest_pt;
      rec->deleted      = 1;
      rec->deleted_at   = src_pt;

      kp = malloc(sizeof(int));
      *kp = key;
      insert_into_hash_table(records, kp, rec);
    }
  }
  else
  {
    switch(ctp.thing)
    {
      case ROBOT:
      case ROBOT_PUSHABLE:
        src_board->robot_list[ctp.param]->xpos = dest_pt.x;
        src_board->robot_list[ctp.param]->ypos = dest_pt.y;
        break;
      case PLAYER:
        world->player_x = dest_pt.x;
        world->player_y = dest_pt.y;
        break;
      default:
        break;
    }
  }

  return 1;
}

static SpecialRecordTable *create_special_record_table(void)
{
  return create_hash_table(int_hash, int_equal);
}

static void destroy_special_record_table(SpecialRecordTable *table)
{
  destroy_hash_table(table);
}

/*
 *  This function is fairly simple. The hard work is done in the x_special
 *  functions above.
 */
static void commit_special_record_table(World *world, Board *src_board,
 SpecialRecordTable *table)
{
  HashEntry *ent;
  HashTableIter *iter;

  iter = create_hash_table_iter(table);
  while((ent = hash_table_iter_next(iter)))
  {
    SpecialRecord *rec;
    rec = (SpecialRecord *)ent->val;

    if(rec->deleted && !rec->inserted)
    {
      fprintf(stderr, "Resolving with DELETION: %d @ %d,%d\n", rec->ctp.param,
          rec->deleted_at.x, rec->deleted_at.y);
      delete_special(world, src_board, rec->ctp, rec->deleted_at,
          NULL);
    }
    else if(rec->inserted && !rec->deleted)
    {
      int off;

      fprintf(stderr, "Resolving with INSERTION: %d\n", rec->ctp.param);

      insert_special(world, src_board, &rec->ctp, rec->inserted_at,
          NULL);

      /*
       *  This is a special case: we actually have to go onto the board and fix
       *  up the param of the newly duplicated thing to be the proper param.
       */
      off = BOARD_OFFSET(src_board, rec->inserted_at.x,
          rec->inserted_at.y);
      if(BOARD_THING(src_board, off) == rec->ctp.thing)
        ((Uint8 *)src_board->level_param)[off] = rec->ctp.param;
      else if(BOARD_UNDER_THING(src_board, off) == rec->ctp.thing)
        ((Uint8 *)src_board->level_under_param)[off] = rec->ctp.param;
      else
      {
        ASSERT(rec->inserted_at.x == world->player_x
            && rec->inserted_at.y == world->player_y);
        world->under_player_param = rec->ctp.param;
      }
    }
    else if(rec->inserted && rec->deleted)
    {
      move_special(world, src_board, rec->ctp, rec->deleted_at,
          rec->inserted_at, NULL);
    }
  }

  destroy_hash_table_iter(iter);
}

/*
 *  Places a thing onto the given layer. If this results in deleting or
 *  inserting a special, then if records is non-NULL, updates it to reflect the
 *  change. Otherwise, the change happens immediately. If unsafe is 1, inserts
 *  the thing in raw mode (i.e. without touching any external structures but
 *  the board, bypassing records, and not checking whether sensitive things
 *  like the player are being inserted).
 */
void put(World *world, Board *src_board, Layer layer, Point pt,
 Ctp ctp, SpecialRecordTable *records, int unsafe)
{
  int off, player_on_top, cur_flags, new_flags, under_flags;

  switch(layer)
  {
    case LAYER_BOARD:
      if(pt.x < 0 || pt.x >= src_board->board_width
          || pt.y < 0 || pt.y >= src_board->board_height)
        break;
      off = pt.x + pt.y * src_board->board_width;

      switch(pt.z)
      {
        case 0:
          if(!unsafe)
            if(!insert_special(world, src_board, &ctp, pt, records))
              break;

          cur_flags   = flags[(int)BOARD_THING(src_board, off)];
          new_flags   = flags[(int)ctp.thing];
          under_flags = flags[(int)BOARD_UNDER_THING(src_board, off)];

          /*
           *  Here's the logic. If the player is being put on something, it can
           *  always go underneath. Otherwise, it can be put underneath if it
           *  has the underneath flag, but two things with the underneath flag
           *  can't be stacked on top of one another.
           */
          if(ctp.thing == PLAYER
              || ((cur_flags & A_UNDER) && !(new_flags & A_UNDER)))
          {
            if(ctp.thing == PLAYER && (under_flags & A_UNDER))
            {
              /* Push layer 1 down to layer 2. */
              world->under_player_color = ((Uint8 *)
                  src_board->level_under_color)[off];
              world->under_player_id = src_board->level_under_id[off];
              world->under_player_param = ((Uint8 *)
                  src_board->level_under_param)[off];
            }

            /* Push layer 0 down to layer 1. */
            src_board->level_under_color[off] = src_board->level_color[off];
            src_board->level_under_id[off]    = src_board->level_id[off];
            src_board->level_under_param[off] = src_board->level_param[off];
          }
          else
          {
            /* Squash the layer 2 thing. */
            if(ctp.thing == PLAYER)
            {
              if(!unsafe)
              {
                Ctp under_ctp;
                under_ctp.color = world->under_player_color;
                under_ctp.thing = world->under_player_id;
                under_ctp.param = world->under_player_param;
                delete_special(world, src_board, under_ctp, pt,
                    records);
              }
              world->under_player_color = 0;
              world->under_player_id    = 0;
              world->under_player_param = 0;
            }

            /* Squash the layer 1 thing. */
            if(!unsafe)
            {
              Ctp under_ctp;
              under_ctp.color = ((Uint8 *)src_board->level_under_color)[off];
              under_ctp.thing = ((mzx_thing *)src_board->level_under_id)[off];
              under_ctp.param = ((Uint8 *)src_board->level_under_param)[off];
              delete_special(world, src_board, under_ctp, pt, records);
            }
            src_board->level_under_color[off] = 0;
            src_board->level_under_id[off]    = 0;
            src_board->level_under_param[off] = 0;
          }

          ((Uint8 *)src_board->level_color)[off]  = ctp.color;
          ((mzx_thing *)src_board->level_id)[off]   = ctp.thing;
          ((Uint8 *)src_board->level_param)[off]  = ctp.param;
          break;

        case 1:
          player_on_top = src_board->level_id[off] == PLAYER;
          new_flags     = flags[(int)ctp.thing];
          under_flags   = flags[(int)src_board->level_under_id[off]];

          /*
           *  Does the object want to go there?
           *  (Remember that anything can go under the player.)
           */ 
          if(!(new_flags & A_UNDER) && !player_on_top)
            break;

          /*
           *  We might be able to push what is already here down a layer, but
           *  not if that would lead to two underneath things being on top of
           *  one another (and only if layer 2 exists in the first place).
           */
          if(player_on_top && (under_flags & A_UNDER)
              && !(new_flags & A_UNDER))
          {
            world->under_player_color = ((Uint8 *)
                src_board->level_under_color)[off];
            world->under_player_id = src_board->level_under_id[off];
            world->under_player_param = ((Uint8 *)
                src_board->level_under_param)[off];
          }
          else
          {
            /* It'll be overwritten, so check if it needs to be deleted. */
            if(!unsafe)
            {
              Ctp under_ctp;
              under_ctp.color = ((Uint8 *)src_board->level_under_color)[off];
              under_ctp.thing = ((mzx_thing *)src_board->level_under_id)[off];
              under_ctp.param = ((Uint8 *)src_board->level_under_param)[off];
              delete_special(world, src_board, under_ctp, pt, records);
            }
          }

          ((Uint8 *)src_board->level_under_color)[off]  = ctp.color;
          ((Uint8 *)src_board->level_under_id)[off]     = ctp.thing;
          ((Uint8 *)src_board->level_under_param)[off]  = ctp.param;
          break;

        case 2:
          player_on_top = src_board->level_id[off] == PLAYER;
          new_flags     = flags[(int)ctp.thing];
          under_flags   = flags[(int)src_board->level_under_id[off]];

          /*
           *  We can only do this if:
           *   - the player is on top, so this layer exists at all;
           *   - we have the underneath flag;
           *   - whatever is on layer 1 does *not* have the underneath flag,
           *     to avoid violating the constraint that two underneath things
           *     can't be on top of one another.
           */
          if(player_on_top && (new_flags & A_UNDER)
              && !(under_flags & A_UNDER))
          {
            world->under_player_color = ctp.color;
            world->under_player_id    = (int)ctp.thing;
            world->under_player_id    = ctp.param;
          }
      }
      break;

    case LAYER_OVERLAY:
      if(pt.x < 0 || pt.x >= src_board->board_width
          || pt.y < 0 || pt.y >= src_board->board_height)
        break;
      off = pt.x + pt.y * src_board->board_width;

      ((Uint8 *)src_board->overlay_color)[off]  = ctp.color;
      ((Uint8 *)src_board->overlay)[off]        = ctp.param;
      break;

    case LAYER_VLAYER:
      if(pt.x < 0 || pt.x >= world->vlayer_width
          || pt.y < 0 || pt.y >= world->vlayer_height)
        break;
      off = pt.x + pt.y * world->vlayer_width;

      ((Uint8 *)world->vlayer_colors)[off]  = ctp.color;
      ((Uint8 *)world->vlayer_chars)[off]   = ctp.param;
      break;
  };
}

/*
 *  Removes the thing at the given point on the given layer, shifting things up
 *  as necessary. If unsafe is 1, records is completely bypassed, allowing you
 *  to delete things like the player or robots from the board without touching
 *  their structures.
 */
void delete_from(World *world, Board *src_board, Layer layer, Point pt,
 SpecialRecordTable *records, int unsafe)
{
  int off;
  Ctp ctp;

  switch(layer)
  {
    case LAYER_BOARD:
      if(pt.x < 0 || pt.x >= src_board->board_width
          || pt.y < 0 || pt.y >= src_board->board_height)
        break;
      off = pt.x + pt.y * src_board->board_width;

      ctp.color = ((Uint8 *)src_board->level_color)[off];
      ctp.param = ((Uint8 *)src_board->level_param)[off];
      ctp.thing = ((mzx_thing *)src_board->level_id)[off];

      switch(pt.z)
      {
        case 0:
          if(!unsafe)
            if(!delete_special(world, src_board, ctp, pt, records))
              break; 

          /* Pull whatever was underneath up one layer. */
          BOARD_COLOR(src_board, off)
            = BOARD_UNDER_COLOR(src_board, off);
          BOARD_THING(src_board, off)
            = BOARD_UNDER_THING(src_board, off);
          BOARD_PARAM(src_board, off)
            = BOARD_UNDER_PARAM(src_board, off);

          /* Pull layer 2 up to layer 1, if it exists. */
          if(ctp.thing == PLAYER)
          {
            BOARD_UNDER_COLOR(src_board, off)
              = PLAYER_UNDER_COLOR(world);
            BOARD_UNDER_THING(src_board, off)
              = PLAYER_UNDER_THING(world);
            BOARD_UNDER_PARAM(src_board, off)
              = PLAYER_UNDER_PARAM(world);

            PLAYER_UNDER_COLOR(world) = 0;
            PLAYER_UNDER_THING(world) = SPACE;
            PLAYER_UNDER_PARAM(world) = 0;
          }
          break;

        case 1:
          if(!unsafe)
            if(!delete_special(world, src_board, ctp, pt, records))
              break; 

          if(ctp.thing == PLAYER)
          {
            ((Uint8 *)src_board->level_under_color)[off]
              = world->under_player_color;
            ((mzx_thing *)src_board->level_under_id)[off]
              = (mzx_thing)world->under_player_id;
            ((Uint8 *)src_board->level_under_param)[off]
              = world->under_player_param;

            world->under_player_color = 0;
            world->under_player_id = SPACE;
            world->under_player_param = 0;
          }
          else
          {
            ((Uint8 *)src_board->level_under_color)[off]  = 0;
            ((mzx_thing *)src_board->level_under_id)[off]   = SPACE;
            ((Uint8 *)src_board->level_under_param)[off]  = 0;
          }
          break;

        case 2:
          if(ctp.thing == PLAYER)
          {
            world->under_player_color = 0;
            world->under_player_id = SPACE;
            world->under_player_param = 0;
          }
      }
      break;

    case LAYER_OVERLAY:
      if(pt.x < 0 || pt.x >= src_board->board_width
          || pt.y < 0 || pt.y >= src_board->board_height)
        break;
      off = pt.x + pt.y * src_board->board_width;

      src_board->overlay[off]       = 0;
      src_board->overlay_color[off] = 0;
      break;

    case LAYER_VLAYER:
      if(pt.x < 0 || pt.x >= world->vlayer_width
          || pt.y < 0 || pt.y >= world->vlayer_height)
        break;
      off = pt.x + pt.y * world->vlayer_width;

      world->vlayer_chars[off]  = 0;
      world->vlayer_colors[off] = 0;
  }
}

/*
 *  Performs a move. Uses its own records table; therefore, records are not
 *  available. For the same reason, there's no such thing as an "unsafe move";
 *  just delete and put if you want a raw move.
 */
void move_from(World *world, Board *src_board, Layer layer, Point src_pt,
 Point dest_pt)
{
  Ctp ctp;
  SpecialRecordTable *records;

  records = create_special_record_table();

  ctp = yank(world, src_board, layer, src_pt);
  delete_from(world, src_board, layer, src_pt, records, 0);
  put(world, src_board, layer, dest_pt, ctp, records, 0);

  commit_special_record_table(world, src_board, records);
  destroy_special_record_table(records);
}

static Ctp extract_player(World *world, Board *src_board)
{
  Ctp ctp = { 0, 0, 0 };

  if(world->current_board == src_board)
  {
    Point pt;
    pt.x = world->player_x;
    pt.y = world->player_y;
    pt.z = 0;

    /* fprintf(stderr, "%d:%d:%d %d:%d:%d %d:%d:%d\n",
        (int)src_board->level_color[BOARD_OFFSET(src_board, pt.x, pt.y)],
        (int)src_board->level_id[BOARD_OFFSET(src_board, pt.x, pt.y)],
        (int)src_board->level_param[BOARD_OFFSET(src_board, pt.x, pt.y)],
        (int)src_board->level_under_color[BOARD_OFFSET(src_board, pt.x,
          pt.y)],
        (int)src_board->level_under_id[BOARD_OFFSET(src_board, pt.x, pt.y)],
        (int)src_board->level_under_param[BOARD_OFFSET(src_board, pt.x,
          pt.y)],
        (int)world->under_player_color,
        (int)world->under_player_id,
        (int)world->under_player_param); */

    ctp = yank(world, src_board, LAYER_BOARD, pt);
    delete_from(world, src_board, LAYER_BOARD, pt, NULL, 1);
  }

  return ctp;
}

static void reinject_player(World *world, Board *src_board, Ctp ctp)
{
  if(world->current_board == src_board)
  {
    Point pt;

    pt.x = world->player_x;
    pt.y = world->player_y;
    pt.z = 0;

    put(world, src_board, LAYER_BOARD, pt, ctp, NULL, 1);
  }
}

static Size get_layer_bounds(World *world, Board *src_board, Layer layer)
{
  Size sz;
  switch(layer)
  {
    case LAYER_BOARD:
    case LAYER_OVERLAY:
      sz.w = src_board->board_width;
      sz.h = src_board->board_height;
      break;
    case LAYER_VLAYER:
      sz.w = world->vlayer_width;
      sz.h = world->vlayer_height;
      break;
    default:
      ASSERT(0);    /* shut up the compiler */
  }
  return sz;
}

static int check_rect(World *world, Board *src_board, Layer layer,
 Point pt, Size size)
{
  Size layer_size;
  layer_size = get_layer_bounds(world, src_board, layer);
  return pt.x >= 0 && pt.x + size.w <= layer_size.w && pt.y >= 0
    && pt.y + size.h <= layer_size.h && size.w >= 0 && size.h >= 0;
}

/* Squashes layer 0 and layer 1 at the given offset. */
static int erase_special(World *world, Board *src_board, Point pt,
 SpecialRecordTable *records)
{
  Ctp ctp;
  int off;
  off = BOARD_OFFSET(src_board, pt.x, pt.y);

  ctp.color = BOARD_COLOR(src_board, off);
  ctp.thing = BOARD_THING(src_board, off);
  ctp.param = BOARD_PARAM(src_board, off);
  if(!delete_special(world, src_board, ctp, pt, records))
    return 0;

  ctp.color = ((Uint8 *)src_board->level_under_color)[off];
  ctp.thing = BOARD_UNDER_THING(src_board, off);
  ctp.param = ((Uint8 *)src_board->level_under_param)[off];
  if(!delete_special(world, src_board, ctp, pt, records))
    return 0;

  return 1;
}

/*
 *  When you just have to get rid of something, use this function. It's useful
 *  for when you called erase_special() above but you couldn't actually
 *  do the insertion the way you wanted, so you have to clear it out (because
 *  you can't just leave the thing there if you already erased with
 *  erase_special).
 */
static void erase_unsafe(World *world, Board *src_board, Point pt)
{
  int off = BOARD_OFFSET(src_board, pt.x, pt.y);
  BOARD_COLOR(src_board, off) = 0;
  BOARD_THING(src_board, off) = SPACE;
  BOARD_PARAM(src_board, off) = 0;
  BOARD_UNDER_COLOR(src_board, off) = 0;
  BOARD_UNDER_THING(src_board, off) = SPACE;
  BOARD_UNDER_PARAM(src_board, off) = 0;
}

static void copy_row_from_board_to_board(World *world, Board *src_board,
 Point src_pt, int width, Point dest_pt, SpecialRecordTable *records)
{
  int doff, soff;
  int forwards, stop;

  doff = BOARD_OFFSET(src_board, dest_pt.x, dest_pt.y);
  soff = BOARD_OFFSET(src_board, src_pt.x, src_pt.y);

  if((forwards = doff <= soff))
    stop = doff + width;
  else
  {
    stop = doff - 1;
    doff += width - 1;
    soff += width - 1;
    dest_pt.x += width - 1;
  }

  while(doff != stop)
  {
    if(!erase_special(world, src_board, dest_pt, records))
      goto next;

    Ctp ctp;
    ctp.color = BOARD_COLOR(src_board, soff);
    ctp.thing = BOARD_THING(src_board, soff);
    ctp.param = BOARD_PARAM(src_board, soff);

    if(!insert_special(world, src_board, &ctp, dest_pt, records))
    {
      /*
       *  We still resolved to delete it once we called erase_special, so
       *  do that now.
       */
      erase_unsafe(world, src_board, dest_pt);
      goto next;
    }

    BOARD_COLOR(src_board, doff) = ctp.color;
    BOARD_THING(src_board, doff) = ctp.thing;
    BOARD_PARAM(src_board, doff) = ctp.param;

    ctp.color = BOARD_UNDER_COLOR(src_board, soff);
    ctp.thing = BOARD_UNDER_THING(src_board, soff);
    ctp.param = BOARD_UNDER_PARAM(src_board, soff);

    if(!insert_special(world, src_board, &ctp, dest_pt, records))
    {
      erase_unsafe(world, src_board, dest_pt);
      goto next;
    }

    BOARD_UNDER_COLOR(src_board, doff) = ctp.color;
    BOARD_UNDER_THING(src_board, doff) = ctp.thing;
    BOARD_UNDER_PARAM(src_board, doff) = ctp.param;

next:
    if(forwards)
      doff++, soff++, dest_pt.x++, src_pt.x++;
    else
      doff--, soff--, dest_pt.x--, src_pt.x--;
  }
}

static void copy_row_from_board_to_overlay(World *world, Board *src_board,
 Point src_pt, int width, Point dest_pt, HashTable *special_ref_table)
{
  int j, soff, doff;

  soff = BOARD_OFFSET(src_board, src_pt.x, src_pt.y);
  doff = OVERLAY_OFFSET(src_board, dest_pt.x, dest_pt.y);

  for (j = 0; j < width; j++)
  {
    src_board->overlay[doff + j]        = get_id_char(src_board, soff + j);
    src_board->overlay_color[doff + j]  = get_id_color(src_board, soff + j);
  }
}

static void copy_row_from_board_to_vlayer(World *world, Board *src_board,
 Point src_pt, int width, Point dest_pt, HashTable *special_ref_table)
{
  int j, soff, doff;

  soff = BOARD_OFFSET(src_board, src_pt.x, src_pt.y);
  doff = OVERLAY_OFFSET(src_board, dest_pt.x, dest_pt.y);

  for (j = 0; j < width; j++)
  {
    world->vlayer_chars[doff + j]   = get_id_char(src_board, soff + j);
    world->vlayer_colors[doff + j]  = get_id_color(src_board, soff + j);
  }
}

static void copy_row_from_overlay_to_board(World *world, Board *src_board,
 Point src_pt, int width, Point dest_pt, SpecialRecordTable *records)
{
  int j, soff, doff;

  doff = BOARD_OFFSET(src_board, dest_pt.x, dest_pt.y);
  soff = OVERLAY_OFFSET(src_board, src_pt.x, src_pt.y);

  for (j = 0; j < width; j++, dest_pt.x++)
  {
    erase_special(world, src_board, dest_pt, records);

    ((Uint8 *)src_board->level_color)[doff + j]
      = ((Uint8 *)src_board->overlay_color)[soff + j];
    ((Uint8 *)src_board->level_param)[doff + j]
      = ((Uint8 *)src_board->overlay)[soff + j];
  }

  memset(src_board->level_id + doff, CUSTOM_BLOCK, width);
  memset(src_board->level_under_color + doff, 0, width);
  memset(src_board->level_under_id + doff, 0, width);
  memset(src_board->level_under_param + doff, 0, width);
}

static void copy_row_from_overlay_to_overlay(World *world, Board *src_board,
 Point src_pt, int width, Point dest_pt, HashTable *special_ref_table)
{
  int doff, soff;
  doff = OVERLAY_OFFSET(src_board, dest_pt.x, dest_pt.y);
  soff = OVERLAY_OFFSET(src_board, src_pt.x, src_pt.y);

  memmove(src_board->overlay + doff, src_board->overlay + soff, width);
  memmove(src_board->overlay_color + doff, src_board->overlay_color + soff,
      width);
}

static void copy_row_from_overlay_to_vlayer(World *world, Board *src_board,
 Point src_pt, int width, Point dest_pt, HashTable *special_ref_table)
{
  int doff, soff;
  doff = VLAYER_OFFSET(world, dest_pt.x, dest_pt.y);
  soff = OVERLAY_OFFSET(src_board, src_pt.x, src_pt.y);

  memcpy(world->vlayer_chars + doff, src_board->overlay + soff, width);
  memcpy(world->vlayer_colors + doff, src_board->overlay_color + soff,
      width);
}

static void copy_row_from_vlayer_to_board(World *world, Board *src_board,
 Point src_pt, int width, Point dest_pt, SpecialRecordTable *records)
{
  int j, soff, doff;

  doff = BOARD_OFFSET(src_board, dest_pt.x, dest_pt.y);
  soff = VLAYER_OFFSET(world, src_pt.x, src_pt.y);

  for (j = 0; j < width; j++, dest_pt.x++)
  {
    erase_special(world, src_board, dest_pt, records);

    ((Uint8 *)src_board->level_color)[doff + j]
      = ((Uint8 *)world->vlayer_colors)[soff + j];
    ((Uint8 *)src_board->level_param)[doff + j]
      = ((Uint8 *)world->vlayer_chars)[soff + j];
  }

  memset(src_board->level_id + doff, CUSTOM_BLOCK, width);
  memset(src_board->level_under_color + doff, 0, width);
  memset(src_board->level_under_id + doff, 0, width);
  memset(src_board->level_under_param + doff, 0, width);
}

static void copy_row_from_vlayer_to_overlay(World *world, Board *src_board,
 Point src_pt, int width, Point dest_pt, HashTable *special_ref_table)
{
  int doff, soff;
  doff = OVERLAY_OFFSET(src_board, dest_pt.x, dest_pt.y);
  soff = VLAYER_OFFSET(world, src_pt.x, src_pt.y);

  memcpy(src_board->overlay + doff, world->vlayer_chars + soff, width);
  memcpy(src_board->overlay_color + doff, world->vlayer_colors + soff,
      width);
}

static void copy_row_from_vlayer_to_vlayer(World *world, Board *src_board,
 Point src_pt, int width, Point dest_pt, HashTable *special_ref_table)
{
  int doff, soff;
  doff = VLAYER_OFFSET(world, dest_pt.x, dest_pt.y);
  soff = VLAYER_OFFSET(world, src_pt.x, src_pt.y);

  memmove(world->vlayer_chars + doff, world->vlayer_chars + soff,
      width);
  memmove(world->vlayer_colors + doff, world->vlayer_colors + soff,
      width);
}

static void(*row_copiers[])(World *, Board *, Point, int, Point, HashTable *) =
{
  copy_row_from_board_to_board,       /* SLOW! */
  copy_row_from_board_to_overlay,     /* slow */
  copy_row_from_board_to_vlayer,      /* slow */
  copy_row_from_overlay_to_board,     /* kinda slow */ 
  copy_row_from_overlay_to_overlay,   /* fast */
  copy_row_from_overlay_to_vlayer,    /* fast */
  copy_row_from_vlayer_to_board,      /* kinda slow */
  copy_row_from_vlayer_to_overlay,    /* fast */
  copy_row_from_vlayer_to_vlayer      /* fast */
};

/*
 *  Does bounds checking and returns 1 on success or 0 if one box or the other
 *  is out of range.
 */
int copy_block(World *world, Board *src_board, Layer src_layer,
 Point src_pt, Size size, Layer dest_layer, Point dest_pt)
{
  Ctp player;
  SpecialRecordTable *table;
  void(*copier)(World *, Board *, Point, int, Point, HashTable *);
  int forwards, stop;

#if 0
  fprintf(stderr, "COPYING: (%d, %d)x(%d, %d)=(%d, %d)\n", dest_pt.x,
      dest_pt.y, size.w, size.h, src_pt.x, src_pt.y);
  if(src_layer == LAYER_OVERLAY || dest_layer == LAYER_OVERLAY)
    return 0;
#endif

  if(!check_rect(world, src_board, src_layer, src_pt, size)
      || !check_rect(world, src_board, dest_layer, dest_pt, size))
  {
    fprintf(stderr, "warning: copy block fails due to out of range\n");
    return 0;
  }

  table = create_special_record_table();
  player = extract_player(world, src_board);
  copier = row_copiers[(int)src_layer * 3 + (int)dest_layer];

  /* Copy forwards or backwards in order to prevent overwriting. */
  forwards = dest_pt.y <= src_pt.y;
  if(forwards)
    stop = dest_pt.y + size.h;
  else
  {
    stop = dest_pt.y - 1;
    dest_pt.y += size.h - 1;
    src_pt.y += size.h - 1;
  }

  while(dest_pt.y != stop)
  {
    copier(world, src_board, src_pt, size.w, dest_pt, table);

    if(forwards)
      src_pt.y++, dest_pt.y++;
    else
      src_pt.y--, dest_pt.y--;
  }

  commit_special_record_table(world, src_board, table);
  destroy_special_record_table(table);

  reinject_player(world, src_board, player);

  return 1;
}

static void erase_row_from_board(World *world, Board *src_board,
 Point dest_pt, int width)
{
  int stop;
  for (stop = dest_pt.x + width; dest_pt.x != stop; dest_pt.x++)
  {
    erase_special(world, src_board, dest_pt, NULL);
    erase_unsafe(world, src_board, dest_pt);
  }
}

static void erase_row_from_overlay(World *world, Board *src_board,
 Point dest_pt, int width)
{
  int off;
  off = OVERLAY_OFFSET(src_board, dest_pt.x, dest_pt.y);

  memset(src_board->overlay + off, ' ', width);
  memset(src_board->overlay_color + off, '\0', width);
}

static void erase_row_from_vlayer(World *world, Board *src_board,
 Point dest_pt, int width)
{
  int off;
  off = VLAYER_OFFSET(world, dest_pt.x, dest_pt.y);

  memset(world->vlayer_chars + off, ' ', width);
  memset(world->vlayer_colors + off, '\0', width);
}

static void(*row_erasers[])(World *, Board *, Point, int) =
{
  erase_row_from_board,       /* slow */
  erase_row_from_overlay,     /* fast */
  erase_row_from_vlayer       /* fast */
};

int erase_block(World *world, Board *src_board, Layer dest_layer,
 Point dest_pt, Size size)
{
  Ctp player;
  void(*eraser)(World *, Board *, Point, int);
  int stop;

  if(!check_rect(world, src_board, dest_layer, dest_pt, size))
  {
    fprintf(stderr, "warning: erase block fails due to out of range\n");
    return 0;
  }

  player = extract_player(world, src_board);
  eraser = row_erasers[(int)dest_layer];

  for (stop = dest_pt.y + size.h; dest_pt.y != stop; dest_pt.y++)
    eraser(world, src_board, dest_pt, size.w);

  reinject_player(world, src_board, player);

  return 1;
}

static Point add_points(Point a, Point b)
{
  Point c;
  c.x = a.x + b.x;
  c.y = a.y + b.y;
  c.z = a.z + b.z;
  return c;
}

static Point subtract_points(Point a, Point b)
{
  Point c;
  c.x = a.x - b.x;
  c.y = a.y - b.y;
  c.z = a.z - b.z;
  return c;
}

/*
 *  Alters a (point, size) pair so that it fits within a rectangle whose origin
 *  is (0, 0) and whose size is given by layer_size.
 */
static void constrain_rect(Point *pt, Size *size, Size layer_size)
{
  int disp;

  if(pt->x < 0)
  {
    disp = -pt->x;
    pt->x = 0; 
    size->w -= disp;
  }
  if(pt->y < 0)
  {
    disp = -pt->y;
    pt->y = 0;
    size->h -= disp;
  }
  if(pt->x + size->w > layer_size.w)
  {
    disp = pt->x + size->w - layer_size.w;
    size->w -= disp;
  }
  if(pt->y + size->h > layer_size.h)
  {
    disp = pt->y + size->h - layer_size.h;
    size->h -= disp;
  }
}

void copy_clipped_block(World *world, Board *src_board, Layer src_layer,
 Point src_pt, Size size, Layer dest_layer, Point dest_pt)
{
  if(!check_rect(world, src_board, src_layer, src_pt, size)
      || !check_rect(world, src_board, dest_layer, dest_pt, size))
  {
    /* Slow code path. */
    Size src_layer_size, dest_layer_size;
    src_layer_size = get_layer_bounds(world, src_board, src_layer);
    dest_layer_size = get_layer_bounds(world, src_board, dest_layer);

    Point orig_dest_pt, orig_src_pt;
    orig_dest_pt = dest_pt; orig_src_pt = src_pt;

    constrain_rect(&dest_pt, &size, dest_layer_size);
    src_pt = add_points(src_pt, subtract_points(dest_pt, orig_dest_pt));

    erase_block(world, src_board, dest_layer, dest_pt, size);

    constrain_rect(&src_pt, &size, src_layer_size);
    dest_pt = add_points(dest_pt, subtract_points(src_pt, orig_src_pt));
  }

  copy_block(world, src_board, src_layer, src_pt, size, dest_layer, dest_pt);
}
