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

#if defined(__linux__)
# include <linux/soundcard.h>
#elif defined(__FreeBSD__)
# include <machine/soundcard.h>
#endif

#ifdef YSHM_SUPPORT
# include "../include/shm.h"
#endif

#include "ytypes.h"
#include "midiiow.h"
#include "audiocd.h"
#include "ysound.h"
#include "options.h"


int YSoundInit(Recorder *recorder, Audio *audio);
int YSoundShellOut(Recorder *recorder);
void YSoundCaliberateCycle(Recorder *recorder);
int YSoundPlayBuffer(Recorder *recorder);
void YSoundSync(Recorder *recorder, int options);
void YSoundShutdown(Recorder *recorder);


#define MIN(a,b)        ((a) < (b) ? (a) : (b))
#define MAX(a,b)        ((a) > (b) ? (a) : (b))
#define CLIP(a,l,h)     (MIN(MAX((a),(l)),(h)))
#define ABSOLUTE(x)     (((x) < 0) ? ((x) * -1) : (x))


/*
 *	Initializes the recorder, the Audio values specified in
 *	audio will be coppied to the recorder's Audio values and
 *	initialized.
 *
 *	Any errors which occured from opening any devices will be printed
 *	to stderr.
 */
int YSoundInit(Recorder *recorder, Audio *audio)
{
	int status;
	Sound *sound_ptr;
	Audio *audio_ptr;
	MIDIAudio *midi_audio_ptr;
#ifdef YSHM_SUPPORT
	int shm_id;
#endif
#ifdef OSS_BUFFRAG
	unsigned int fragment, num_fragments, fragment_size;
	int buffer_size;
#endif	/* OSS_BUFFRAG */
/*	struct mixer_info mixer_info_buf; */
	struct synth_info synth_info_buf;


	if((recorder == NULL) || (audio == NULL))
	    return(-1);

	/* Get pointers to substructures on recorder. */
	sound_ptr = &recorder->sound;
	audio_ptr = &recorder->audio;
	midi_audio_ptr = &recorder->audio.midi_audio;

	/* Reset recorder values. */
	memset(sound_ptr, 0x00, sizeof(Sound));
	memset(audio_ptr, 0x00, sizeof(Audio));
	memset(midi_audio_ptr, 0x00, sizeof(MIDIAudio));


	/* Get values from input Audio structure. */
	sound_ptr->buffer_length = 0;
	sound_ptr->buffer_content_length = 0;

	sound_ptr->device_buffer_length = 0;
	sound_ptr->device_buffer_content_length = 0;

	audio_ptr->cycle_set = audio->cycle_set;

	audio_ptr->cycle.ms = audio->cycle.ms;
	audio_ptr->cycle.us = audio->cycle.us;
	audio_ptr->compensated_cycle.ms = audio_ptr->cycle.ms;
	audio_ptr->compensated_cycle.us = audio_ptr->cycle.us;

	audio_ptr->write_ahead.ms = audio->write_ahead.ms;
	audio_ptr->write_ahead.us = audio->write_ahead.us;

	audio_ptr->cycle_ahead_left.ms = audio->write_ahead.ms;
	audio_ptr->cycle_ahead_left.us = audio->write_ahead.us;

	audio_ptr->cumulative_latency.ms = 0;
	audio_ptr->cumulative_latency.us = 0;
	
	audio_ptr->sample_size = audio->sample_size;
	audio_ptr->channels = audio->channels;  
	audio_ptr->sample_rate = audio->sample_rate;
	audio_ptr->bytes_per_second = audio->sample_rate *
	    MAX((audio->sample_size / 8), 1)
	;

#ifdef OSS_BUFFRAG
	audio_ptr->allow_fragments = audio->allow_fragments;
	audio_ptr->num_fragments = audio->num_fragments;  
	audio_ptr->fragment_size = audio->fragment_size;  
#endif  /* OSS_BUFFRAG */

	audio_ptr->flip_stereo = audio->flip_stereo;

	audio_ptr->direction = audio->direction;
	audio_ptr->audio_mode_name = NULL;
	audio_ptr->fd = -1;
	audio_ptr->mixer_fd = -1;

	audio_ptr->sound_name = NULL;

	audio_ptr->shelled_out = False;


	/* ******************************************************** */
	/* Initialize MIDI IO resources. */
#ifdef ALSA_RUN_CONFORM
	midi_audio_ptr->midi_device_number = MAX(
	    option.midi_device_number, 0
	);
#endif	/* ALSA_RUN_CONFORM */
	if(MIDIIOWInit(midi_audio_ptr))
	{
	    YSoundShutdown(recorder);
	    return(-1);
	}

	/* ******************************************************** */
	/* Initialize audio CD context. */
	audio_ptr->audiocd_context = AudioCDInit();

	/* ******************************************************** */
	/* Open audio device. */
	audio_ptr->fd = open(
	    fname.device,
	    ((audio_ptr->direction == AUDIO_DIRECTION_RECORD) ?
		O_RDONLY : O_WRONLY
	    )
	);
	if(audio_ptr->fd < 0)
	{
	    fprintf(
		stderr,
		"%s: Cannot open for %s.\n",
		fname.device,
		((audio_ptr->direction == AUDIO_DIRECTION_RECORD) ?
		    "recording" : "playing"
		)
	    );

	    YSoundShutdown(recorder);
	    return(-1);
	}

	/* Open mixer. */
	if(fname.mixer[0] != '\0')
	{
	    audio_ptr->mixer_fd = open(
		fname.mixer,
	        O_RDWR
	    );
	    if(audio_ptr->mixer_fd < 0)
	    {
		/* Warn about failure to open mixer, do not fail
		 * entire procedure.
		 */
		fprintf(stderr,
		    "%s: Cannot open.\n",
		    fname.mixer
		);
	    }
	}
	else
	{
	    /* No mixer specified. */
	    audio_ptr->mixer_fd = -1;
	}


#ifdef OSS_BUFFRAG
	/*
	 * OSS ioctl() call SNDCTL_DSP_SETFRAGMENT:
	 *
	 * Accepts an int parameter which has format 0x00nn00ss where
	 * the nn is max number of buffer fragments (between 0x02 and 0xff)
	 * and the ss gives indirectly the size of a buffer fragment
	 * (fragment_size = (1 << ss)). Valid sizes are between
	 * (ss=0x07 -> 128 bytes and ss=0x11 (17 dec) -> 128k).
	 *
	 *      Linux documentation suggestion: 0x00020009
	 *      XBlast Sound Server: 0x0004000a
	 */
	if(audio_ptr->allow_fragments)
	{
	    num_fragments = audio_ptr->num_fragments;
	    fragment_size = audio_ptr->fragment_size;

	    /* Pack fragment argument. */
	    fragment = (num_fragments << 16) | fragment_size;

	    /* Set new fragment parameters. */
	    status = ioctl(
		audio_ptr->fd,
		SNDCTL_DSP_SETFRAGMENT,
		&fragment
	    );
	    if(status == -1)
	    {
		fprintf(stderr,
     "%s: Warning: Cannot set buffer fragment configuration to 0x%.8x\n",
		    fname.device, fragment
		);
	    }
	}
#endif  /* OSS_BUFFRAG */ 


	/* Set sample format (aka sample size or number of bits). */
	if(ioctl(
	    audio_ptr->fd,
	    SNDCTL_DSP_SAMPLESIZE,
	    &audio_ptr->sample_size
	) == -1)
	{
	    fprintf(stderr,
		"%s: Warning: Cannot set sample size to %i bits.\n",
		fname.device, audio_ptr->sample_size
	    );
	}

	/* Set channels (1 or 2, mono or stereo respectivly). */
	if(ioctl(
	    audio_ptr->fd,
	    SNDCTL_DSP_CHANNELS,
	    &audio_ptr->channels
	) == -1)
	{
	    fprintf(stderr,
		"%s: Warning: Cannot set channels to %i.\n",
		fname.device, audio_ptr->channels
	    );
	}

	/* Set sample rate. */
	if(ioctl(
	    audio_ptr->fd,
	    SNDCTL_DSP_SPEED,   /* aka SOUND_PCM_WRITE_RATE. */
	    &audio_ptr->sample_rate
	) == -1)
	{
	    fprintf(stderr,
		"%s: Warning: Cannot set sample rate to %i Hz.\n",
		fname.device, audio_ptr->sample_rate
	    );
	}


/* Query audio format. */
/*
ioctl(audio_ptr->fd, AFMT_QUERY, &status);
printf("Format: %i\n", status);
 */

#ifdef OSS_BUFFRAG
	/* OSS requires exact audio buffer segment size (because of the
	 * fragmented buffers).
	 *
	 * All write()'s to the device must have a buffer of exactly
	 * this size.  SNDCTL_DSP_GETBLKSIZE must be called after
	 * SNDCTL_DSP_SETFRAGMENT.
	 */
	status = ioctl(
	    audio_ptr->fd,
	    SNDCTL_DSP_GETBLKSIZE,
	    &buffer_size
	);
	if(status == -1)
	{
	    fprintf(stderr,
		"%s: Warning: Cannot get buffer size.\n",
		fname.device
	    );
	}

	sound_ptr->buffer_length = buffer_size;
	sound_ptr->device_buffer_length = buffer_size;


	/*   Calculate theoretical and compensated cycle.
	 *
	 *   Warning: This value is rarly correct and should be
	 *   set manually by being defined in a YMode.
	 */
	if(audio_ptr->cycle_set == CYCLE_SET_HARDWARE)
	{
	    audio_ptr->cycle.ms =
		(int)sound_ptr->buffer_length *
		1000 / (int)audio_ptr->sample_rate /
	        MAX(((int)audio_ptr->sample_size / 8), 1);
	    audio_ptr->cycle.us =
		(double)sound_ptr->buffer_length /
		(double)audio_ptr->sample_rate * 1000000 /
		MAX(((int)audio_ptr->sample_size / 8), 1);
	    audio_ptr->cycle.us = audio_ptr->cycle.us -
	        (audio_ptr->cycle.ms * 1000);

	    /* For now, compensated is the same as theoretical. */
	    audio_ptr->compensated_cycle.ms = audio_ptr->cycle.ms;
	    audio_ptr->compensated_cycle.us = audio_ptr->cycle.us;
	}

	/* Not sure what this is... divides fragment segments? */
/*
	status = 4;
	ioctl(audio_ptr->fd, SOUND_PCM_SUBDIVIDE, &status);
 */

#endif	/* OSS_BUFFRAG */



	/* ********************************************************** */
	/* Allocate "midway" Sound buffer. */
#ifdef YSHM_SUPPORT
	sound_ptr->buffer = (SoundBuffer *)SHMNew(
	    sound_ptr->buffer_length * sizeof(SoundBuffer),
	    &shm_id
	);
	sound_ptr->buffer_shm_id = shm_id;
#else
	sound_ptr->buffer = (SoundBuffer *)calloc(
	    sound_ptr->buffer_length,
	    sizeof(SoundBuffer)
	);
#endif	/* YSHM_SUPPORT */
	if(sound_ptr->buffer == NULL)
	{
	    YSoundShutdown(recorder);
	    return(-1);
	}

	/* Allocate Sound buffer shared with the actual DSP device. */
	sound_ptr->device_buffer = (SoundBuffer *)calloc(  
	    sound_ptr->device_buffer_length,
	    sizeof(SoundBuffer)
	);
	if(sound_ptr->device_buffer == NULL)
	{
	    YSoundShutdown(recorder);
	    return(-1);
	}


	/* Get sound name. */
/*
	status = ioctl(
	    audio_ptr->fd, SOUND_MIXER_INFO, &mixer_info_buf
	);
	if(status != -1)
	{
	    free(audio_ptr->sound_name);
	    audio_ptr->sound_name = strdup(mixer_info_buf.name);
	}
 */
	synth_info_buf.device = 0;
	status = ioctl(
	    audio_ptr->fd, SNDCTL_SYNTH_INFO, &synth_info_buf
	);
	if(status != -1)
	{
	    free(audio_ptr->sound_name);
	    audio_ptr->sound_name = strdup(synth_info_buf.name);
	}


	/* Caliberate cycle (only if cycle is to be set by program). */
	if(audio_ptr->cycle_set == CYCLE_SET_PROGRAM)
	    YSoundCaliberateCycle(recorder);

/*
fprintf(stderr, "Cycle: %i.%i (comp %i.%i)  Buffer: %i bytes\n",
 audio_ptr->cycle.ms,
 audio_ptr->cycle.us,
 audio_ptr->compensated_cycle.ms,
 audio_ptr->compensated_cycle.us,
 sound_ptr->device_buffer_length
);

fprintf(stderr, "Channels: %i  Bits: %i  Sample Rate: %i\n",
 audio_ptr->channels,
 audio_ptr->sample_size,
 audio_ptr->sample_rate
);   
*/


	return(0);
}

