/*  Motti -- a strategy game
    Copyright (C) 1999 Free Software Foundation

    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 St, Fifth Floor, Boston, MA  02110-1301  USA
*/

#include <config.h>

#ifdef HAVE_NCURSES_H
#include <ncurses.h>
#else
#include <curses.h>
#endif

#include <stdlib.h>
#include <unistd.h>
#ifndef _POSIX_JOB_CONTROL
#include <stdio.h>
#endif

#include "curses_if.h"
#include "map.h"
#include "occupy.h"
#include "comm.h"

enum colors {
  COL_SEA = 1,	/* Sea.  */
  COL_ME,	/* Player in turn.  */
  COL_REMOTE,	/* Remote active player.  */
  COL_OTHER,	/* Other players.  */
  /* TODO: change invade routines to support these two.  */
  COL_ENC,	/* Last encircled and occupied cells.  */
  COL_O_ENC,	/* Last encircled cells in other players' control.  */
  COL_CAP,	/* Color of capital.  */
  COL_R_CAP,	/* Same, remote active player.  */
  COL_O_CAP,	/* Same, inactive players.  */
  COL_OCC,	/* Color of occupied areas.  */
  COL_R_OCC,	/* Same, remote active player.  */
  COL_O_OCC	/* Same, inactive players.  */
};

static chtype choose_color (int);
static chtype first_char (int);
static chtype second_char (map_val);
static void draw_map (void);
static void draw_statusline (void);
static void move_curs (int, int);
static enum refresh_amount read_char (void);

static bool use_color = FALSE;
static WINDOW *map_pad, *status_line;

static int map_x, map_y;
static int map_max_x, map_max_y;
static int curs_x, curs_y;
static int map_physical_right, map_physical_bottom;
static int player_mask;
static int ask_quit;

extern void
curses_game_init (info)
     union conn_info *info;
{
  const chtype turn_text[] = {'t', 'u', 'r', 'n', '\0'};
  const chtype cross_text[] = {'c', 'r', 'o', 's', 's', 'e', 's', '\0'};

  atexit (kill_curses);
  initscr ();
  cbreak ();
  noecho ();
  nonl ();
  intrflush (stdscr, FALSE);

  use_color = TRUE;
  if (use_color = has_colors ())
    {
      start_color ();
      /* Hard-coded color values here.  Having these user-configurable would
	 mean yet another .*rc file, which I decided not to have.  If you
	 dislike my choices, then recompile.  Besides I'm too lazy to do any
	 rc stuff.  */
      init_pair (COL_SEA, COLOR_BLUE, COLOR_BLACK);
      init_pair (COL_ME, COLOR_YELLOW, COLOR_BLACK);
      init_pair (COL_REMOTE, COLOR_GREEN, COLOR_BLACK);
      init_pair (COL_OTHER, COLOR_WHITE, COLOR_BLACK);
#if 0
      /* These two are yet unimplemented.  */
      init_pair (COL_ENC, COLOR_RED, COLOR_BLACK);
      init_pair (COL_O_ENC, COLOR_MAGENTA, COLOR_BLACK);
#endif
      init_pair (COL_CAP, COLOR_CYAN, COLOR_BLUE);
      init_pair (COL_R_CAP, COLOR_GREEN, COLOR_BLUE);
      init_pair (COL_O_CAP, COLOR_WHITE, COLOR_BLUE);
      init_pair (COL_OCC, COLOR_CYAN, COLOR_MAGENTA);
      init_pair (COL_R_OCC, COLOR_GREEN, COLOR_RED);
      init_pair (COL_O_OCC, COLOR_WHITE, COLOR_RED);
    }
  map_pad = newpad (game_map.height, game_map.width*2);
  status_line = newwin (1, COLS, 0, 0);
  keypad (map_pad, TRUE);
  mvwaddchnstr (status_line, 0, (COLS - 8), turn_text, 4);
  mvwaddchnstr (status_line, 0, (COLS - 24), cross_text, 7);

  /* No need to worry about these having negative values.  */
  map_max_x = game_map.width - (COLS/2-1);
  map_max_y = game_map.height - (LINES-1);
  map_physical_right = COLS/2-1 < game_map.width ? COLS :
    (game_map.width+game_map.width);
  map_physical_bottom = LINES-1 < game_map.height ? LINES :
    (game_map.height+game_map.height);

  /* Bogus refresh.  I guess I'm mutilating curses somehow here...  */
  refresh ();

  player_mask = info->common.conn_mask;
  info->common.fd = STDIN_FILENO;

  do_refresh (ALL);
}

extern void
kill_curses ()
{
  static curses_killed = 0;
  if (!curses_killed)
    {
      curses_killed = 1;
      endwin ();
    }
}

