/*
 * Stonage: a puzzle game
 * Copyright (C) 2001 Luc Saillard <luc@saillard.org>
 * 
 * Functions to play soundship file using a YM2149 processor.
 * (found in the Atari ST machine)
 *
 * 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., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 * 
 */

#include "stonage.h"
#include "music_ym.h"
#include "game.h"

#include <errno.h>
#include <unistd.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <dirent.h>
#include <string.h>
#include <math.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/ioctl.h>

#include <linux/soundcard.h>

/*
 *  This is taken from psgplay written by Stefan Berndtsson <stefan@nocrew.org>
 *  This piece of code is under GPL
 */

#ifdef FIXED_MATH
#  define FIXED_TYPE int
#  define FIXED(x) ((int)((x) * 256))
#  define DIV(x, y) ((((x) + ((y) >> 1)) << 8) / (y))
#  define MOD(x, y) ((x) % (y))
#  define FIXED_TO_INT(x) (((x) + 128) >> 8)
#else
#  define FIXED_TYPE double
#  define FIXED(x) x
#  define DIV(x, y) ((x) / (y))
#  define MOD(x, y) fmod (x, y)
#  define FIXED_TO_INT(x) (x)
#endif

#define MAX_CHANNELS 3
#define MFP_FREQUENCY 2457600
#define PSG_FREQUENCY 2000000
#define SAMPLE_FREQ 44100
#define SAMPLE_CHAN 2
#define SAMPLE_BITS 16
#define SAMPLE_OFFSET (SAMPLE_CHAN*SAMPLE_BITS/8)
#define SAMPLE_BUFFER (SAMPLE_FREQ*SAMPLE_CHAN*SAMPLE_BITS/8)
#define VBL 50
#define DETAIL (1<<14)
#define DETAIL_LOOP (DETAIL-1)
#define TYPES 3
#define ECHO_DISTANCE 5
#define ECHO_LEVEL 7
#define INLINE static __inline__

#define MODE_SQUARE 0
#define MODE_PSGSQR 1
#define MODE_SINUS 2


typedef struct {
  unsigned char *buffer;
  int size;
} raw_buffer;

typedef struct {
  int volume;
  int frequency;
  int psg_freq;
  int env_enable;
  int tone_enable;
  int timer_enable;
  int noise_enable;
  int period_count;
} Channel;

typedef struct {
  Channel channel[MAX_CHANNELS];
  int env_mode;
  int env_psg_freq;
  FIXED_TYPE env_frequency;
  int env_period_count;
  int env_period_hold;
  int timer_freq;
  int timer_duration;
  int timer_count;
  int timer_vmax;
} VBL_Frame;

typedef struct {
  long id;
  long length;
  long mask;
  int regs;
  short no_digi;
  signed short tone[DETAIL][TYPES];
  signed short noise[DETAIL];
  unsigned char *data;
  void *buffer;
} YM_Header;

static YM_Header ymdata;

typedef struct {
  int echo;
  int verbose;
  int different;
  int repeat;
  int quiet;
  int stereo;
  int surround;
  char *output;
} PSG_Settings;

#ifdef WORDS_BIGENDIAN
#define SWAPSHORT(x) (x)
#define SWAPLONG(x) (x)
#define SWAPSTOLITTLE(x) (((x & 0xff00)>>8)|((x & 0xff)<<8))
#define SWAPLTOLITTLE(x) (((x & 0xff000000)>>24)|((x & 0xff0000)>>8)|\
                          ((x & 0xff00)<<8)|((x & 0xff)<<24))
#else
#define SWAPSHORT(x) (((x & 0xff00)>>8)|((x & 0xff)<<8))
#define SWAPLONG(x) (((x & 0xff000000)>>24)|((x & 0xff0000)>>8)|\
                     ((x & 0xff00)<<8)|((x & 0xff)<<24))
#define SWAPSTOLITTLE(x) (x)
#define SWAPLTOLITTLE(x) (x)
#endif

#define ONE FIXED (1)
#define PI FIXED (M_PI)


