#ifdef HAVE_CONFIG_H
#include "config.h"
#endif /* HAVE_CONFIG_H */

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <ncurses.h>
#include "conf.h"
#include "dir.h"
#include "err.h"
#include "mem.h"
#include "player.h"
#include "queue.h"
#include "tui.h"


/* Call curses initialization routines.
 */

static void tui_setup_display (struct TUI *tui)
{
  int status = 0;
  static struct TUI *my_tui;
  
  if (tui)
    my_tui = tui;
  
  initscr ();
  status |= halfdelay (3);
  status |= noecho ();
  status |= nonl ();
  status |= intrflush (stdscr, FALSE);
  keypad (stdscr, TRUE);
  curs_set (0);
  
  if (status == ERR) {
    endwin ();
    fprintf (stderr, "error: Could not setup wanted curses mode.\n");
    exit (ERROR);
  }
  
  /* LINES & COLS are not updated after resize Linux, why?
   */
  
  my_tui->lines = LINES;
  my_tui->cols = COLS;
  my_tui->redraw = 1;
}

/*
static void tui_exit_by_signal (struct PLAYER *p)
{
  struct PLAYER *my_p;
  
  if (p) {
    my_p = p;
    return;
  }
  my_p->kill = my_p->pid;
  player_kill (my_p);
  endwin ();
  exit (0);
}
*/

static void tui_signal_handler (int signal)
{
  if (signal == SIGWINCH) {
    endwin ();
    tui_setup_display (0);
  }
  /*
  if (signal == SIGINT) {
    tui_exit_by_signal (0);
  }
  */
}


/* Ask for Yes or No
 */

static int tui_yes_no ()
{
  int r = 2;
  
  do {
    switch (getch ()) {
    case 'y':
    case 'Y':
      r = 1;
      break;
      
    case 'n':
    case 'N':
      r = 0;
      break;
    }
  } while (r == 2);
  
  return r;
}


/* Build index with items visible in the current directory.
 */

static void tui_update_list (struct TUI *tui)
{
  int pos = 0, i, items;
  
  if (tui->view != -1) {
    items = dir_count_parent (tui->dinfo, tui->view);
    if (items)
      mem_resize ((void *)&tui->list, items * sizeof (int));
    
    for (i=0; i<items; i++) {
      pos = dir_match_parent (tui->dinfo, tui->view, pos);
      tui->list[i] = pos;
      pos++;
    }
  } else {
    items = tui->dinfo->trees;
    mem_resize ((void *)&tui->list, items * sizeof (int));
    pos = 0;
    for (i=0; i<items; i++) {
      pos = dir_match_null_level (tui->dinfo, pos);
      tui->list[i] = pos;
      pos++;
    }
  }
  tui->disp[DM_ITEMS].items = items;
}


/* Search for key in item list
 */

static int tui_search_list (struct TUI *tui, int key)
{
  int i;
  
  for (i=0; i<tui->disp[DM_ITEMS].items; i++) {
    if (tui->list[i] == key)
      return i;
  }
  return 0;
}


/* Print message on the status bar.
 */

static void tui_status_message (struct TUI *tui, char *message)
{
  move (0,2);
  clrtoeol ();
  addnstr (message, tui->cols - 2);
  refresh ();
}


/* Redraw status bar
 */

static void tui_redraw_status (struct TUI *tui)
{
  int strsize;
  char *str, buf[11];
  
  move (0, 0);
  clrtoeol ();
  if (tui->player->paused)
    addch ('P');
  move (0,2);
  
  switch (tui->dm) {
  case DM_ITEMS:
    addstr ("(items) ");
    if (tui->view != -1) {
      str = dir_build_path (tui->dinfo, tui->view);
      strsize = strlen (str);
      if (strsize <= tui->cols - 10)
	addnstr (str, strsize);
      else
	addch ('+');
      free (str);
    }
    break;
    
  case DM_QUEUE:
    addstr ("(queue) ");
    buf[9] = 0;
    snprintf (buf, 11, "%d", tui->queue->items);
    if (!buf[9])
      addstr (buf);
    else
      addch ('+');
  }
  refresh ();
}


/* Redraw list
 */

