/*
 *  controller.c - Main loop and interactive interface.
 *
 *  (C) 1994 Mikael Nordqvist (d91mn@efd.lth.se, mech@df.lth.se)
 */

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/time.h>
#include <sys/wait.h>
#include <errno.h>
#include <time.h>
#include <sys/soundcard.h>
#include <dirent.h>

#include "mod.h"
#include "message.h"
#include "ncurses.h"

/* External variables */

SEQ_DECLAREBUF();
extern int seqfd, gus_dev;
extern struct mod_info M;
extern struct options default_opt, opt;
extern char workdir[PATH_MAX+1];

extern pid_t pid_main, pid_player;
extern int total_time;     /* Total time spent playing modules */

extern int nr_songs, nr_files;
extern int swin_LINES;     /* For page-DN/UP */
extern int nr_dirs;

/* Variables */

int ack_pipe[2];           /* Player acks main */
int player2main_synced[2]; /* Message from player to main (synced) */
int main2player[2];        /* Message from main to player */

int nr_visible_voices;     /* Maximum number of visible voices */
char loaded;               /* True when a module has been loaded */
char quit;                 /* Set when program is aborting     */

/* Local variables */

static int maxfd;
static int first_voice;   /* First voice to be shown on screen */

static int first_sample;  /* First sample shown on screen    */
static char playing;      /* Are playing or idling           */
static char selecting;    /* Are we selecting a new song?    */

static int starting_time; /* When module was started */
static int restart_time;  /* When player had to be restarted last */

static int cur_filenr;    /* Current file active (real order) */
static int saved_filenr;  /* Saved file# in case we abort file-select */
static char saved_dir[PATH_MAX+1]; /* Ditto with workdir */

static char voice_toggle; /* Voice-toggling (2 keys) */

/* Variables for commands requiring player to back up */

static int pending_restart, pending_info;
static int last_speed, last_tempo, last_pos, last_line;

/* Constants */

#define STATUS_READY    1
#define STATUS_PLAYING  2


void main_loop(void)
{
    fd_set rset;
    int nr;
    
    maxfd=getdtablesize();
    draw_screen();
    set_voice_detail(1); /* Start with highest detail */

    set_status(STATUS_READY);
    selecting=quit=0;
    cur_filenr=first_voice=0;

    if(opt.play_list) {
	if(opt.auto_next) {
	    cur_filenr=seqidx_to_fileidx(0);
	    load_next_module();
	}
	else {
	    start_selecting();
	    print_selectingbar();
	    print_files(cur_filenr);
	}
    }
    else {
	strcpy(saved_dir, workdir);
	init_dir(workdir);
	start_selecting();
	print_selectingbar();
	print_files(cur_filenr);
    }

    while(!quit) {
	FD_ZERO(&rset);
	FD_SET(STDIN_FILENO, &rset);
	if(loaded)
	    FD_SET(seqfd, &rset);
	if((nr=select(maxfd, &rset, 0, 0, 0)) >= 0) {
	    if(FD_ISSET(STDIN_FILENO, &rset))
		read_key();
	    else if(FD_ISSET(seqfd, &rset))
		read_sequencer();
	    if(!nr)
		warning("nr == 0 ?!?!?\n");
	}
	else {
	    if(errno != EINTR)
		error("select() failed (errno=%d).\n", errno);
	}
    }
}


