/*
 * ConnectN
 *
 * Copyright (C) Evan Harris, 1994, 1995.
 *
 * Permission is granted to freely redistribute and modify this code,
 * providing the author(s) get credit for having written it.
 */

#include <stdlib.h>
#include <stdio.h>
#include <time.h>
#include <unistd.h>
#include <vga.h>
#ifndef NOMOUSE
#include <vgamouse.h>
#endif

#include "c4.h"
#include "c4chip.h"
#include "vga16.h"
#include "mouse.h"
#include "key.h"


#if !defined(CLOCKS_PER_SECOND) && defined(CLOCKS_PER_SEC)
#define CLOCKS_PER_SECOND CLOCKS_PER_SEC
#endif

#define SCREENMODE G640x480x16
#define SCREENWIDTH 640
#define SCREENHEIGHT 480

#define BOARDWIDTH 552
#define BOARDHEIGHT SCREENHEIGHT

#define VERSION "Connect%d\n  v1.1"
#define VERSIONLEFT (SCREENWIDTH - 72)
#define VERSIONBOTTOM 200

#define QUITLEFT (SCREENWIDTH - 80)
#define QUITBOTTOM 76
#define QUITTOP (QUITBOTTOM - 15)
#define QUITRIGHT (QUITLEFT + 79)

#define NEWGAMELEFT QUITLEFT
#define NEWGAMEBOTTOM 48
#define NEWGAMETOP (NEWGAMEBOTTOM - 15)
#define NEWGAMERIGHT (NEWGAMELEFT + 79)

#define WINLEFT (SCREENWIDTH - 72)
#define WINBOTTOM 280

#define MOUSEKEYMOVE 8

#define PALETTESIZE 7

int palette[PALETTESIZE * 3] = {
    0x00, 0x00, 0x00,		/* black */
    0x00, 0x00, 0x30,		/* blue */
    0x30, 0x00, 0x00,		/* red */
    0x3f, 0x3f, 0x00,		/* yellow */
    0x3f, 0x3f, 0x3f,		/* white */
    0x20, 0x20, 0x20,		/* grey */
    0x00, 0x30, 0x00,		/* green */
};

#define TEXTFG 4
#define TEXTBG 0
#define BUTTONFG 0
#define BUTTONBG 5
#define BOARDFG 1
#define BOARDBG 0
#define THINKFG 5
#define THINKBG 0
#define BASEFG 6
#define MOUSEFG 6
#define MOUSEDARKFG 5

int turnfg[2] = {
    2, 3,
};

#define MINWIDTH 5
#define MAXWIDTH 21

#define NEWGAME -1
#define QUIT -2
#define UNKNOWN -8
#define ILLEGAL -9

#ifndef MOUSESAMPLERATE
#define MOUSESAMPLERATE MOUSE_DEFAULTSAMPLERATE
#endif

#define USLEEP_TIME 30000


static int columnthickness, columnleftedge, columntopedge;
static int columnheight, columns;
static int incolumn[MAXWIDTH];


void
NewGame(int width, int height)
{
    int w, h, l, x, y;
    
    if (columnthickness < 16) {
	for (w = 0; w < width; w++) {
	    for (h = 0; h < height; h++) {
		x = columnleftedge + (3 * w + 1) * columnthickness;
		y = columntopedge + (3 * h + 1) * columnthickness;
		for (l = 0; l < 2 * columnthickness; l++) {
		    vga16_drawscansegment(smallchiplines[smallchip[2][l]],
					  x, y++, 2 * columnthickness);
		}
	    }
	    incolumn[w] = 0;
	}
    } else {
	for (w = 0; w < width; w++) {
	    for (h = 0; h < height; h++) {
		x = columnleftedge + (3 * w + 1) * columnthickness;
		y = columntopedge + (3 * h + 1) * columnthickness;
		for (l = 0; l < 2 * columnthickness; l++) {
		    vga16_drawscansegment(bigchiplines[bigchip[2][l]], x, y++,
					  2 * columnthickness);
		}
	    }
	    incolumn[w] = 0;
	}
    }
    vga16_text(WINLEFT, WINBOTTOM, "        ", TEXTFG, TEXTBG);
}


