/* 
   glidetv - display TV picture on a 3dfx card and control the TV tuner
   (3dfx card and glide library required)

   Copyright (C) 1997 Ralph Metzler (rjkm@thp.uni-koeln.de)
   Copyright (C) 1998 Heikki Hannikainen <hessu@pspt.fi>
	 Copyright (C) 1999 Christian Gebauer (gebauer@bigfoot.com)

   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., 675 Mass Ave, Cambridge, MA 02139, USA.

   
   Changelog:
   	Initial version by Ralph Metzler (rjkm@thp.uni-koeln.de)
   	sometime during the year 1997...
   
   	31-Jan-1998	Heikki Hannikainen <hessu@pspt.fi>:

   		- Reformatted (indent...)
   		- Split up the stuff to functions, comment it to some degree
   		  (just to make myself clear how it works...)
   		- Added support for xtvscreen's config file ~/.xtvrc
   		  for input of channel data
   		- Added commands for selecting channels
   		- 7-segment LED styled channel indicator (ugly code)

		23-Jan-1999 Christian Gebauer (gebauer@bigfoot.com):

		  - Removed the channel indicator (it is not very useful
		    if you want to use your computer monitor as a tv-set).
			- Added proper initialization of the bttv-driver.
			- Added support for the picture-parameters in
			  xtvscreen´s config file.
			- Added support for different video-sources by pressing 'V'
			- sound card support:
			  glidetv is now able to adjust the audio volume.
				On exit the original volume is restored
			  The mayor advantage is that glidetv avoids the annoying
				cracking sound during the channel-switch by muting the audio
				during the procedure.
			- Add a key for muting audio.
			- Added a "Hot-key". With '/' the current channel is saved,
			  the '*'-Key recalls it latter. This is useful for switching
				to a channel with a high number.
			- Added more "channel-keys" in the row "asdfghjklöä" (Channel
			  Number 10 to 20)
			- Fixed the weird	reaction on function-keys.
			- Tweaked the grSstWinOpen-call a little bit, because the old
			  one doesn't work with a newer glide on my machine.
 */

#define CFGLEN 256

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <assert.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <sys/ioctl.h>
#include <ctype.h>
#include <linux/soundcard.h>

#include <glide.h>
#include <linutil.h>

#include "../driver/bttv.h"
#include "../driver/bt848.h"

struct channel_t 
{
  char *name;
  int freq;
  int nr;
  int contrast;
  int brightness;
  int hue;
  int colour;
  
  struct channel_t *next;
  struct channel_t *prev;
} *channels = NULL;

char	*bttvname = "/dev/bttv";	    /* BTTV device name */
int	fbttv;			                   	/* File descriptor for the device */
int one=1;
int zero=0;
int bttv_up=0;                      
struct video_capability vcap; 
struct video_channel *vchan;
int cur_source=0;
	
/* Real video resolution used: */
int width = 768, height = 576;		 /* set this to 640 x 480 for NTSC */

/* Glide graphics: */
GrHwConfiguration hwconfig;	      	/* Hardware configuration */
GrLfbInfo_t info;

/* Mixer */
int mix_fd=-1;
int vol_up;
int vol_orig;
int mix_id;
#define LEFT  0x0001
#define RIGHT 0x0100
int audio_mute = 0;
int audio_disabled = 1;


/*
 *  Print error-message and try close glide
 */

void fail(char *msg)
{
  if (bttv_up)
    {
      ioctl(fbttv, VIDIOCCAPTURE,&zero);
      close(fbttv);
    }
  printf(msg);
	grLfbUnlock(GR_LFB_WRITE_ONLY, GR_BUFFER_FRONTBUFFER);
  grGlideShutdown();
  exit(1);
}


/*
 *  Initialize Mixer
 */