static chtype
choose_color (loc)
     int loc;
{
  if (game_map.map[loc] == SEA_VAL)
    return COLOR_PAIR (COL_SEA);
  if ((game_map.map[loc] & MASK_PLAYER) == game_map.turn)
    {
      if (1<<(game_map.turn-1) & player_mask)
	{
	  if (game_map.map[loc] & MASK_CAPITAL)
	    return COLOR_PAIR (COL_CAP);
	  else if (game_map.map[loc] & MASK_OCCUPIED)
	    return COLOR_PAIR (COL_OCC);
	  else
	    return COLOR_PAIR (COL_ME);
	}
      else
	{
	  if (game_map.map[loc] & MASK_CAPITAL)
	    return COLOR_PAIR (COL_R_CAP);
	  else if (game_map.map[loc] & MASK_OCCUPIED)
	    return COLOR_PAIR (COL_R_OCC);
	  else
	    return COLOR_PAIR (COL_REMOTE);
	}
    }
  else
    {
      if (game_map.map[loc] & MASK_CAPITAL)
	return COLOR_PAIR (COL_O_CAP);
      else if (game_map.map[loc] & MASK_OCCUPIED)
	return COLOR_PAIR (COL_O_OCC);
      else
	return COLOR_PAIR (COL_OTHER);
    }
}

static chtype
first_char (loc)
     int loc;
{
  if (game_map.map[loc] & MASK_CAPITAL)
    {
      if (game_map.map[loc] & MASK_CROSS)
	return 'X';
      else
	return '!';
    }
  else if (game_map.map[loc] & MASK_CROSS)
    return 'x';
  return ' ' | A_BOLD;
}

static chtype
second_char (val)
     map_val val;
{
  int pl, occ;
  occ = val & MASK_OCCUPIED;
  pl = val & MASK_PLAYER;

  if (pl == SEA_VAL)
    return '.';
  return 'a'-1+pl - (occ ? 32 : 0);
}

extern void
c_draw_loc (i)
     const int i;
{
  chtype color = 0;

  if (use_color)
    color = choose_color (i);
  if (game_map.map[i] & MASK_OCCUPIED)
    color |= A_BOLD;
  waddch (map_pad, first_char (i) | color);
  waddch (map_pad, second_char (game_map.map[i]) | color);
}

/* TODO: maybe some optimization here?  */
static void
draw_map ()
{
  register int i;

  wmove (map_pad, 0, 0);
  for (i = 0 ; i < game_map.size; i++)
    c_draw_loc (i);
}

static void
draw_statusline ()
{
  char update;
  static char last_mode;
  update = need_update (&last_mode);

  if (update & MODE_ATT)
    {
      chtype att[] = {'a' | A_NORMAL, 't', 't', 'a', 'c', 'k', '\0'};
      chtype noact[] = {' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ',
			' ', '\0'};
      if (game_map.def_mode == MODE_ATT)
	attrset (A_STANDOUT);
      if (game_map.modes & MODE_ATT)
	{
	  mvwaddchnstr (status_line, 0, 0, att, 6);
	  attrset (A_NORMAL);
	  mvwaddch (status_line, 0, 8, ('6'-game_map.n_att) | A_BOLD);
	}
      else
	mvwaddchstr (status_line, 0, 0, noact);
    }

  if (update & MODE_DEF)
    {
      chtype def[] = {'d' | A_NORMAL, 'e', 'f', 'e', 'n', 'd', '\0'};
      chtype noact[] = {' ', ' ', ' ', ' ', ' ', ' ', '\0'};
      if (game_map.def_mode == MODE_DEF)
	attrset (A_STANDOUT);
      mvwaddchnstr (status_line, 0, 16, (game_map.modes & MODE_DEF) ?
		    def : noact, 6);
      attrset (A_NORMAL);
    }

  if (update & MODE_GUE)
    {
      chtype gue[] = {'g' | A_NORMAL, 'u', 'e', 'r', 'i', 'l', 'l',
		      'a', '\0'};
      chtype noact[] = {' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', '\0'};
      if (game_map.def_mode == MODE_GUE)
	attrset (A_STANDOUT);
      mvwaddchnstr (status_line, 0, 32, (game_map.modes & MODE_GUE) ?
		    gue : noact, 8);
      attrset (A_NORMAL);
    }

  mvwaddch (status_line, 0, COLS-16, ('0'+game_map.n_cross) | A_BOLD);
  if (update & UPDATE_TURN)
    mvwaddch (status_line, 0, COLS-1, (second_char (game_map.turn |
						    MASK_OCCUPIED) | A_BOLD));
}

/* TODO: a routine to scroll wrapped maps.  Low priority, to do when I
   feel like it.  */