void
InitDisplay(int width, int height, int connectnum, int reverse)
{
    int thick, i, l, t;
    char versionstr[20];

    vga_disabledriverreport();
    vga_init();
#if !defined(USEMOUSEFUNCS) && !defined(NOMOUSE)
    vga_setmousesupport(1);
#endif

    if (vga_setmode(SCREENMODE) != 0) {
	fprintf(stderr, "Mode %s not available!\n",
		vga_getmodename(SCREENMODE));
	exit(1);
    }

#if defined(USEMOUSEFUNCS) && !defined(NOMOUSE)
    mouse_init("/dev/mouse", vga_getmousetype(), MOUSESAMPLERATE);
    mouse_setxrange(0, SCREENWIDTH - 1);
    mouse_setyrange(0, SCREENHEIGHT - 1);
    mouse_setwrap(MOUSE_NOWRAP);
#endif

    vga16_init();

    vga_setpalvec(0, PALETTESIZE, &palette[0]);

    vga16_text(NEWGAMELEFT, NEWGAMEBOTTOM, " NEW GAME ", BUTTONFG, BUTTONBG);
    vga16_text(QUITLEFT, QUITBOTTOM, "   QUIT   ", BUTTONFG, BUTTONBG);
    sprintf(versionstr, VERSION, connectnum);
    vga16_text(VERSIONLEFT, VERSIONBOTTOM, versionstr, TEXTFG, TEXTBG);

    thick = 16;
    if (((width + 1) * thick + width * 2 * thick > BOARDWIDTH)
	|| ((height + 1) * thick + height * 2 * thick > BOARDHEIGHT)) {
	thick = 8;
    }

    columnthickness = thick;
    columnleftedge = (((BOARDWIDTH - ((width + 1) * thick + width * 2 * thick))
		       / 2) / 8) * 8;
    columntopedge =
	(BOARDHEIGHT - ((height + 1) * thick + height * 2 * thick)) / 2;
    columnheight = height;
    columns = width;

    vga16_filledblock(columnleftedge, columntopedge,
		      columnleftedge + (3 * width + 1) * thick - 1,
		      columntopedge + (3 * height + 1) * thick - 1,
		      BOARDFG);
    if (columnleftedge - thick >= 0
	&& columnleftedge + (3 * width + 2) * thick <= BOARDWIDTH
	&& columntopedge + (3 * height + 3) * thick <= BOARDHEIGHT) {
	vga16_filledblock(columnleftedge - thick,
			  columntopedge + (3 * height + 2) * thick,
			  columnleftedge - 1,
			  columntopedge + (3 * height + 3) * thick - 1,
			  BOARDFG);
	vga16_filledblock(columnleftedge + (3 * width + 1) * thick,
			  columntopedge + (3 * height + 2) * thick,
			  columnleftedge + (3 * width + 2) * thick - 1,
			  columntopedge + (3 * height + 3) * thick - 1,
			  BOARDFG);
    }
    if (columntopedge + (3 * height + 3) * thick <= BOARDHEIGHT) {
	vga16_filledblock(columnleftedge,
			  columntopedge + (3 * height + 1) * thick,
			  columnleftedge + thick - 1,
			  columntopedge + (3 * height + 3) * thick - 1,
			  BOARDFG);
	vga16_filledblock(columnleftedge + 3 * width * thick,
			  columntopedge + (3 * height + 1) * thick,
			  columnleftedge + (3 * width + 1) * thick - 1,
			  columntopedge + (3 * height + 3) * thick - 1,
			  BOARDFG);
	vga16_filledblock(0, columntopedge + (3 * height + 3) * thick,
			  BOARDWIDTH - 1, BOARDHEIGHT - 1, BASEFG);
    }

    NewGame(width, height);

    for (i = 0; i < width; i++) {
	incolumn[i] = 0;
    }

    if (reverse) {
	if (thick < 16) {
	    for (i = 0; i < 15; i++) {
		for (l = 0; l < 2 * thick; l++) {
		    if (smallchiplines[i][l] == turnfg[0]) {
			smallchiplines[i][l] = turnfg[1];
		    } else if (smallchiplines[i][l] == turnfg[1]) {
			smallchiplines[i][l] = turnfg[0];
		    }
		}
	    }
	} else {
	    for (i = 0; i < 30; i++) {
		for (l = 0; l < 2 * thick; l++) {
		    if (bigchiplines[i][l] == turnfg[0]) {
			bigchiplines[i][l] = turnfg[1];
		    } else if (bigchiplines[i][l] == turnfg[1]) {
			bigchiplines[i][l] = turnfg[0];
		    }
		}
	    }
	}
	t = turnfg[0];
	turnfg[0] = turnfg[1];
	turnfg[1] = t;
    }
}