/*
 *	Shells out the recorder, by closing all device descriptors.
 *
 *	This simulates the recorder being active but in reality
 *	any operations to the actual recorder device(s) compoents
 *	will not be performed. This fools the clients into thinking
 *	that the sound objects are still being played.
 *
 *	To reinitialize the recorder again, you need to call
 *	YSoundShutdown() and then YSoundInit().
 */
int YSoundShellOut(Recorder *recorder)
{
	Audio *audio_ptr;


	if(recorder == NULL)
	    return(-1);

	audio_ptr = &recorder->audio;


	/* Close DSP device. */
	if(audio_ptr->fd > -1)
	{
	    close(audio_ptr->fd);
	    audio_ptr->fd = -1;
	}

	audio_ptr->shelled_out = True;

	/* Leave mixer device up. */

/* Not sure what to do about the MIDI device. */


	return(0);
}

/*
 *	Attempts to caliberate the right audio.cycle and
 *	audio.compensated_cycle for the sound device.
 *
 *	Consider this (program calculating cycle).
 */
void YSoundCaliberateCycle(Recorder *recorder)
{
#define TOTAL_SYNC_PASSES	4

	int i, fd;
	time_t prev_delta_u, delta_u;

	Sound *sound_ptr;
	Audio *audio_ptr;
	YTime start_time, end_time;

	SoundBuffer *buf;
	YDataLength buf_len;


	if(recorder == NULL)
	    return;

	/* Get pointers to substructures. */
	sound_ptr = &recorder->sound; 
	audio_ptr = &recorder->audio; 

	/* Not allow program to set cycle? */
	if(audio_ptr->cycle_set != CYCLE_SET_PROGRAM)
	    return;

	if(sound_ptr->device_buffer == NULL)
	    return;
	if(recorder->audio.fd < 0)
	    return;

	fd = recorder->audio.fd;
	buf = sound_ptr->device_buffer;
	buf_len = sound_ptr->device_buffer_length;


	/* Reset actual buffer for sound device. */
	memset(buf, (SoundBuffer)0x00, buf_len);


	/* ****************************************************** */
	/* Begin syncing. */

	/*   Sync in case audio is still playing.  Otherwise
	 *   the average cycle calculated is garbage.
	 */
	ioctl(recorder->audio.fd, SNDCTL_DSP_SYNC, 0);

	/*   Write and sync a couple of times, get average cycle
	 *   value.
	 */
	GetCurrentTime(&start_time);
	write(fd, buf, buf_len);
	ioctl(recorder->audio.fd, SNDCTL_DSP_SYNC, 0);
	GetCurrentTime(&end_time);

	delta_u = MAX(((end_time.ms * 1000) + end_time.us) -
		      ((start_time.ms * 1000) + start_time.us), 0
		  );
	prev_delta_u = delta_u;

	for(i = 0; i < TOTAL_SYNC_PASSES; i++)
	{
	    GetCurrentTime(&start_time);
	    write(fd, buf, buf_len);
	    ioctl(recorder->audio.fd, SNDCTL_DSP_SYNC, 0);
	    GetCurrentTime(&end_time);

	    delta_u = MAX(((end_time.ms * 1000) + end_time.us) -
			  ((start_time.ms * 1000) + start_time.us), 0
		      );

	    /* Average it. */
	    delta_u = (prev_delta_u + delta_u) / 2;
	    prev_delta_u = delta_u;
	}

	audio_ptr->cycle.ms = delta_u / 1000;
	audio_ptr->cycle.us = delta_u % 1000;

	audio_ptr->compensated_cycle.ms = audio_ptr->cycle.ms;
	audio_ptr->compensated_cycle.us = audio_ptr->cycle.us;
}