void load_next_module(void)
{
    struct mod_message mess;
    char buf[80];
    int nr, tmp;

    voice_toggle=-1; /* Forget previous keypress */

    /* Release resources used by the previous module */
    if(loaded) {
	mess.type=MESSAGE_EXIT;
	send_to_player(&mess);
	
	drain_pipes(0);
	do {
	    if((nr=waitpid(pid_player, 0, 0)) == -1 && errno != EINTR)
		error("waitpid() failed in main.\n");
	}
	while(nr != pid_player);
	drain_pipes(1);
	
	free_module();
	loaded=0;
    }

    first_voice=0;
    print_channelnumbers(first_voice);
    clear_screen();
    clear_all_info();

    info("\n\n");

    /* Make sure sequencer is clean */
    cleanup_sound();
    init_sound();

    while(!quit) {
	get_module(fileidx_to_seqidx(cur_filenr));	
	print_filename(fileidx_to_seqidx(cur_filenr));
	tmp=load_module();
	chdir(workdir); /* Restore current directory */
	if(!tmp) {
	    if(quit != QUIT_SIGNAL) {
		print_status("Loading failed");
		sleep(1);
		free_module();
		loaded=0;
		if(should_autoselect_next()) {
		    select_next_file();
		    continue;
		}
		else {
		    clear_all_info();
		    set_status(STATUS_READY);
		    start_selecting();
		    print_selectingbar();
		    print_files(cur_filenr);
		    return;
		}
	    }
	    else {
		return; /* Leave if we got a Ctrl-C */
	    }
	}
	else
	    break; /* Found module, leave load-loop */
    }	

    if(quit) {   /* Is this one still needed? */
	return;
    }

    switch(pid_player=fork()) {
      case -1:
	error("Fork failed.\n");
	break;
	
      case 0:  /* Child */
	play_module();
	cleanup_sound();
	exit(0);
	break;
	
      default: /* Parent */
    }
    
    starting_time=restart_time=time(0);
    
    pending_restart=0;
    first_sample=1;
    loaded=1;

    last_speed=6; /* Defaults if a key is hit b4 1st line is played */
    last_tempo=125;

    set_voice_detail(opt.voice_detail);
    
    clear_screen();
    print_samples(1);
    set_status(STATUS_PLAYING);
    print_channelnumbers(first_voice);
    
    read(STDIN_FILENO, buf, 80); /* Flush input */
}


char *basename(char *s)
{
    int i;
    for(i=strlen(s)-1; i >=0 && s[i] != '/'; --i)
	;
    return &s[++i];
}