static void tui_redraw_list (struct TUI *tui)
{
  int line, pos, item, strsize;
  char *str;
  struct TUI_DISP *d = &tui->disp[tui->dm];
  
  switch (tui->dm) {
  case DM_ITEMS:
    pos = d->base;
    for (line=2; line<tui->lines; line++) {
      move (line, 0);
      clrtoeol ();
      
      /* Screen is sometimes messed up if refresh() below is left out.
       * ncurses bug?
       */
      
      refresh ();
      
      if (pos < d->items) {
	item = tui->list[pos];
	if (tui->dinfo->item[item].queued)
	  addch ('q');
	if (!tui->dinfo->item[item].type)
	  addch ('D');
	strsize = tui->dinfo->item[item].size;
	if (strsize > (tui->cols - 2))
	  strsize = tui->cols - 2;
	str = tui->dinfo->strings;
	str += tui->dinfo->item[item].offset;
	mvaddnstr (line, 2, str, strsize);
	pos++;
      }
    }
    break;
    
  case DM_QUEUE:
    pos = d->base;
    for (line=2; line<tui->lines; line++) {
      move (line, 0);
      clrtoeol ();
      
      /* Screen is sometimes messed up if refresh() below is left out.
       * ncurses bug?
       */
      
      refresh ();
      
      if (pos < d->items) {
	item = queue_get_item (tui->queue, pos);
	strsize = tui->dinfo->item[item].size;
	if (strsize > (tui->cols - 2))
	  strsize = tui->cols - 2;
	str = tui->dinfo->strings;
	str += tui->dinfo->item[item].offset;
	mvaddnstr (line, 2, str, strsize);
	pos++;
      }
    }
  }
  refresh ();
}


/* Update display variables to wanted cursor pos.
 */

static void tui_upd_y (struct TUI_DISP *td, struct TUI *tui,
		       int offset, int redraw)
{
  int lines = tui->lines - 2;
  
  td->cursor += offset;
  if (td->cursor < 0)
    td->cursor = 0;
  if (td->cursor >= td->items)
    td->cursor = td->items - 1;
  
  if (td->cursor < td->base) {
    td->base = td->cursor - (lines - 1);
    if (td->base < 0)
      td->base = 0;
    redraw = 1;
  }
  if (td->cursor >= (td->base + lines)) {
    td->base = td->cursor;
    if (td->base > (td->items - lines))
      td->base = td->items - lines;
    redraw = 1;
  }
  if (redraw)
    tui_redraw_list (tui);
}


/* Move cursor and update screen as needed
 * mode is one of CURSOR_UP, CURSOR_DOWN, CURSOR_PG_UP, CURSOR_PG_DOWN
 * or CURSOR_REDRAW
 */

static void tui_move_cursor (struct TUI *tui, int mode)
{
  int dot_mode = 0;
  struct TUI_DISP *d;
  
  d = &tui->disp[tui->dm];
  
  switch (mode) {
  case CURSOR_UP:
    if (d->cursor > 0) {
      mvaddch (d->cursor - d->base + 2, 1, ' ');
      tui_upd_y (d, tui, -1, 0);
      mvaddch (d->cursor - d->base + 2, 1, '*');
      refresh ();
    }
    break;
     
  case CURSOR_DOWN_ADD:
    dot_mode = 1;
    
  case CURSOR_DOWN_DELETE:
    if (!dot_mode)
      dot_mode = 2;
    
  case CURSOR_DOWN:
    if (dot_mode == 1)
      mvaddch (d->cursor - d->base + 2, 0, 'q');
    if (dot_mode == 2)
      mvaddch (d->cursor - d->base + 2, 0, ' ');      
    if (d->cursor < (d->items - 1)) {
      mvaddch (d->cursor - d->base + 2, 1, ' ');
      tui_upd_y (d, tui, 1, 0);
      mvaddch (d->cursor - d->base + 2, 1, '*');
      refresh ();
    }
    break; 
    
  case CURSOR_PG_UP:
    if (d->cursor > 0) {
      mvaddch (d->cursor - d->base + 2, 1, ' ');
      tui_upd_y (d, tui, -(tui->lines - 2), 0);
      mvaddch (d->cursor - d->base + 2, 1, '*');
      refresh ();
    }
    break;
    
  case CURSOR_PG_DOWN:
    if (d->cursor < (d->items - 1)) {
      mvaddch (d->cursor - d->base + 2, 1, ' ');
      tui_upd_y (d, tui, tui->lines - 2, 0);
      mvaddch (d->cursor - d->base + 2, 1, '*');
      refresh ();
    }
    break;
    
  case CURSOR_REDRAW:
    if (d->items) {
      if (d->cursor >= d->items)
	d->cursor = d->items - 1;
      tui_upd_y (d, tui, 0, 1);
      mvaddch (d->cursor - d->base + 2, 1, '*');
      refresh ();
    } else {
      tui_redraw_list (tui);
    }
  }
}