void open_mixer(void)
{
	if (!audio_disabled) {
		mix_fd=open("/dev/mixer",O_RDWR);
		if(mix_fd<0) {
      printf("Fail: open /dev/mixer/\n");
      exit(1);
    }
		if(ioctl(mix_fd,MIXER_READ(mix_id),&vol_orig)<0) {
      printf("Fail: get volume\n");
      exit(1);
    }
	}
}


/*
 *  Shutdown mixer
 */

void close_mixer(void)
{
	if (!audio_disabled) {
		if(ioctl(mix_fd,MIXER_WRITE(mix_id),&vol_orig)<0)
			fail("Fail: set volume\n");	
		close(mix_fd);
	}
}


/*
 *	Initialize the Glide library
 */

void init_glide(void)
{
  GrScreenResolution_t resolution = GR_RESOLUTION_800x600;
  
  float scrWidth = 800.0f;
  float scrHeight = 600.0f;
  
  grGlideInit();
  assert(grSstQueryHardware(&hwconfig));
  grSstSelect(0);
  assert(grSstWinOpen(0, resolution, GR_REFRESH_60Hz, GR_COLORFORMAT_ABGR, GR_ORIGIN_UPPER_LEFT, 2, 0));
  grDitherMode(GR_DITHER_DISABLE);
  grClipWindow(0, 0, (FxU32) scrWidth, (FxU32) scrHeight);
  grBufferClear(0x00, 0, 0);
	
  info.size = sizeof(GrLfbInfo_t);
  
  if (grLfbLock(GR_LFB_WRITE_ONLY, GR_BUFFER_FRONTBUFFER,
		GR_LFBWRITEMODE_565, GR_ORIGIN_UPPER_LEFT, FXFALSE, &info)
	    == FXFALSE) 
    {
      fprintf(stderr, "grLfbLock: Failed to take write lock\n");
      grGlideShutdown();
      exit(1);
    }    
}


/*
 *	Close the Glide library
 */

void close_glide(void)
{
  grLfbUnlock(GR_LFB_WRITE_ONLY, GR_BUFFER_FRONTBUFFER);
  grGlideShutdown();
}


/*
 *	Open the bttv device
 */

void open_bttv(void)
{
  if ((fbttv = open(bttvname, O_RDWR)) < 0) {
    fprintf(stderr, "Cannot open %s: %s\n", bttvname, strerror(errno));
    exit(1);
  }
  bttv_up =1;
}


/*
 *	Initialize the bttv device
 */

void init_bttv(void)
{
  struct video_window vwin;
  struct video_buffer vbuf;
  struct video_audio *vaudio;
  int i;
  struct channel_t *channel;

  if(ioctl(fbttv,VIDIOCGCAP,&vcap))
    fail("VIDIOCGCAP failed \n");

  vchan = (struct video_channel*)malloc(sizeof(struct video_channel)*vcap.channels);
  memset(vchan,0,sizeof(struct video_channel)*vcap.channels);
  for (i = 0; i < vcap.channels; i++) {
		vchan[i].channel = i;
		if (ioctl(fbttv,VIDIOCGCHAN,&vchan[i]))
			fail("VIDIOCGCHAN failed \n");
	}

  vaudio = (struct video_audio*)malloc(sizeof(struct video_audio)*vcap.audios);
  memset(vaudio,0,sizeof(struct video_audio)*vcap.audios);
  for (i = 0; i < vcap.audios; i++) {
		vaudio[i].audio = i;
		if (-1 == ioctl(fbttv,VIDIOCGAUDIO,&vaudio[i]))
			fail("VIDIOCGAUDIO failed \n");
	}

  if(ioctl(fbttv, VIDIOCGWIN, &(vwin)) == -1)
    fail("VIDIOCGWIN failed \n");

  if(ioctl(fbttv, VIDIOCGFBUF, &(vbuf)) == -1)
    fail("VIDIOCGFBUF failed \n");

  vbuf.base=(void *)(1|(unsigned long)info.lfbPtr);
  vbuf.depth=16;
  vbuf.bytesperline=info.strideInBytes;
  vbuf.width=800;
  vbuf.height=600;
  if(ioctl(fbttv, VIDIOCSFBUF, &vbuf))
    fail("VIDIOCSFBUF failed \n");
	
  vaudio[0].flags &= ~VIDEO_AUDIO_MUTE;
  if (-1 == ioctl(fbttv,VIDIOCSAUDIO,&vaudio[0]))
    fail("VIDIOCSAUDIO failed \n");

  vwin.clipcount=0;
  vwin.flags=0;
  vwin.y=0;
  vwin.x=0;
  vwin.width=width;
  vwin.height=height;
  if (ioctl(fbttv, VIDIOCSWIN, &vwin))
    fail("VIDIOCSWIN failed \n");
	
 
	if ((cur_source >= vcap.channels) || (cur_source < 0))   /* Wrong commandline parameter */
		cur_source = 0;
	if(ioctl(fbttv, VIDIOCSCHAN,&vchan[cur_source]))
    fail("VIDIOCSCHAN failed \n");

	
  channel=channels;
  i = channel->freq;
  if(ioctl(fbttv, VIDIOCSFREQ,&i))
    fail("VIDIOCSFREQ failed \n");
   
  if(ioctl(fbttv, VIDIOCCAPTURE,&one))
    fail("VIDIOCCAPTURE failed \n");
  
  free(vaudio);
}