void read_key(void)
{
    struct mod_message mess;
    int ch, tmp;
    char tmpname[NAME_MAX+1];
    char c;
    char dotdot=0, done;

    if(opt.verbose || opt.quiet) {
	read(STDIN_FILENO, &c, 1);
	ch=c;
	if(opt.quiet)
	    return;
    }
    else
	ch=getch();

    if(!(ch >= '0' && ch <= '9'))
	voice_toggle=-1;
    
    /* Keys always available */
    done=1;
    switch(ch) {
      case KEY_F(1):
      case 'h':
	print_minihelp();
	break;
      case 'q':
	mess.type=MESSAGE_EXIT;
	send_to_player(&mess);
	quit=QUIT_QUIT;
	if(playing)
	    total_time+=time(0)-restart_time;
	break;
      case '+':
	change_mastervolume(5);
	break;
      case '-':
	change_mastervolume(-5);
	break;
      case 'm':	
	change_mastervolume(0);
	break;
#if 0
      case '*':
	debug("Main sending NOP.\n");
	mess.type=MESSAGE_NOP;
	write(main2player[PIPE_WRITE], &mess, sizeof(mess));
	break;
#endif
      default:
	done=0;
    }

    if(done)      /* Ugly but saves time */
	goto skip;

    if(selecting) {
	switch(ch) {
	  case KEY_UP:
	  case 'p':
	    cur_filenr=MAX(cur_filenr-1, 0);
	    print_files(cur_filenr);
	    break;
	  case KEY_DOWN:
	  case 'n':
	    cur_filenr=MIN(cur_filenr+1, nr_files-1);
	    print_files(cur_filenr);
	    break;
	  case KEY_PPAGE:
	  case 'P':
	    cur_filenr=MAX(cur_filenr-swin_LINES/2, 0);
	    print_files(cur_filenr);
	    break;
	  case KEY_NPAGE:
	  case 'N':
	    cur_filenr=MIN(cur_filenr+swin_LINES/2, nr_files-1);
	    print_files(cur_filenr);
	    break;
	  case KEY_HOME:
	    cur_filenr=0;
	    print_files(cur_filenr);
	    break;
	  case KEY_END:
	    cur_filenr=nr_files-1;
	    print_files(cur_filenr);
	    break;
	  case '\n':
	    if(!is_dir(cur_filenr)) { /* is_dir() == 0 when opt.play_list */
		do_select_file();
		if(playing)
		    total_time+=time(0)-restart_time;
		load_next_module();
	    }
	    else {
		if(!strcmp("..", get_dirnamestring(cur_filenr)))
		    dotdot=1;
		chdir(get_dirnamestring(cur_filenr));
	      yucky_goto:
		strcpy(tmpname, basename(workdir));
		getcwd(workdir, PATH_MAX); /* Change working directory */
		init_dir(workdir);
		cur_filenr=(dotdot ? name_to_filenr(tmpname) : 0);
		print_selectingbar();
		print_files(cur_filenr);
	    }
	    break;
	  case 'u':
	    if(opt.play_list || !strcmp("/", workdir))
		break;
	    chdir("..");
	    dotdot=1;
	    goto yucky_goto;
	  case 'l':
	    if(!loaded)
		return;

	    abort_selecting();
	    print_channelnumbers(first_voice);
	    if(playing && !default_opt.noscroll)
		print_line(last_pos, last_line, first_voice);
	    else
		clear_screen();
	    break;
	  default: /* All other keys ignored */
	}
    }
    else {
	/* The following keys are available only when we have a loaded song */
	if(loaded) {
	    switch(ch) {
	      case KEY_RIGHT:
		first_voice=
		    MAX(0, MIN(first_voice+1, M.nr_voices-nr_visible_voices));
		print_channelnumbers(first_voice);
		break;
	      case KEY_LEFT:
		first_voice=MAX(first_voice-1, 0);
		print_channelnumbers(first_voice);
		break;
	      case 'f':
		if(playing) {
		    pending_restart=MESSAGE_JUMPPATTERN;
		    pending_info=1;
		    if(!selecting)
			clear_screen();
		}
		else {
		    last_pos=MIN(last_pos+1, M.songlength-1);
		    print_pos(last_pos, 0);
		}
		break;
	      case 'b':
		if(playing) {
		    pending_info=-1;
		    pending_restart=MESSAGE_JUMPPATTERN;
		    if(!selecting)
			clear_screen();
		}
		else {
		    last_pos=MAX(last_pos-1, 0);
		    print_pos(last_pos, 0);
		}
		break;
	      case 'r':
		set_status(STATUS_PLAYING);
		pending_restart=MESSAGE_RESTART;
		handle_restart(42); /* Done here as no lines coming */
		clear_screen();
		break;
	      case KEY_UP:
	      case 'a':
		first_sample=MAX(first_sample-1, 1);
		print_samples(first_sample);
		break;
	      case KEY_DOWN:
	      case 'z':
		first_sample=MAX(1, MIN(first_sample+1, M.nr_samples-7));
		print_samples(first_sample);
		break;
	      case KEY_NPAGE:
		first_sample=MAX(1, MIN(first_sample+8, M.nr_samples-7));
		print_samples(first_sample);
		break;
	      case KEY_PPAGE:
		first_sample=MAX(first_sample-8, 1);
		print_samples(first_sample);
		break;
	      case KEY_HOME:
		first_sample=1;
		print_samples(first_sample);
		break;
	      case KEY_END:
		first_sample=MAX(1, M.nr_samples-7);
		print_samples(first_sample);
		break;
	      case '\n':
		if(playing) {
		    set_status(STATUS_READY);
		    pending_restart=MESSAGE_STOP;
		    total_time+=time(0)-restart_time; /* Must be done here
						       * as status=ready.
						       */
		    clear_screen();
		}
		else {
		    starting_time=time(0);
		    pending_restart=MESSAGE_JUMPPATTERN;
		    pending_info=0;
		    handle_restart(last_pos);   /* Done here as no lines are
					         * sent from player.
						 */
		    set_status(STATUS_PLAYING); /* _after_ restart */
		}
		break;
	      case '0': case '1': case '2': case '3': case '4':
	      case '5': case '6': case '7': case '8': case '9':
		if(M.nr_voices <= 9) { /* Single-key mode */
		    tmp=ch-'0';
		    voice_toggle=-1;
		    if(tmp == 0 || tmp > M.nr_voices)
			break;
		}
		else { /* Double-key mode */
		    if(voice_toggle == -1) { /* First key */
			voice_toggle=ch-'0';
			if(voice_toggle*10 > M.nr_voices)
			    voice_toggle=-1;
			break;
		    }
		    else { /* Second key */
			tmp=voice_toggle*10+ch-'0';
			voice_toggle=-1;
			if(!tmp || tmp > M.nr_voices)
			    break;
		    }
		}
		tmp--;
		tmp=1<<tmp;
		opt.active_voices=(opt.active_voices&(~tmp))|
		    ((~(opt.active_voices&tmp))&tmp);
		
		if(playing) {
		    pending_info=0;
		    pending_restart=MESSAGE_JUMPPATTERN;
		    if(!selecting)
			clear_screen();
		}
		print_channelnumbers(first_voice);
		break;
	      default:
	    }
        }
	
	/* Keys available anytime we are not selecting a new file */
	switch(ch) {
	  case ' ':
	  case 'n':
	    select_next_file();
	    if(playing)
		total_time+=time(0)-restart_time;
	    load_next_module();
	    break;
	  case 'p':
	    select_prev_file();
	    if(playing)
		total_time+=time(0)-restart_time;
	    load_next_module();
	    break;
	  case 'l':
	    start_selecting();
	    print_selectingbar();
	    print_files(cur_filenr);
	    break;

	  case 's':
	    if(default_opt.noscroll) {
		default_opt.noscroll=0;
		if(playing)
		    print_line(last_pos, last_line, first_voice);
	    }
	    else {
		clear_screen();
		default_opt.noscroll=1;
	    }
	    break;
	  case 'v':
	    if(nr_visible_voices == 4)
		nr_visible_voices=9;
	    else if(nr_visible_voices == 9)
		nr_visible_voices=17;
	    else
		nr_visible_voices=4;

	    first_voice=
		MAX(0, MIN(first_voice, M.nr_voices-nr_visible_voices));
	    
	    print_channelnumbers(first_voice);
	    if(playing && !default_opt.noscroll)
		print_line(last_pos, last_line, first_voice);
	    break;
	  default:
	}
    }
  skip:
#ifdef FLUSH_INPUT
    read(STDIN_FILENO, tmpname, 80); /* Flush input (for slow CPU's) */
#endif
}