/* Prints help message and waits for key press.
 */

static void tui_help_screen (struct TUI *tui)
{
  static char help[] = {"  (help)\n"
			"\n"
			"I a  add tune\n"
			"  b  page up\n"
			"  c  clear queue\n"
			"  d  delete tune\n"
			"  h  help message\n"
			"  i  up\n"
			"I j  left\n"
			"  k  down\n"
			"I l  right\n"
			"  m  move to top\n"
			"  p  pause\n"
			"  q  quit\n"
			"  r  randomize\n"
			"  s  skip tune\n"
			"  t  toggle display\n"
			"  sp page down\n"
			"\n"
			"  <press any key>"};
  
  erase ();
  mvaddstr (0, 0, help);
  refresh ();
  while (getch () == -1) {}
  
  tui_redraw_status (tui);
  tui_move_cursor (tui, CURSOR_REDRAW);
}


/* Enter directory at cursor position and redraw screen
 */

static void tui_enter_dir (struct TUI *tui)
{
  int item;
  struct DIR_ITEM *item_p;
  
  if ((tui->dm == DM_ITEMS) && tui->disp[DM_ITEMS].items) {    
    item = tui->list[tui->disp[DM_ITEMS].cursor];
    item_p = &tui->dinfo->item[item];
    if (!item_p->type) {
      tui->view = item;
      tui_update_list (tui);
      tui->disp[DM_ITEMS].cursor = 0;
      tui_redraw_status (tui);
      tui_move_cursor (tui, CURSOR_REDRAW);
    }
  }
}


/* Leave directory and redraw screen
 */

static void tui_leave_dir (struct TUI *tui)
{
  int old, cursor;
  struct DIR_ITEM *item_p = &tui->dinfo->item[tui->view];
  
  if ((tui->dm == DM_ITEMS) && (tui->view != -1)) {
    old = tui->view;
    if (!item_p->level)
      tui->view = -1;
    else
      tui->view = tui->dinfo->item[tui->view].parent;
    
    tui_update_list (tui);
    cursor = tui_search_list (tui, old);
    
    tui->disp[DM_ITEMS].cursor = cursor;
    tui_redraw_status (tui);
    tui_move_cursor (tui, CURSOR_REDRAW);
  }
}


/* Add item at cursor position to queue.
 * Add dot to display as well.
 */

static void tui_add_to_queue (struct TUI *tui)
{
  int item;
  
  if (tui->dm == DM_ITEMS) {
    item = tui->list[tui->disp[DM_ITEMS].cursor];
    if (tui->dinfo->item[item].type) {
      if (!tui->dinfo->item[item].queued) {
	queue_append (tui->queue, tui->dinfo, item);  
	tui->disp[DM_QUEUE].items = tui->queue->items;
	tui_move_cursor (tui, CURSOR_DOWN_ADD);
      } else {
	tui_move_cursor (tui, CURSOR_DOWN);
      }
    } else {
      queue_append_dir (tui->queue, tui->dinfo, item);
      tui->disp[DM_QUEUE].items = tui->queue->items;
      tui_move_cursor (tui, CURSOR_DOWN);
    }
  }
}


/* Delete item at cursor position to queue.
 * Delete dot from display as well.
 */

static void tui_delete_from_queue (struct TUI *tui)
{
  int item, pos, playing_item;
  struct QUEUE *q = tui->queue;
  
  if (!tui->queue->items)
    return;
  
  switch (tui->dm) {
  case DM_ITEMS:
    item = tui->list[tui->disp[DM_ITEMS].cursor];
    if (tui->dinfo->item[item].queued) {
      pos = queue_search_for_item (tui->queue, item);
      if (pos != -1) {
	if (!pos) {
	  tui->player->cmd = PLAYER_SKIP;
	  tui->disp[DM_ITEMS].cursor++;
	} else {
	  queue_delete (tui->queue, tui->dinfo, pos);
	  tui->disp[DM_QUEUE].items = tui->queue->items;
	  tui_move_cursor (tui, CURSOR_DOWN_DELETE);
	}
      }
    } else {
      if (!tui->dinfo->item[item].type && q->items) {
	playing_item = q->base[q->pos];
	queue_delete_dir (tui->queue, tui->dinfo, item);
	tui->disp[DM_QUEUE].items = tui->queue->items;
	if (!q->items) {
	  tui->player->cmd = PLAYER_RELOAD;
	} else {
	  if (playing_item != q->base[q->pos])
	    tui->player->cmd = PLAYER_RELOAD;
	}
      } 
      tui_move_cursor (tui, CURSOR_DOWN);
    }
    break;
    
  case DM_QUEUE:
    if (!tui->disp[DM_QUEUE].cursor) {
      tui->player->cmd = PLAYER_SKIP;
    } else {
      queue_delete (tui->queue, tui->dinfo, tui->disp[DM_QUEUE].cursor);
      tui->disp[DM_QUEUE].items = tui->queue->items;
      tui_redraw_status (tui);
      tui_move_cursor (tui, CURSOR_REDRAW);
    }
  }
}