void
EndDisplay()
{
    vga_setmode(TEXT);
#if defined(USEMOUSEFUNCS) && !defined(NOMOUSE)
    mouse_close();
#endif    
}


static int oldx = -1, oldy, oldcolour[40];

void
MakeMove(int turn, int move)
{
    int x, y, l;

    if (incolumn[move] < columnheight) {
	if (oldx != -1) {
	    RestoreUnderMousePointer(oldx, oldy, SCREENWIDTH, SCREENHEIGHT,
				     oldcolour);
	}

	incolumn[move]++;
	x = columnleftedge + (3 * move + 1) * columnthickness;
	y = columntopedge
	    + (3 * (columnheight - incolumn[move]) + 1) * columnthickness;
	if (columnthickness < 16) {
	    for (l = 0; l < 2 * columnthickness; l++) {
		vga16_drawscansegment(smallchiplines[smallchip[turn][l]],
				      x, y++, 2 * columnthickness);
	    }
	} else {
	    for (l = 0; l < 2 * columnthickness; l++) {
		vga16_drawscansegment(bigchiplines[bigchip[turn][l]], x, y++,
				      2 * columnthickness);
	    }
	}

	if (oldx != -1) {
	    SaveUnderMousePointer(oldx, oldy, SCREENWIDTH, SCREENHEIGHT,
				  oldcolour);
	    RenderMousePointer(oldx, oldy, MOUSEFG, MOUSEDARKFG,
			       SCREENWIDTH, SCREENHEIGHT);
	}
    }
}


void
ShowWin(int turn)
{
    if (turn) {
	vga16_text(WINLEFT, WINBOTTOM, " I WIN!", turnfg[turn], TEXTBG);
    } else {
	vga16_text(WINLEFT, WINBOTTOM, "YOU WIN!", turnfg[turn], TEXTBG);
    }
}


void
ShowTie()
{
    vga16_text(WINLEFT, WINBOTTOM, " A TIE!", TEXTFG, TEXTBG);
}


static long thinktime;

void
ThinkingOn()
{
    thinktime = clock();
    
    vga16_text(WINLEFT, WINBOTTOM, "THINKING", THINKFG, THINKBG);
    vga_runinbackground(1);
}


void
ThinkingOff()
{
    char buf[10];
    int timetaken = clock() - thinktime;

    while (!vga_oktowrite()) {
	usleep(100000);		/* 1/10 sec */
    }
    vga_runinbackground(0);
    vga16_text(WINLEFT, WINBOTTOM, "        ", THINKFG, THINKBG);
    sprintf(buf, "%.2fs", (double)timetaken / CLOCKS_PER_SECOND);
    vga16_text(WINLEFT + 4 * (8 - strlen(buf)), WINBOTTOM, buf,
	       THINKFG, THINKBG);
}


void
MoveMousePointer(int x, int y)
{
    if (x != oldx || y != oldy) {
	if (oldx != -1) {
	    RestoreUnderMousePointer(oldx, oldy, SCREENWIDTH, SCREENHEIGHT,
				     oldcolour);
	}
	SaveUnderMousePointer(x, y, SCREENWIDTH, SCREENHEIGHT, oldcolour);
	RenderMousePointer(x, y, MOUSEFG, MOUSEDARKFG, SCREENWIDTH,
			   SCREENHEIGHT);
	oldx = x;
	oldy = y;
    }
}