/*
 *	Close the bttv device
 */

void close_bttv(void)
{
	free(vchan);
  if(ioctl(fbttv, VIDIOCCAPTURE,&zero))
    fail("VIDIOCCAPTURE failed \n");
  close(fbttv);
}


/*
 *	Read the configuration
 */

void get_xtvrc(char *path, int size)
{
  const char *home = getenv("HOME");
  const char *file = ".xtvrc";
	
  strncpy(path, home, size);
  strncat(path, "/", size);
  strncat(path, file, size);
}

void read_config(void)
{
  FILE *f;
  char fn[1024];
  char s[CFGLEN];
  char *cmd, *par;
  int i = 0;
  struct channel_t *channel = NULL, *prev, **prevp = &channels;
  
  get_xtvrc(fn, 1024);
	
  if (!(f = fopen(fn, "r"))) {
		fprintf(stderr, "Could not open %s for reading: %s\n", fn, strerror(errno));
		exit(1);
	}
  
  while (fgets(s, CFGLEN, f)) {
		cmd = strtok(s, " ");
		par = strtok(NULL, "\n");
		if (!strcasecmp(cmd, "Channel:")) {
			prev = channel;
			channel = malloc(sizeof(struct channel_t));
			*prevp = channel;
			prevp = &channel->next;
			channel->prev = prev;
			i++;
			channel->nr = i;
			channel->name = strdup(par);
		} else {
			if ((!strcasecmp(cmd, "Frequency:")) && (channel)) {
	      channel->freq = atoi(par);
	      fprintf(stderr, "Channel %d: %s at %d", channel->nr, channel->name, channel->freq);
	    } 
			if ((!strcasecmp(cmd, "CBHC:")) && (channel)) {
	      sscanf(par,"%i%i%i%i",&(channel->contrast),&(channel->brightness),&(channel->hue),&(channel->colour));
	      fprintf(stderr,"   (%s)\n",par);
	    }  
		}
	}
  
  fclose(f);
  
  if (!channel) {
		fprintf(stderr, "Uh-oh. No channels found! Need to create ~/.xtvrc with xtvscreen first.\n");
		exit(1);
	}
  
  channel->next = channels;
  channel->next->prev = channel;
}


/*
 *	Find a channel by number
 */
 
struct channel_t *find_channel(struct channel_t *old, int i)
{
  struct channel_t *ch = channels;
  
  do {
		ch = ch->next;
	} while ((ch->nr != i) && (ch != channels));
  
  if (ch->nr == i)
    return ch;
   else
    return old;
}