static int audio_device;
static pthread_t sound_thread;
static int sound_initialised=0;
static int player_active=0;

static int YM_Oss_Init(const int argc, const char **argv);
static void YM_PlaySampleOss(const void *buffer, const int buflen);
static void (*YM_PlaySample)(const void *buffer, const int size);

static double psg_one_sample(double in, int mode);


INLINE FIXED_TYPE env_wave_00XX(FIXED_TYPE in)
{
  if(in < PI)
    return ONE - DIV (in, PI);
  else 
    return 0;
}

INLINE FIXED_TYPE env_wave_01XX(FIXED_TYPE in)
{
  if(in < PI)
    return DIV (in, PI);
  else
    return 0;
}

INLINE FIXED_TYPE env_wave_1000(FIXED_TYPE in)
{
  return ONE - DIV (MOD (in, PI), PI);
}

INLINE FIXED_TYPE env_wave_1001(FIXED_TYPE in)
{
  if(in < PI)
    return ONE - DIV (in, PI);
  else 
    return 0;
}

INLINE FIXED_TYPE env_wave_1010(FIXED_TYPE in)
{
  FIXED_TYPE tmp;
  tmp = MOD (in, PI*2);
  if(tmp < PI)
    return ONE - DIV (tmp, PI);
  else
    return DIV (tmp - PI, PI);
}

INLINE FIXED_TYPE env_wave_1011(FIXED_TYPE in)
{
  if(in < PI)
    return ONE - DIV (in, PI);
  else
    return ONE;
}

INLINE FIXED_TYPE env_wave_1100(FIXED_TYPE in)
{
  return DIV (MOD (in, PI), PI);
}

INLINE FIXED_TYPE env_wave_1101(FIXED_TYPE in)
{
  if(in < PI)
    return DIV (in, PI);
  else
    return ONE;
}

INLINE FIXED_TYPE env_wave_1110(FIXED_TYPE in)
{
  FIXED_TYPE tmp;
  tmp = MOD (in, PI*2);
  if(tmp < PI)
    return DIV (tmp, PI);
  else
    return ONE - DIV (tmp - PI, PI);
}

INLINE FIXED_TYPE env_wave_1111(FIXED_TYPE in)
{
  if(in < PI)
    return DIV (in, PI);
  else
    return ONE;
}

static FIXED_TYPE env_wave(FIXED_TYPE in, int mode)
{
  switch(mode) {
  case 0:
  case 1:
  case 2:
  case 3:
    return env_wave_00XX(in);
    break;
  case 4:
  case 5:
  case 6:
  case 7:
    return env_wave_01XX(in);
    break;
  case 8:
    return env_wave_1000(in);
    break;
  case 9:
    return env_wave_1001(in);
    break;
  case 10:
    return env_wave_1010(in);
    break;
  case 11:
    return env_wave_1011(in);
    break;
  case 12:
    return env_wave_1100(in);
    break;
  case 13:
    return env_wave_1101(in);
    break;
  case 14:
    return env_wave_1110(in);
    break;
  case 15:
    return env_wave_1111(in);
    break;
  }
  return 0;
}

static double psg_one_sample(double in, int mode)
{
  double out,tmp;

  tmp = fmod(in, M_PI*2);

  switch(mode) {
  case MODE_SQUARE:
    if(tmp < M_PI)
      return -1;
    else
      return 1;
  case MODE_SINUS:
    return sin(tmp);
  case MODE_PSGSQR:
  default:
    if(tmp < M_PI)
      out = 2 / tmp;
    else
      out = -(2 / (tmp - M_PI));
    
    if(out > 1)
      out = 1;
    
    if(out < -1)
      out = -1;
    
    return out;
  }
}

INLINE int psgfreq_to_freq(int psgfreq)
{
  if(psgfreq)
    {
      int denominator = SAMPLE_FREQ * psgfreq;
      return (DETAIL *(PSG_FREQUENCY/16.0) + (denominator >> 1)) / denominator;
    }
  else
    return 0;
}

