/*
 * X11 interface for Atom-4 game board
 * Implementation file
 *
 * $Id: xtriboard.cc,v 1.24 2003/03/13 02:25:17 hsteoh Exp hsteoh $
 */

#include "exception.h"
#include "xtriboard.h"


#define BORDER_WIDTH		1
#define CURSOR_Y_OFFSET		-3	// offset from piece already on board


#ifndef DATADIR				// should be defined by build script
#define DATADIR		"."		// search current dir if undefined
#endif

#define TILE_XPM	DATADIR "/tritile12.xpm"

char *xtriboard::ballnames[NUM_BALLS]={
  DATADIR "/blackball12.xpm",
  DATADIR "/redball12.xpm",
  DATADIR "/greenball12.xpm",
  DATADIR "/yellowball12.xpm",
  DATADIR "/blueball12.xpm",
  DATADIR "/purpleball12.xpm",
  DATADIR "/turqball12.xpm",
  DATADIR "/whiteball12.xpm"
};

// To simplify the calculation, we approximate the marble's area as a hexagon
// bounded by the perpendiculars to our tile boundaries, and use linear
// equations to calculate our position.
void xtriboard::calc_bcoor(int rx, int ry, int &bx, int &by) {
  int xbase = rx / CELL_WIDTH;
  int xrem = rx % CELL_WIDTH;
  int ybase = ry / CELL_HEIGHT;
  int yrem = ry % CELL_HEIGHT;

  // Notes:
  // - the equations we use are based on the fact that an equilateral triangle
  //   of w units per side has its center point at a distance of w/sqrt(3)
  //   units from each side.
  // - we're using 173/100 as an approximation of sqrt(3).
  // - because we're shifting the entire board down by (1/2,1/2) so that
  //   marbles at (0,0) are not clipped, our (bx,by) calculations are
  //   skewed by (-1,-1) so that they map correctly to real board coors.
  if (ybase%2 == 0) {			// even row (we number rows from 0)
    if (xrem < CELL_WIDTH/2) {		// upperleft quadrant
      // Determine which side of y = (CELL_WIDTH-x)/sqrt(3) we're on
      if (173*yrem < 100*(CELL_WIDTH-xrem)) {
        // in upperleft tritant
        bx = xbase-1;
        by = ybase-1;
        return;
      }
    } else {				// upperright quadrant
      // Determine which side of y = x/sqrt(3) we're on
      if (173*yrem < 100*xrem) {
        // in upperright tritant
        bx = xbase;
        by = ybase-1;
        return;
      }
    }

    // Neither; we must be in lower tritant (center marble)
    bx = xbase;
    by = ybase;

  } else {				// odd row
    if (xrem < CELL_WIDTH/2) {		// lowerleft quadrant
      // Determine which side of (CELL_HEIGHT-y) = (CELL_WIDTH-x)/sqrt(3)
      // we're on
      if (173*(CELL_HEIGHT-yrem) < 100*(CELL_WIDTH-xrem)) {
        // in lowerleft tritant
        bx = xbase-1;
        by = ybase;
        return;
      }
    } else {
      // Determine which side of (CELL_HEIGHT-y) = x/sqrt(3) we're on
      if (173*(CELL_HEIGHT-yrem) < 100*xrem) {
        // in lowerright tritant
        bx = xbase;
        by = ybase;
        return;
      }
    }

    // Neither; we must be in upper tritant (center marble)
    bx = xbase;
    by = ybase-1;
  } // endelse(odd/even row)
}

// Notes:
// - we add half a cell width to the width because odd rows are shifted to
//   the right by half a cell's width.
// - on top of this, we need to add one full cell's width, to give room for
//   marbles at the edge of the board not to be clipped.
// - similarly, for the height, we need to add one full cell's height
xtriboard::xtriboard(xconnection *connection, xsprite_engine *engine,
                     xwindow *parent, atom4 *g, int x, int y) :
	xwindow(connection, parent, x,y,
                CELL_WIDTH*g->board_width() + CELL_WIDTH/2,
                CELL_HEIGHT*(g->board_height()+1),
	        BORDER_WIDTH, connection->black(), connection->black()),
	eng(engine), game(g) {
  Display *disp = conn->display();

  cell_wd = g->board_width();
  cell_dp = g->board_height();

  // Create window buffer ((wd,ht) set up by base ctor)
  buffer = XCreatePixmap(disp, win, wd, ht, conn->scrn_depth());

  tritile=NULL;
  try {
    int i;
    tritile=new tiled_bckgnd(conn, buffer, TILE_XPM);
    tritile->paint(0,0, wd,ht);

    // (The following is for easier exception handling)
    for (i=0; i<NUM_BALLS; i++) balls[i] = NULL;

    for (i=0; i<NUM_BALLS; i++) {
      balls[i] = new xflatsprite(eng, ballnames[i]);
      if (!balls[i]) throw exception("@Error while loading %s\n",
                                     ballnames[i]);
    }

    cursor = new xcursor(eng, win, balls[game->current_tile()-'a'],
                         CELL_WIDTH, CELL_HEIGHT*2);

    // Select input that we're interested in
    XSelectInput(disp, win, ExposureMask |
                            EnterWindowMask | LeaveWindowMask |
                            PointerMotionMask |
                            FocusChangeMask);

    // Paint initial state to buffer
    game->get_board()->mapcell(drawcell, this);
  } catch(exception &e) {
    for (int i=0; i<NUM_BALLS; i++) {
      if (balls[i]) delete balls[i];
    }
    if (tritile) delete tritile;
    XFreePixmap(disp, buffer);
    throw;
  }
}

xtriboard::~xtriboard() {
  Display *disp = conn->display();

  delete cursor;
  for (int i=0; i<NUM_BALLS; i++) {
    if (balls[i]) delete balls[i];
  }
  delete tritile;
  XFreePixmap(disp, buffer);
}

void xtriboard::drawcell(int bx, int by, celltype cell, void *context) {
  xtriboard *bp = (xtriboard *)context;

  if (cell>='a' && cell<'a'+NUM_BALLS) {
    bp->draw_at(bx,by, bp->balls[cell-'a']);
  }
}

void xtriboard::refresh() {
  GC copygc = eng->get_copygc();

  // Redraw screen
  tritile->paint(0,0, wd,ht);
  game->get_board()->mapcell(drawcell, this);
  XCopyArea(conn->display(), buffer, win, copygc, 0,0, width(),height(), 0,0);

  // Re-sync cursor, just in case current tile has changed
  cursor->set_sprite(balls[game->current_tile()-'a']);
}

void xtriboard::expose(XExposeEvent ev) {
  int x=ev.x, y=ev.y, w=ev.width, h=ev.height;
  GC copygc = eng->get_copygc();

  XCopyArea(ev.display, buffer, win, copygc, x,y, w,h, x,y);
}

void xtriboard::mouse_enter(XEnterWindowEvent ev) {
  // nothing for now
}

void xtriboard::mouse_leave(XLeaveWindowEvent ev) {
  cursor->off();			// turn off cursor when mouse leaves
}

void xtriboard::mouse_move(XMotionEvent ev) {
  int x=ev.x, y=ev.y;
  int bx,by;

  if (game->is_local_turn()) {
    calc_bcoor(x,y,bx,by);
    if (game->check_legal(bx,by)) {
      cursor->move(real_xcoor(bx,by), real_ycoor(bx,by)+CURSOR_Y_OFFSET);
      cursor->on();
      return;
    }
  }

  // Not in a valid position, not our turn, or round is over, so hide the
  // auto cursor
  cursor->off();
}