void start_selecting(void)
{
    saved_filenr=cur_filenr;
    if(!opt.play_list)
	getcwd(saved_dir, PATH_MAX); /* Store working directory */
    selecting=1;
}

/* Do what is necessery when selecting a file (except perform the actual
 * loading).
 */

void do_select_file(void)
{
    if(!opt.play_list)
	strcpy(saved_dir, workdir); /* Set the new working directory */
    selecting=0;
}


void abort_selecting(void)
{
    if(!opt.play_list) {
	if(strcmp(saved_dir, workdir)) {
	    strcpy(workdir, saved_dir);
	    chdir(workdir);
	    init_dir(workdir);
	}
    }
    cur_filenr=saved_filenr;
    selecting=0;
}


int should_autoselect_next(void)
{
    if(!opt.auto_next)
	return 0;

    /* No forever when there is no playlist */
    if(opt.play_list && opt.replay_forever)
	return 1;
    
    if(opt.play_list) {
	if(fileidx_to_seqidx(cur_filenr)==nr_songs-1) {
	    return 0;
	}
    }
    else {
	if(cur_filenr == nr_files-1) {
	    return 0;
	}
    }
    return 1;
}


void select_next_file(void)
{
    int tmp;
    
    if(opt.play_list) {
	if((tmp=fileidx_to_seqidx(cur_filenr)) == nr_songs-1) {
	    if(opt.replay_forever) {
		cur_filenr=seqidx_to_fileidx(0); /* Take the first one again */
	    }
	    else
		return; /* Replay last song */
	}
	else
	    cur_filenr=seqidx_to_fileidx(tmp+1);
    }
    else {
	cur_filenr=MIN(cur_filenr+1, nr_files-1);
    }
}