static VBL_Frame *psg_data_to_frame(YM_Header *head, int frame_number, VBL_Frame *old)
{
  unsigned char *tmp, tmp2[16];
  int prediv[8] = {-1, 4, 10, 16, 50, 64, 100, 200 };
  
  VBL_Frame *new;
  int i, tmpval;

  new = (VBL_Frame *) malloc(sizeof(VBL_Frame));

  tmp = head->data + frame_number;
  /* tmp = tmp2; */

  for(i=0;i<head->regs;i++)
    tmp2[i] = tmp[i*head->length];

  for(i=0;i<MAX_CHANNELS;i++) {
    new->channel[i].psg_freq = tmp2[2*i] + ((tmp2[2*i+1]&0xf) << 8);
    new->channel[i].volume = 15-(((tmp2[8+i]&0x1f)==16)?15:(tmp2[8+i]&0x1f));
    new->channel[i].frequency = psgfreq_to_freq(new->channel[i].psg_freq);
    new->channel[i].period_count = (old->channel[i].period_count);
    new->channel[i].tone_enable = !(tmp2[7] & (1<<i));
    new->channel[i].noise_enable = !(tmp2[7] & (8<<i));
    new->channel[i].env_enable = ((tmp2[8+i]&0x1f) == 16);
    new->channel[i].timer_enable = 0;
  }

  if(head->regs > 14) {
    tmpval = (tmp2[1] & 0x30) >> 4;
    
    if(tmpval) {
      new->channel[tmpval-1].timer_enable = 1;

      tmpval = (tmp2[6] & 0xe0) >> 5; 
      new->timer_freq = (MFP_FREQUENCY/prediv[tmpval])/tmp2[14];
      new->timer_duration = (SAMPLE_FREQ/new->timer_freq);
      if(tmp2[1] & 0x40)
	new->timer_count = 0;
      else
	new->timer_count = old->timer_count;
      
      new->timer_vmax = 15;// (tmp2[5] & 0xf0) >> 4;
    }
  }

  new->env_psg_freq = tmp2[11] + (tmp2[12] << 8);
  if(new->env_psg_freq)
    {
      new->env_frequency = DIV (PSG_FREQUENCY, 256 * new->env_psg_freq);
      /* 
	 new->env_frequency =
	 (((double)PSG_FREQUENCY/(double)256)/(double)new->env_psg_freq);
	 */
    }
  else
    new->env_frequency = 0;

  if(tmp2[13] == 0xff) {
    new->env_mode = old->env_mode;
    new->env_period_count = old->env_period_count;
    new->env_period_hold = old->env_period_hold;
  } else {
    new->env_mode = tmp2[13];
    new->env_period_count = 0;
    new->env_period_hold = 0;
  }
  free(old);

  return new;
}

static VBL_Frame *psg_empty_to_frame(void)
{
  VBL_Frame *new;
  int i;

  new = (VBL_Frame *) malloc(sizeof(VBL_Frame));
  new->env_period_count = 0;
  new->env_period_hold = 0;
  new->env_mode = 0;
  for(i=0;i<MAX_CHANNELS;i++) {
    new->channel[i].period_count = 0;
  }
  return new;
}