static void
move_curs (x, y)
     int x, y;
{
  enum refresh_amount refresh = CURSOR;

  if (x)
    { 
      /* Suicidal programming: use unsigned, if 0-1, result is greater
	 than game_map.[width|height].  */
      unsigned int new_x = curs_x+x;
      if (new_x < game_map.width)
	{
	  int disp_x = new_x-map_x;
	  curs_x = new_x;
	  if (disp_x < 0)
	    {
	      map_x -= map_x >= 4 ? 4 : map_x;
	      refresh = ALL;
	    }
	  else if (disp_x >= COLS/2)
	    {
	      register int dist_edge = map_max_x-map_x;
	      map_x += dist_edge >= 4 ? 4 : dist_edge;
	      refresh = ALL;
	    }
	}
    }
  if (y)
    {
      unsigned int new_y = curs_y+y;
      if (new_y < game_map.height)
	{
	  int disp_y = new_y-map_y;
	  curs_y = new_y;
	  if (disp_y < 0)
	    {
	      map_y -= map_y >= 4 ? 4 : map_y;
	      refresh = ALL;
	    }
	  else if (disp_y >= LINES-1)
	    {
	      register int dist_edge = map_max_y-map_y;
	      map_y += abs (dist_edge) >= 4 ? 4 : dist_edge;
	      refresh = ALL;
	    }
	}
    }
  do_refresh (refresh);
}

extern void
c_process_action (act)
     Action *act;
{
  if (act->type == 0)
    return;
  if (!(act->type & (EVENT_ENCIRCLEMENT|EVENT_NEWTURN)))
    {
      if (act->count == 0)
	{
	  do_refresh (STATUSLINE);
	  return;
	}
      while (act->count-- > 0)
	{
	  wmove (map_pad, act->loc[act->count].y,
		 act->loc[act->count].x+act->loc[act->count].x);
	  c_draw_loc (parse_real_coord (act->loc[act->count]));
	}
      do_refresh (REFRESH_MAP);
      return;
    }
  do_refresh (ALL);
}

/* TODO: User-defineable keys.  In the meantime just edit the source and
   compile.  */
extern void
c_read_key (info)
     union conn_info *info;
{
  int key;

  while ((key = wgetch (map_pad)) == ERR);
  if (key == 12)	/* C-l pressed.  */
    do_refresh (REFRESH_MAP);
  if (ask_quit)
    {
      if (key == 'y')
	{
	  pid_t pid;
	  connection_terminated (info);
#ifndef _POSIX_JOB_CONTROL
	  mvaddstr (1, 0, "Players still playing, please don't interrupt");
	  mvaddstr (2, 0, "(I thought everyone had POSIX job control by now...)");
	  refresh ();
#endif
	  /* If we are here, then some X guys are still around.  */
	  kill_curses ();
#ifdef _POSIX_JOB_CONTROL
	  pid = fork ();	/* Yuck, but I want to go to background.  */
	  /* Probably ok to die, things are bad if the fork fails.  */
	  if (pid == -1)
	    die ("failed to quit gracefully");
	  else if (pid > 0)	/* Parent dies.  */
	    _exit (0);
	  /* Child goes on.  */
	  setsid ();	/* Disown the console.  */
#endif
	}
      else
	{
	  ask_quit = 0;
	  do_refresh (REFRESH_MAP);
	}
      return;
    }
  else if (key == 'q')
    {
      ask_quit = 1;
      do_refresh (CURSOR);
      return;
    }

  if (!(player_mask & 1<<(game_map.turn-1)))
    return;
  switch (key)
    {
      Action act;
      Coord cross_loc;
    case KEY_A1:
      move_curs (-1, -1);
      break;
    case 'j':
    case KEY_UP:
      move_curs (0, -1);
      break;
    case KEY_A3:
      move_curs (1, -1);
      break;
    case 'l':
    case KEY_RIGHT:
      move_curs (1, 0);
      break;
    case KEY_C3:
      move_curs (1, 1);
      break;
    case 'k':
    case KEY_DOWN:
      move_curs (0, 1);
      break;
    case KEY_C1:
      move_curs (-1, 1);
      break;
    case 'h':
    case KEY_LEFT:
      move_curs (-1, 0);
      break;
    case KEY_B2:
    case ' ':
      cross_loc.x = curs_x % game_map.width;
      cross_loc.y = curs_y % game_map.height;
      if (toggle_cross (cross_loc))
	{
	  Coord dim = {0, 0};
	  wmove (map_pad, curs_y, curs_x+curs_x);
	  signal_mapchange (cross_loc, cross_loc);
	  signal_turnwinchange ();
	}
      break;
    case 'z':
    case KEY_F(1):
      action (MODE_ATT, &act);
      signal_action (&act);
      break;
    case 'x':
    case KEY_F(2):
      action (MODE_DEF, &act);
      signal_action (&act);
      break;
    case 'c':
    case KEY_F(3):
      action (MODE_GUE, &act);
      signal_action (&act);
      break;
    }
}

extern void
do_refresh (refresh)
     const enum refresh_amount refresh;
{
  /* Fall-throughs intentional, don't panic.  */
  switch (refresh)
    {
    case ALL:
      draw_map ();
    case REFRESH_MAP:
      pnoutrefresh (map_pad, map_y, map_x*2, 1, 0,
		    map_physical_bottom, map_physical_right);
    case STATUSLINE:
      draw_statusline ();
      wnoutrefresh (status_line);
    case CURSOR:
      if (ask_quit)
	mvaddstr (1, 0, "Really quit? (y/n)");
      move (curs_y-map_y+1, (curs_x-map_x)*2);
      refresh ();
      doupdate ();
    }
}