void select_prev_file(void)
{
    int tmp;

    if(opt.play_list) {
	if(!(tmp=fileidx_to_seqidx(cur_filenr)))
	    return; /* Replay first song */
	cur_filenr=seqidx_to_fileidx(tmp-1);
    }
    else {
	cur_filenr=MAX(cur_filenr-1, nr_dirs);
    }
}


void read_sequencer(void)
{
    char buf[4];
    int msg;
    struct mod_message mess;
    
    safe_read(seqfd, (struct mod_message *)buf, 4);
    
    msg=(*(unsigned long *)buf)>>8;
    switch(msg) {
      case MESSAGE_PRINTLINE:
      case MESSAGE_PRINTSPEED:
	read_syncedmessage();
	break;
      case MESSAGE_DONE:       /* Song played to the end */
	total_time+=time(0)-restart_time;

	if(opt.loop_module) {
	    pending_restart=MESSAGE_RESTART;
	    handle_restart(42); /* Done here as no lines coming */
	}
	else {
	    set_status(STATUS_READY);
	    last_pos=last_line=0;
	    print_pos(0, 0);
	    if(!selecting) {
		if(should_autoselect_next()) {
		    select_next_file();
		    load_next_module();
		}
		else {
		    clear_screen();
		    /* Exit if we are in batch-mode */
		    if(opt.quiet) {
			mess.type=MESSAGE_EXIT;
			send_to_player(&mess);
			quit=QUIT_QUIT;
		    }
		}
	    }
	}
	break;
      default:
	error("Invalid message %d\n", msg);
    }
}


void read_syncedmessage(void)
{
    struct mod_message mess;
    unsigned int t;
    
    safe_read(player2main_synced[PIPE_READ], &mess, sizeof(mess));
    switch(mess.type) {
      case MESSAGE_PRINTSPEED:
	last_speed=mess.a1.speed;
	last_tempo=mess.a2.tempo;
	print_speed(last_speed, last_tempo);
	break;
      case MESSAGE_PRINTLINE:
	if(pending_restart)
	    handle_restart(mess.a1.songpos);
	else {
	    last_pos=mess.a1.songpos;
	    last_line=mess.a2.line;
	    if(!pending_syncedmessage()) {
		if(!selecting && !default_opt.noscroll)
		    print_line(last_pos, last_line, first_voice);
	    }
	    print_pos(last_pos, last_line);
	    t=time(0)-starting_time;
	    if(opt.maxtime && t > opt.maxtime) {
		total_time+=time(0)-restart_time-1;
		set_status(STATUS_READY);
		last_pos=last_line=0;
		print_pos(0, 0);
		if(selecting) {
		    pending_restart=MESSAGE_STOP;
		    handle_restart(42);
		}
		else {
		    clear_screen();
		    if(should_autoselect_next()) {
			select_next_file();
			load_next_module();
		    }
		    else {
			pending_restart=MESSAGE_STOP;
			handle_restart(42);
		    }
		}
	    }
	    else
		print_time(t);
	}
	break;
      default:
	error("Illegal synced message received by main (%d).\n", mess.type);
    }
}


int pending_syncedmessage(void)
{
    fd_set rset;
    int nr;
    struct timeval null_time={0,0};

    FD_ZERO(&rset);
    FD_SET(seqfd, &rset);
    if((nr=select(seqfd+1, &rset, 0, 0, &null_time)) >= 0) {
	if(FD_ISSET(seqfd, &rset))
	    return 1;
	else
	    return 0;
    }
    else {
	if(errno != EINTR)
	    error("select() failed (errno=%d).\n", errno);
    }
    return 0; /* Remove warning */
}