/*
 *  Parse commandline
 */

void cmd_line(int argc, char **argv)
{
  int i = 1;
	
	while (i < argc) {
		if (strcmp("-h",argv[i]) == 0) {
				printf("Usage: glidetv [-h] [-s Number] [-a Channel Volume]\n\n");
				printf("       -h                 this message\n");
				printf("       -s Number          select the default videosource\n");		
				printf("       -a Channel Volume  enable soundcard support:\n");
				printf("                          Channel: {line/cd/mic}\n");
				printf("                          Volume: 0-100\n");
				exit(0);
		} else {
			if (strcmp("-s",argv[i]) == 0) {
				cur_source=atoi(argv[i+1]);
				i+=2;
			} else {
				if (strcmp("-a",argv[i]) == 0) {
					audio_disabled = 0;
					if (strcmp("line",argv[i+1]) == 0)
						mix_id = SOUND_MIXER_LINE;
					else
						if (strcmp("cd",argv[i+1]) == 0)
							mix_id = SOUND_MIXER_CD;
						else
							if (strcmp("mic",argv[i+1]) == 0)
								mix_id = SOUND_MIXER_MIC;
							else {
								printf("Wrong audio-parameter\n");
								exit(1);
							}
					vol_up = atoi(argv[i+2]);
					if ((vol_up > 100)||(vol_up <= 0)) {
						printf("Wrong audio-parameter\n");
						exit(1);						
					}
					i+=3;
				}
			}
		}
	}
}



/*
 *	Start it up
 */

void init(int argc, char **argv)
{
	cmd_line(argc,argv);
  read_config();
  open_mixer();
  open_bttv();
  init_glide();
  init_bttv();
}


/*
 *	Close it down
 */

void closedown(void)
{
  close_mixer();
  close_bttv();
  close_glide();
  fprintf(stderr, "TV closed.\r\n");
  exit(0);
}


/*
 *  Change the audio volume
 */

void set_volume(int vol)
{
	if (!audio_disabled) {
		if (!audio_mute) {
			vol=(LEFT+RIGHT)*vol;
			if(ioctl(mix_fd,MIXER_WRITE(mix_id),&vol)<0)
				fail("Fail: set volume\n");
		}
	}
}


/*
 *	Change the tuner frequency
 */
 
void change_channel(struct channel_t *channel)
{
  struct video_picture params;
  ulong i = channel->freq;
    
  if (!channel) {
		fprintf(stderr, "Cannot change to a NULL channel!\n");
		closedown();
	}
  
	set_volume(0);
	
  if(ioctl(fbttv, VIDIOCCAPTURE,&zero))
    fail("VIDIOCCAPTURE failed \n");

  fprintf(stderr, "Choosing channel %d: %s\r\n", channel->nr, channel->name);
  
  if (ioctl(fbttv, VIDIOCSFREQ, &i))
    fail("VIDIOCSFREQ failed \n");
	
	usleep(99999);

  if (ioctl(fbttv, VIDIOCGPICT, &params)) 
		fail("VIDIOCGPICT failed \n");
	else {
		params.contrast = channel->contrast<<7;
		params.brightness = (channel->brightness+128)<<8;
		params.hue = (channel->hue+128)<<8;
		params.colour = channel-> colour<<7;
		if (ioctl(fbttv, VIDIOCSPICT, &params))
			fail("VIDIOCSPICT failed \n");
	}
 
  if(ioctl(fbttv, VIDIOCCAPTURE,&one))
    fail("VIDIOCCAPTURE failed \n");

	set_volume(vol_up);
}


/*
 *  start/stop capturing
 */

void toggle_capturing(void)
{
	static int capturing = 1;
	
	if (capturing) {
		if(ioctl(fbttv, VIDIOCCAPTURE,&zero))
			fail("VIDIOCCAPTURE failed \n");
		close_glide();
		fprintf(stderr, "Capturing disabled, press C again to continue...\r\n");
		capturing = 0;
	} else {
		init_glide();
		if(ioctl(fbttv, VIDIOCCAPTURE,&one))
			fail("VIDIOCCAPTURE failed \n");
		fprintf(stderr, "Capturing enabled.\r\n");
		capturing = 1;
	}
}