static void psg_play_frame(YM_Header *head, VBL_Frame *frame, PSG_Settings *set,
		    int currframe, int echoframe)
{
  static signed short buffer[ECHO_DISTANCE+1][SAMPLE_BUFFER/VBL];
  int i,c,tmp,count,left,right,timer_vol;
  int sample[MAX_CHANNELS];

  for(i=0;i<(SAMPLE_FREQ/VBL);i++) {
    for(c=0;c<MAX_CHANNELS;c++) {
      tmp = 0;
      count = 0;
      if(frame->channel[c].timer_enable) {
	frame->timer_count = 
	  (frame->timer_count + 1)%(frame->timer_duration*2);
	if(frame->timer_count > frame->timer_duration) {
	  timer_vol = 0;
	} else {
	  timer_vol = frame->timer_vmax;
	}
      } else {
	timer_vol = frame->channel[c].volume;
      }
      if(frame->channel[c].tone_enable) {
	tmp = head->tone[frame->channel[c].period_count&DETAIL_LOOP]
	  [MODE_SQUARE] >> timer_vol;
	count++;
	frame->channel[c].period_count += frame->channel[c].frequency;
      }
      if(frame->channel[c].noise_enable) {
	tmp += head->noise[i&DETAIL_LOOP] >> timer_vol;
	count++;
      }
      if(frame->channel[c].env_enable) {
#ifdef FIXED_MATH
	const int factor = (int)(256 * M_PI*2/DETAIL + 0.5);
	tmp *= env_wave(frame->env_period_count*factor, frame->env_mode);
	tmp = FIXED_TO_INT (tmp);
#else
	tmp *= env_wave(frame->env_period_count*M_PI*2/DETAIL, frame->env_mode);
#endif
      }
      if(count)
	sample[c] = tmp/count;
    }
    if(frame->env_frequency)
      frame->env_period_count =
	FIXED_TO_INT (FIXED (frame->env_period_count) +
		      2 * DIV (DETAIL, SAMPLE_FREQ * frame->env_frequency));
    /* frame->env_period_count += DETAIL/(SAMPLE_FREQ/frame->env_frequency)*2; */

      left = 2*sample[0]+sample[1];
      right = sample[1]+2*sample[2];

      buffer[currframe][i*2] = (10 * left + ECHO_LEVEL * buffer[echoframe][i*2+1])/(ECHO_LEVEL+10);
      buffer[currframe][i*2+1] = (10 * right + ECHO_LEVEL * buffer[echoframe][i*2])/(ECHO_LEVEL+10);

      //buffer[currframe][i*2] = (signed short)left;
      //buffer[currframe][i*2+1] = (signed short)right;
  }
  YM_PlaySample(buffer[currframe], SAMPLE_BUFFER/VBL);
}




/**
 * This is the thread which play YM music.
 *
 * @param arg
 * @return always NULL
 */
static void *
YM_Thread(void *arg)
{
  VBL_Frame *frame, *old;
  int i;

  old = psg_empty_to_frame();

  while (player_active)
   {
     i=0;
     while(i<ymdata.length && player_active)
      {
	//usleep(1000);
	frame = psg_data_to_frame(&ymdata, i, old);
#if 1
	  psg_play_frame(&ymdata, frame, NULL, i%(ECHO_DISTANCE+1),(i+ECHO_DISTANCE+2)%(ECHO_DISTANCE+1));
#else
	  psg_play_frame(&ymdata, frame, NULL, 0, 0);
#endif
	old = frame;
	i++;
      }
   }
  YM_Free();
  trace("Bye bye");
  return NULL;
}


/**
 * Initialise YM Sound System
 *
 * @param argc
 * @param argv
 * @return TRUE if the subsystem is initialized else 0
 */
int
YM_Init(const char argc, const char **argv)
{
  if (YM_Oss_Init(argc,argv)==0)
    return 0;

  sound_initialised=1;
  return 1;
}


/**
 * Release YM Sound System
 *
 * @param argc
 * @param argv
 * @return TRUE if the subsystem is initialized else 0
 */
void
YM_Release(void)
{
  if (sound_initialised && audio_device>=0)
   {
     YM_Stop();
     YM_Free();
     close(audio_device);
     sound_initialised=0;
   }
}



/**
 *
 * Free ressource use by sound system
 *
 */
void YM_Free(void)
{
  if (sound_initialised)
   {
     if (ymdata.buffer)
       free(ymdata.buffer);
     ymdata.buffer=NULL;
   }
}

/**
 *
 * Play Music
 *
 */
int
YM_Start(void)
{
  if (sound_initialised && ymdata.buffer)
   {
     player_active=1;
     if (pthread_create(&sound_thread,NULL,YM_Thread,NULL))
      {
	fprintf(stderr,"YM_Start: Error when creating thread\n");
	return 0;
      }
     return 1;
   }
  return 0;
}

/**
 *
 * Stop Playing Music
 *
 */
void
YM_Stop(void)
{
  if (sound_initialised)
   {
     player_active=0;
   }
}