void handle_restart(int songpos)
{
    struct mod_message mess;

    if(playing)
	total_time+=time(0)-restart_time;

    mess.type=pending_restart;
    switch(pending_restart) {
      case MESSAGE_STOP:
      case MESSAGE_RESTART:
	send_to_player(&mess);
	if(pending_restart == MESSAGE_RESTART)
	    send_to_player_generic(&opt.active_voices,
				   sizeof(opt.active_voices));
	wait_ack();
	if(pending_restart != MESSAGE_STOP) {
	    set_status(STATUS_PLAYING);
	    last_line=0;
	}
	starting_time=time(0);
	opt.maxtime=0;
	break;
      case MESSAGE_JUMPPATTERN:
	mess.a1.songpos=MIN(MAX(0, songpos+pending_info), M.songlength-1);
	send_to_player(&mess);
	mess.a1.speed=last_speed;
	mess.a2.tempo=last_tempo;
	send_to_player(&mess);
	send_to_player_generic(&opt.active_voices, sizeof(opt.active_voices));
	last_line=0;
	wait_ack();
	opt.maxtime=0;
	break;
      default:
	error("Internal error in handle_restart()");
    }
    restart_time=time(0);
    pending_restart=0;
}


/* If outgoing_too is false this function will hang until somone drains
 * the main2player-pipe (busy-wait, but this should only be for very
 * short periods of time if everything is working as it should).
 */

void drain_pipes(char outgoing_too)
{
    fd_set rset;
    char buf[256];
    int nr;
    struct timeval nulltime;

    while(1) {
	nulltime.tv_sec=0;
	nulltime.tv_usec=0;
	FD_ZERO(&rset);

	FD_SET(main2player[PIPE_READ], &rset);
	FD_SET(player2main_synced[PIPE_READ], &rset);
	FD_SET(seqfd, &rset);
	FD_SET(STDIN_FILENO, &rset);

        if((nr=select(maxfd, &rset, 0, 0, &nulltime)) > 0) {
	    if(outgoing_too && FD_ISSET(main2player[PIPE_READ], &rset))
		read(main2player[PIPE_READ], buf, 256);
	    if(FD_ISSET(player2main_synced[PIPE_READ], &rset))
		read(player2main_synced[PIPE_READ], buf, 256);
	    if(FD_ISSET(seqfd, &rset))
		read(seqfd, buf, 256);
	    if(FD_ISSET(STDIN_FILENO, &rset))
		read(STDIN_FILENO, buf, 256);
	}
	else {
	    if(nr < 0)
		error("Select problems (%d).\n", errno);
	    break;
	}
    }
}


/* S between 1 and 3. If 0 and loaded we select a "good" one */

void set_voice_detail(int s)
{
    int detail[]={4, 9, 17};

    if(!s) {
	if(loaded) {
	    if(M.nr_voices <=4)
		s=1;
	    else if(M.nr_voices <=9)
		s=2;
	    else
		s=3;
	}
	else
	    s=1;
    }  
    nr_visible_voices=detail[s-1];
}


void set_status(int s)
{
    switch(s) {
      case STATUS_PLAYING:
	print_status("Playing");
	playing=1;
	break;
      case STATUS_READY:
	print_status("Ready");
	playing=0;
	break;
      default:
	error("Unknown status (%d).\n", s);
    }
}


void send_to_player(struct mod_message *m)
{
    safe_write(main2player[PIPE_WRITE], m, sizeof(struct mod_message));
}


void send_to_player_generic(void *m, int size)
{
    safe_write(main2player[PIPE_WRITE], (struct mod_message *)m, size);
}


/* Handshake-functions */

void send_ack()
{
    char data;
    safe_write(ack_pipe[PIPE_WRITE], (struct mod_message *)&data, 1);
}


void wait_ack()
{
    char data;
    safe_read(ack_pipe[PIPE_READ], (struct mod_message *)&data, 1);
}