int
ParseMousePosition(int x, int y)
{
    int move;
    
    if (x >= NEWGAMELEFT && x <= NEWGAMERIGHT
	&& y >= NEWGAMETOP && y <= NEWGAMEBOTTOM) {
	return NEWGAME;
    } else if (x >= QUITLEFT && x <= QUITRIGHT
	       && y >= QUITTOP && y <= QUITBOTTOM) {
	return QUIT;
    } else if ((x - columnleftedge) % (columnthickness * 3)
	       >= columnthickness) {
	move = ((x - columnleftedge - columnthickness)
		/ (3 * columnthickness));
	if (move >= columns) {
	    return UNKNOWN;
	}
	return move;
    }

    return UNKNOWN;
}


int
GetMove(int width, int height)
{
    int move = UNKNOWN, key;

#if !defined(NOMOUSE)
    int x, y, button;
    
    if (oldx == -1) {
	x = mouse_getx();
	y = mouse_gety();
	MoveMousePointer(x, y);
    }
#else
    MoveMousePointer(0, 0);
#endif
    
    while (move == UNKNOWN) {
	usleep(USLEEP_TIME);	/* don't chew up as much CPU */
	
#if !defined(NOMOUSE)
	if (mouse_update()) {
	    x = mouse_getx();
	    y = mouse_gety();
	    button = mouse_getbutton();

	    MoveMousePointer(x, y);

	    if (button & MOUSE_LEFTBUTTON) {
		move = ParseMousePosition(x, y);
	    }
	}
#endif

	if ((key = key_getkey()) != -1 ) {
	    switch (key) {
	      case 'n':
	      case 'N':
		move = NEWGAME;
		break;
	      case 'q':
	      case 'Q':
		move = QUIT;
		break;
	      case '1':
	      case '2':
	      case '3':
	      case '4':
	      case '5':
	      case '6':
	      case '7':
	      case '8':
	      case '9':
		move = key - '1';
		if (move >= width || incolumn[move] == height) {
		    move = ILLEGAL;
		}
		break;
	      case KEY_CURSORUP:
		if (oldy >= MOUSEKEYMOVE) {
		    MoveMousePointer(oldx, oldy - MOUSEKEYMOVE);
		}
		break;
	      case KEY_CURSORDOWN:
		if (oldy < SCREENHEIGHT - MOUSEKEYMOVE) {
		    MoveMousePointer(oldx, oldy + MOUSEKEYMOVE);
		}
		break;
	      case KEY_CURSORLEFT:
		if (oldx >= MOUSEKEYMOVE) {
		    MoveMousePointer(oldx - MOUSEKEYMOVE, oldy);
		}
		break;
	      case KEY_CURSORRIGHT:
		if (oldx < SCREENWIDTH - MOUSEKEYMOVE) {
		    MoveMousePointer(oldx + MOUSEKEYMOVE, oldy);
		}
		break;
	      case '\n':
		move = ParseMousePosition(oldx, oldy);
		break;
	      default:
		break;
	    }
	}
    }

    return move;
}