/*
 *	Copys the midway buffer to the sound buffer and attempts
 *	to have the sound device play it.  Afterwards the midway
 *	buffer's contents are reset.
 */
int YSoundPlayBuffer(Recorder *recorder)
{
	int bytes_written;
	Sound *sound_ptr;
	Audio *audio_ptr;


	if(recorder == NULL)
	    return(-1);

	/* Get pointers to substructures. */
	sound_ptr = &recorder->sound;
	audio_ptr = &recorder->audio;


	/* Check if DSP device is opened. If it is not, then still
	 * return 0 because it may need to indicate it is shelled
	 * out.
	 */
	if(audio_ptr->fd < 0)
	    return(0);

	/* Make sure buffers are allocated. */
	if((sound_ptr->buffer == NULL) ||
	   (sound_ptr->device_buffer == NULL)
	)
	    return(-1);

	/* **************************************************** */
	/* Copy buffer to device_buffer. */

	memcpy(
	    sound_ptr->device_buffer,		/* target. */
	    sound_ptr->buffer,			/* source. */
	    MIN(sound_ptr->buffer_length,
	        sound_ptr->device_buffer_length
	    )
	);

	/* Write to audio device. */
	bytes_written = write(
	    audio_ptr->fd,
	    sound_ptr->device_buffer,
	    sound_ptr->device_buffer_length
	);


	/* Clear midway buffer. */
	memset(
	    sound_ptr->buffer,
	    (SoundBuffer)0x00,
	    sound_ptr->buffer_length
	);


	return(0);
}