/* Clear queue
 */

static void tui_clear_queue (struct TUI *tui)
{
  queue_clear (tui->queue, tui->dinfo);
  tui->disp[DM_QUEUE].items = tui->queue->items;
  tui->player->cmd = PLAYER_RELOAD;
  if (tui->dm == DM_QUEUE)
    tui_redraw_status (tui);
  tui_move_cursor (tui, CURSOR_REDRAW);
}


/* Move item at cursor position to position 0 in queue
 */
/*
static void tui_move_item (struct TUI *tui)
{
  int pos_x, item_x, item0;
  
  pos_x = tui->disp[tui->dm].cursor;
  item0 = tui->queue->base[tui->queue->pos];
  
  if (tui->dm == DM_ITEMS) {
    item_x = tui->list[pos_x];
    if (item_x == item0)
      return;
    if (!tui->dinfo->item[item_x].queued)
      return;
    pos_x = queue_search_for_item (tui->queue, item_x);
  } else {
    if (pos_x)
      item_x = queue_get_item (tui->queue, pos_x);
    else
      return;
  }
  
  queue_delete (tui->queue, tui->dinfo, pos_x);
  
  tui->dinfo->item[item0].queued = 0;
  tui->dinfo->item[item_x].queued = 1;
  tui->queue->base[tui->queue->pos] = item_x;
  
  tui->player->cmd = PLAYER_RELOAD;
  tui->disp[DM_QUEUE].items = tui->queue->items;
  if (tui->dm == DM_QUEUE)
    tui_redraw_status (tui);
  tui_move_cursor (tui, CURSOR_REDRAW);
}
*/


static void tui_move_item (struct TUI *tui)
{
  int del_item, del_pos, cursor;
  
  cursor = tui->disp[tui->dm].cursor;
  
  if (tui->dm == DM_ITEMS) {
    del_item = tui->list[cursor];
    if (del_item == tui->queue->base[0])
      return;
    del_pos = queue_search_for_item (tui->queue, del_item);
    if (del_pos == -1)
      return;
  } else {
    if (!cursor)
      return;
    del_pos = cursor;
    del_item = queue_get_item (tui->queue, del_pos);
  }
  
  queue_delete (tui->queue, tui->dinfo, del_pos);
  queue_prepend (tui->queue, tui->dinfo, del_item);
  
  tui->player->cmd = PLAYER_RELOAD;
  tui->disp[DM_QUEUE].items = tui->queue->items;
  tui_move_cursor (tui, CURSOR_REDRAW);
}


/* Randomize items in queue
 */

static void tui_randomize_queue (struct TUI *tui)
{
  queue_shuffle (tui->queue, tui->dinfo);
  tui->player->cmd = PLAYER_RELOAD;
  if (tui->dm == DM_QUEUE) {
    tui_redraw_status (tui);
    tui_move_cursor (tui, CURSOR_REDRAW);
  }
}


/* Main player routine.
 * Note: case 1 ... 255. Player returned an error code, probably because
 * an audio device was busy. Restart.
 */