void
Poll()
{
    int key, move;
#if !defined(NOMOUSE)
    int x, y, button;
#endif
    
    if (!vga_oktowrite()) {
	return;
    }
    
#if !defined(NOMOUSE)    
    if (mouse_update()) {
	x = mouse_getx();
	y = mouse_gety();
	button = mouse_getbutton();

	MoveMousePointer(x, y);

	if (button & MOUSE_LEFTBUTTON) {
	    move = ParseMousePosition(x, y);
	    if (move == QUIT) {
		EndDisplay();
		exit(0);
	    }
	}
    }
#endif

    if ((key = key_getkey()) != -1 ) {
	switch (key) {
	  case 'q':
	  case 'Q':
	    EndDisplay();
	    exit(0);
	  case KEY_CURSORUP:
	    if (oldy >= MOUSEKEYMOVE) {
		MoveMousePointer(oldx, oldy - MOUSEKEYMOVE);
	    }
	    break;
	  case KEY_CURSORDOWN:
	    if (oldy < SCREENHEIGHT - MOUSEKEYMOVE) {
		MoveMousePointer(oldx, oldy + MOUSEKEYMOVE);
	    }
	    break;
	  case KEY_CURSORLEFT:
	    if (oldx >= MOUSEKEYMOVE) {
		MoveMousePointer(oldx - MOUSEKEYMOVE, oldy);
	    }
	    break;
	  case KEY_CURSORRIGHT:
	    if (oldx < SCREENWIDTH - MOUSEKEYMOVE) {
		MoveMousePointer(oldx + MOUSEKEYMOVE, oldy);
	    }
	    break;
	  case '\n':
	    move = ParseMousePosition(oldx, oldy);
	    if (move == QUIT) {
		EndDisplay();
		exit(0);
	    }
	    break;
	  default:
	    break;
	}
    }
}


int
main(int argc, char **argv)
{
    int computerfirst = 0, height = 6, width = 7, connectnum = 4;
    int intelligence = 5, reverse = 0;
    int turn, move, gameover, end = 0;
    int c, err = 0;
    
    while ((c = getopt(argc, argv, "ch:i:n:rw:")) != -1) {
	switch (c) {
	  case 'c':
	    computerfirst = 1;
	    break;
	  case 'h':
	    height = atoi(optarg);
	    if (height < 4 || height > 19) {
		err++;
	    }
	    break;
	  case 'i':
	    intelligence = atoi(optarg);
	    if (intelligence < 1 || intelligence > 10) {
		err++;
	    }
	    break;
	  case 'n':
	    connectnum = atoi(optarg);
	    if (connectnum < 3 || connectnum > 10) {
		err++;
	    }
	    break;
	  case 'r':
	    reverse = TRUE;
	    break;
	  case 'w':
	    width = atoi(optarg);
	    if (width < MINWIDTH || height > MAXWIDTH) {
		err++;
	    }
	    break;
	  case '?':
	    err++;
	    break;
	}
    }
    if (err) {
	fprintf(stderr, "Usage: %s [-c] [-h N] [-i N] [-n N] [-r] [-w N]\n",
		argv[0]);
	fprintf(stderr, "\t-c    computer plays first\n");
	fprintf(stderr, "\t-h N  set board height to N (4-19)\n");
	fprintf(stderr, "\t-i N  computer player intelligence level (1-10)\n");
	fprintf(stderr, "\t-n N  set number to connect to N (3-10)\n");
	fprintf(stderr, "\t-r    reverse chip colours\n");
	fprintf(stderr, "\t-w N  set board width to N (%d-%d)\n",
		MINWIDTH, MAXWIDTH);
	exit(1);
    }

    InitDisplay(width, height, connectnum, reverse);

    poll(Poll, 0);
    
    new_game(width, height, connectnum);
    turn = computerfirst;
    gameover = 0;

    while (!end) {
	if (turn) {
	    ThinkingOn();
	    move = automatic_move(turn, intelligence);
	    ThinkingOff();
	    MakeMove(turn, move);
	} else {
	    move = GetMove(width, height);
	    switch (move) {
	      case NEWGAME:
		end_game();
		new_game(width, height, connectnum);
		NewGame(width, height);
		turn = !computerfirst; /* changed below */
		gameover = 0;
		break;
	      case QUIT:
		end = 1;
		break;
	      case ILLEGAL:
		break;
	      default:
		if (!gameover) {
		    make_move(turn, move);
		    MakeMove(turn, move);
		}
		break;
	    }
	}
	if (move != ILLEGAL && !gameover) {
	    if (is_winner(turn) || is_tie()) {
		if (is_winner(turn)) {
		    ShowWin(turn);
		} else {
		    ShowTie();
		}
		turn = 0;
		gameover = 1;
	    } else {
		turn = !turn;
	    }
	}
    }

    EndDisplay();

    return 0;
}