/*
 *	Syncronizes recorder with process, then resets cycle ahead
 *	left value in recorder's Audio.
 */
void YSoundSync(Recorder *recorder, int options)
{
	Audio *audio_ptr;


	if(recorder == NULL)
	    return;

	audio_ptr = &recorder->audio;


	/* Tell DSP device to sync if it's opened. */
	if(audio_ptr->fd > -1)
	{
	    /* Sync and end any current playback or recording for
	     * the DSP device.
	     */
	    ioctl(audio_ptr->fd, SNDCTL_DSP_SYNC, 0);
	}

	/* Reset cycle ahead time, so writes to the DSP
	 * device are performed more frequently for a while.
	 */
	audio_ptr->cycle_ahead_left.ms = audio_ptr->write_ahead.ms;
	audio_ptr->cycle_ahead_left.us = audio_ptr->write_ahead.us;

/*
fprintf(stderr,
 "Sync: Cyclc ahead reset to %ld ms\n",
 (long)((audio_ptr->cycle_ahead_left.ms * 1000) +
 audio_ptr->cycle_ahead_left.us)
);
 */
}

/*
 *	Shuts down the recorder, deallocating any of its
 *	allocated resources.
 */
void YSoundShutdown(Recorder *recorder)
{
	Sound *sound_ptr;
	Audio *audio_ptr;
	MIDIAudio *midi_audio_ptr;


	if(recorder == NULL)
	    return;


	/* Get pointers to substructures. */
	sound_ptr = &recorder->sound;
	audio_ptr = &recorder->audio;
	midi_audio_ptr = &recorder->audio.midi_audio;


	/* Shutdown MIDI IO resources. */
	MIDIIOWShutdown(midi_audio_ptr);

	/* Shutdown audio CD context. */
	AudioCDShutdown(audio_ptr->audiocd_context);
	audio_ptr->audiocd_context = NULL;


	/* Close recorder's audio device as needed. */
	if(audio_ptr->fd > -1)
	{
	    /* Wait for sound to finish playing. */
	    ioctl(audio_ptr->fd, SNDCTL_DSP_SYNC, 0);

	    close(audio_ptr->fd);
	    audio_ptr->fd = -1;
	}

	/* Close recorder's mixer as needed. */
	if(audio_ptr->mixer_fd > -1)
	{
	    close(audio_ptr->mixer_fd);
	    audio_ptr->mixer_fd = -1;
	}


	/* Free allocated substructures. */
#ifdef YSHM_SUPPORT
	SHMUnref(sound_ptr->buffer);
#else
	free(sound_ptr->buffer);
#endif
	sound_ptr->buffer = NULL;
	sound_ptr->buffer_length = 0;
	sound_ptr->buffer_content_length = 0;

	free(sound_ptr->device_buffer);
	sound_ptr->device_buffer = NULL;
	sound_ptr->device_buffer_length = 0;
	sound_ptr->device_buffer_content_length = 0;

	memset(&audio_ptr->cycle, 0, sizeof(YDeltaTime));
	memset(&audio_ptr->compensated_cycle, 0, sizeof(YDeltaTime));

	memset(&audio_ptr->write_ahead, 0, sizeof(YDeltaTime));
	memset(&audio_ptr->cycle_ahead_left, 0, sizeof(YDeltaTime));
	memset(&audio_ptr->cumulative_latency, 0, sizeof(YDeltaTime));

	audio_ptr->sample_size = 0;
	audio_ptr->channels = 0;
	audio_ptr->sample_rate = 0;
	audio_ptr->bytes_per_second = 0;

#ifdef OSS_BUFFRAG
	audio_ptr->allow_fragments = False;
	audio_ptr->num_fragments = 0;
	audio_ptr->fragment_size = 0;
#endif /* OSS_BUFFRAG */

	audio_ptr->flip_stereo = False;
	free(audio_ptr->audio_mode_name);
	audio_ptr->audio_mode_name = NULL;
	audio_ptr->fd = -1;
	audio_ptr->mixer_fd = -1;
	free(audio_ptr->sound_name);
	audio_ptr->sound_name = NULL;
	audio_ptr->shelled_out = False;
}