/*
 *  mute/unmute audio
 */

void toggle_audio(void)
{
	if (!audio_disabled) {
		if (audio_mute) {
			audio_mute = 0;
			set_volume(vol_up);
			fprintf(stderr, "Audio unmuted.\r\n");
		} else {
			set_volume(0);
			fprintf(stderr, "Audio muted, press M again to unmute...\r\n");
			audio_mute = 1;
		}
	}
}


/*
 *  Decrease audio volume
 */

void decrease_volume(void)
{
	if (!audio_disabled)
		if (vol_up) {
			if ((vol_up-=2)<0)
				vol_up = 0;
			set_volume(vol_up);
			fprintf(stderr, "Audio volume at %i.\r\n",vol_up);
		}
}


/*
 *  Increase audio volume
 */

void increase_volume(void)
{
	if (!audio_disabled)
		if (vol_up<100) {
			if ((vol_up+=2)>100)
				vol_up = 100;
			set_volume(vol_up);
			fprintf(stderr, "Audio volume at %i.\r\n",vol_up);
		}
}


/*
 *  Switch video source
 */

void switch_source(void)
{
	if (++cur_source >= vcap.channels)
		cur_source = 0;
	if(ioctl(fbttv, VIDIOCSCHAN,&vchan[cur_source]))
    fail("VIDIOCSCHAN failed \n");
	fprintf(stderr, "Video Source: %s.\r\n",vchan[cur_source].name);
}

	
/*
 *  Process the channelkeys
 */

void process_channelkeys(struct channel_t **channel, char c)
{
	int num=0;
	
	if ((c >= '1') && (c <= '9')) {
		*channel = find_channel(*channel, (int)c - 48);
		change_channel(*channel);
	} else {
		if (c == 'a')
			num = 10;
		if (c == 's')
			num = 11;
		if (c == 'd')
			num = 12;
		if (c == 'f')
			num = 13;
		if (c == 'g')
			num = 14;
		if (c == 'h')
			num = 15;
		if (c == 'j')
			num = 16;
		if (c == 'k')
			num = 17;
		if (c == 'l')
			num = 18;
		if ((c == 'ö')||(c == ';'))
			num = 19;
		if ((c == 'ä')||(c == '\''))
			num = 20;
		if (num != 0) {
			*channel = find_channel(*channel,num);
			change_channel(*channel);
		}
	}
}
 
/*
 *	Main
 */

int main(int argc, char **argv)
{
  char c;
  struct channel_t *channel, *jump;
 
  init(argc,argv);
	
  jump = channel = channels;
  change_channel(channel);
  
  while (1)
	{
		c = tolower(lin_getch());
		if (lin_kbhit()) {            /* Catch function-keys */
			while (lin_kbhit())
				c = lin_getch();
			c = 0;
		}
		switch (c) {
		 case '+':
		 case 'n':
		 case ' ':
			 channel = channel->next;
			 change_channel(channel);
			 break;
		 case '-':
		 case 'p':
			 channel = channel->prev;
			 change_channel(channel);
			 break;
		 case '/':
			 jump=channel;
			 break;
		 case '*':
			 channel=jump;
			 change_channel(channel);
			 break;
		 case 'c':
			 toggle_capturing();
			 break;
		 case 'm':
			 toggle_audio();
			 break;
		 case 'z':
		 case 'y':
			 decrease_volume();
			 break;
		 case 'x':
			 increase_volume();
			 break;
		 case 'v':
			 switch_source();
			 break;
		 case 'q':
			 closedown();
			 break;
		 default : process_channelkeys(&channel,c);
		}
	}
	return 0;
}