/**
 *
 * load a new module in memory
 *
 *
 */
int
YM_Load(const char *musicfile)
{
  int fd,i,size;
  char tmp[1024];
  struct stat st;
  unsigned char id[4];
  void *data;
  char *name1,*name2,*name3;

  if (!sound_initialised)
    return 0;
  fd=-1;
  i=0;
  while (datadir_path[i])
   {
     snprintf(tmp,1023,"%s/music/%s",datadir_path[i],musicfile);
     trace("Trying to load %s",tmp);

     fd = open(tmp, O_RDONLY);
     if(fd>=0)
       break;
     i++;
   }
  if (fd<0)
   {
     fprintf(stderr,_("Can't load this soundchip file %s\n"),tmp);
     return 0;
   }

  stat(tmp, &st);
  data = malloc(st.st_size);
  read(fd,id,4);
  size = read(fd,data,st.st_size);
  close(fd);

  ymdata.buffer=data;

  switch(id[2] - '0')
    {
     case 2:
     case 3:
       trace("Soundchip YM version %d",id[2] - '0');
       ymdata.length = (size)/14;
       ymdata.data = data;
       ymdata.regs = 14;
       break;
     case 4:
     case 5:
     case 6:
       trace("Soundchip YM version %d",id[2] - '0');
       ymdata.length = SWAPLONG(*((long *) ((unsigned char *)data + 8)));
       ymdata.mask = SWAPLONG(*((long *) ((unsigned char *)data + 12)));
       ymdata.no_digi = SWAPSHORT(*((short *) ((unsigned char *)data + 16)));
       if(ymdata.no_digi) 
	{
	  fprintf(stderr, "Digidrum is unsupported.\n");
	  return 0;
	}
       ymdata.regs = 16;
       name1 = (unsigned char *)data + 30;
       name2 = name1 + strlen(name1) + 1;
       name3 = name2 + strlen(name2) + 1;
       ymdata.data = name3 + strlen(name2) + 1;
       trace("1: %s", name1);
       trace("2: %s", name2);
       trace("3: %s", name3);
       break;
     default:
       return 0;
    }
  for(i=0;i<DETAIL;i++) 
   {
     ymdata.tone[i][MODE_SQUARE] = 
       (int)(32760*psg_one_sample(i*M_PI*2/DETAIL, MODE_SQUARE)/3 + 0.5);
     ymdata.tone[i][MODE_PSGSQR] = 
       (int)(32760*psg_one_sample(i*M_PI*2/DETAIL, MODE_PSGSQR)/3 + 0.5);
     ymdata.tone[i][MODE_SINUS] = 
       (int)(32760*psg_one_sample(i*M_PI*2/DETAIL, MODE_SINUS)/3 + 0.5);
     ymdata.noise[i] = ((RANDOM() & 65535) - 32768)/3;
   }

  return 1;
}


/**
 * Initialize audio subsystem for OSS
 *
 * @param argc the number or argument found in argv
 * @param argv the commandline argument
 * @return return TRUE if the subsystem is initialized
 */
static int 
YM_Oss_Init(const int argc, const char **argv)
{
  int fd = -1;

  int format = AFMT_S16_NE;
  int speed = 44100;
  int stereo = 1;

  fd = open("/dev/dsp", O_WRONLY);
  if(fd < 0)
   {
     fprintf(stderr,"Couln't open /dev/dsp");
    return 0;
   }

  ioctl(fd, SNDCTL_DSP_RESET, 0);
  ioctl(fd, SNDCTL_DSP_SETFMT, &format);
  ioctl(fd, SNDCTL_DSP_SPEED, &speed);
  ioctl(fd, SNDCTL_DSP_STEREO, &stereo);

  audio_device = fd;

  YM_PlaySample=YM_PlaySampleOss;
  
  return 1;
}

/**
 * Play a buffer using OSS
 * @param buffer
 * @param size
 * @return nothing
 */
static void
YM_PlaySampleOss(const void *buffer, const int buflen)
{
  write(audio_device,buffer,buflen);
}