static void tui_player_control (struct TUI *tui)
{
  int item;
  struct PLAYER *p = tui->player;
  
  switch (p->cmd) {
  case PLAYER_PAUSE:
    if (p->paused) {
      p->paused = 0;
      player_cont (p);
    } else {
      p->paused = 1;
      player_stop (p);
    }
    tui_redraw_status (tui);
    break;
    
  case PLAYER_RELOAD:
    if (p->pid) {
      p->kill = p->pid;
      p->pid = 0;
    }
    p->cmd = PLAYER_NO_CMD;
    return;
    
  case PLAYER_SKIP:
    if (tui->queue->items) {
      if (p->pid) {
	p->kill = p->pid;
	p->pid = 0;
      }
      queue_skip (tui->queue, tui->dinfo);
      tui->disp[DM_QUEUE].items = tui->queue->items;
      if (tui->dm == DM_QUEUE)
	tui_redraw_status (tui);
      tui_move_cursor (tui, CURSOR_REDRAW);
      p->cmd = PLAYER_NO_CMD;
      return;
    }
  }
  p->cmd = PLAYER_NO_CMD;
  
  if (p->kill)
    player_kill (p);
  
  if (!p->paused) {
    switch (player_status (p)) {
    case 0:
      if (tui->queue->items && p->pid) {
	queue_skip (tui->queue, tui->dinfo);
	tui->disp[DM_QUEUE].items = tui->queue->items;
	if (tui->dm == DM_QUEUE)
	  tui_redraw_status (tui);
	tui_move_cursor (tui, CURSOR_REDRAW);
	if (!tui->queue->items)
	  p->pid = 0;
      }
    case 1 ... 255:
      if (tui->queue->items) {
	item = queue_get_item (tui->queue, 0);
	p->pid = player_play (tui->cfg, tui->dinfo, item);
      }
    }
  }
}


static void tui_event_loop (struct TUI *tui)
{
  int exit = 0, key = -1, resize;
  
  tui_update_list (tui);
  
  do {
    do {
      if (tui->lines < TUI_MIN_LINES || tui->cols < TUI_MIN_COLS) {
	resize = 1;
      } else {
	resize = 0;
	if (tui->redraw) {
	  tui_redraw_status (tui);
	  tui_move_cursor (tui, CURSOR_REDRAW);
	  tui->redraw = 0;
	}
      }
      tui_player_control (tui);
      key = getch ();
    } while (key == -1 || resize);
   
    switch (key) {
    case KEY_NPAGE:
    case ' ':
      tui_move_cursor (tui, CURSOR_PG_DOWN);
      break;
      
    case 'a':
    case 'A':
      tui_add_to_queue (tui);
      break;
      
    case KEY_PPAGE:
    case 'b':
    case 'B':
      tui_move_cursor (tui, CURSOR_PG_UP);
      break;
      
    case 'c':
    case 'C':
      tui_clear_queue (tui);
      break;
      
    case 'd':
    case 'D':
      tui_delete_from_queue (tui);
      break;
      
    case 'h':
    case 'H':
      tui_help_screen (tui);
      break;
      
    case KEY_UP:
    case 'i':
    case 'I':
      tui_move_cursor (tui, CURSOR_UP);
      break;
      
    case KEY_LEFT:
    case 'j':
    case 'J':
      tui_leave_dir (tui);
      break;
      
    case KEY_DOWN:
    case 'k':
    case 'K':
      tui_move_cursor (tui, CURSOR_DOWN);
      break;
      
    case KEY_RIGHT:
    case 'l':
    case 'L':
      tui_enter_dir (tui);
      break;
      
    case 'm':
    case 'M':
      tui_move_item (tui);
      break;
      
    case 'p':
    case 'P':
      tui->player->cmd = PLAYER_PAUSE;
      break;
      
    case 'q':
    case 'Q':
      tui_status_message (tui, "Quit? (y/n)");
      exit = tui_yes_no ();
      if (!exit) {
	tui_redraw_status (tui);
      } else {
	tui->player->kill = tui->player->pid;
	player_kill (tui->player);
      }
      break;
      
    case 'r':
    case 'R':
      tui_randomize_queue (tui);
      break;
      
    case 's':
    case 'S':
      tui->player->cmd = PLAYER_SKIP;
      break;
      
    case 't':
    case 'T':
      tui->dm ^= 1;
      tui_redraw_status (tui);
      tui_move_cursor (tui, CURSOR_REDRAW);
    }
  } while (!exit);
}


void tui_start (struct TUI *tui)
{
  struct sigaction sa, old_sa;
  struct PLAYER player = {0};
  
  tui_setup_display (tui);
  tui->view = -1;
  tui->player = &player;
  /* tui_exit_by_signal (tui->player); */
  
  sa.sa_handler = tui_signal_handler;
  if (sigemptyset (&sa.sa_mask) == -1) {
    endwin ();
    err_exit (ERROR);
  }
  sa.sa_flags = SA_RESTART;
  if (sigaction (SIGWINCH, &sa, &old_sa) == -1) {
    endwin ();
    err_exit (ERROR);
  }
  /*
  if (sigaction (SIGINT, &sa, &old_sa) == -1) {
    endwin ();
    err_exit (ERROR);
  }
  */
  
  tui_event_loop (tui);
  endwin ();
}
