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

#include "wav.h"


static void WavReportError(
	const char *filename, const char *reason, int how_bad
);

void WavDestroyData(wav_data_struct *wd);

static FILE *WavOpenFile(
	const char *filename, const char *mode, off_t *size_rtn
);

int WavIsFPWav(FILE *fp);
int WavIsFileWav(const char *filename);
int WavReadHeader(const char *filename, FILE *fp, wav_data_struct *wd);
int WavReadPartialData(
	wav_data_struct *wd,
	long offset,
	long max_chunk_size,
	int read_opt
);


#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))


/*
 *	Error reporting routine.
 */
static void WavReportError(
	const char *filename, const char *reason, int how_bad
)
{
	if(filename != NULL)
	{
	    fprintf(stderr, "%s: ", filename);
	}
	if(reason != NULL)
	{
	    fprintf(stderr, "%s (Error level %i)", reason, how_bad);
	}
	fprintf(stderr, "\n");


	return;
}

/*
 *	Resets all members in the wd structure, deallocating any
 *	resources and closing any opened files.
 */
void WavDestroyData(wav_data_struct *wd)
{
	if(wd == NULL)
	    return;

	/* Filename. */
	free(wd->filename);
	wd->filename = NULL;

	if(wd->fp != NULL)
	{
	    fclose(wd->fp);
	    wd->fp = NULL;
	}

	/* Format reading stuff for internal functions only. */
	wd->format_tag = 0;

	/* Header and parms. */
	wd->header_pos = 0;
	wd->header_len = 0;
	wd->channels = 0;
	wd->samples_per_sec = 0;
	wd->bytes_per_sec = 0;
	wd->block_align = 0;

	/* Format specific fields. */
	wd->bits_per_sample = 0;
	wd->samples_per_block = 0;
	wd->coefficients = 0;
	wd->comp_type = 0;
	wd->revision = 0;

	wd->data_starting_pos = 0;
	wd->data_chunk_size = 0;

	free(wd->data);
	wd->data = NULL;
	wd->data_len = 0;

	return;
}

/*
 *	Procedure to open a file and fetch its size.
 */
static FILE *WavOpenFile(
	const char *filename, const char *mode, off_t *size_rtn
)
{
	FILE *fp;
	struct stat stat_buf;


	if(filename == NULL)
	{
	    fprintf(stderr, "Cannot open file with no name.\n");
	    *size_rtn = 0;
	    return(NULL);
	}
	if(mode == NULL)
	{
	    fprintf(stderr, "%s: Open mode not givin.\n", filename);
	    *size_rtn = 0;
	    return(NULL);
	}

	if(stat(filename, &stat_buf))
	{
	    fprintf(stderr, "%s: No such file.\n", filename);
	    (*size_rtn) = 0;
	    return(NULL);
	}
	(*size_rtn) = stat_buf.st_size;

	fp = fopen(filename, mode);
	if(fp == NULL)
	{
	    fprintf(stderr, "%s: Cannot open.\n", filename);
	    (*size_rtn) = 0;
	    return(NULL);
	}

	return(fp);
}

/*
 *	Checks if the given wav file specified by fp really
 *	is a wav file by rewinding it and reading data at the fp.
 *
 *	Returns WavSuccess if it is or other value if it is not.
 */
int WavIsFPWav(FILE *fp)
{
	u_int32_t uix;
	u_int32_t internal_filesize;


	if(fp == NULL)
	    return(WavErrorNoAccess);

	/* Rewind to beginning of file. */
	rewind(fp);

	/* Check for RIFF ID code. */
	fread(&uix, 1, 4, fp);
	if(uix != WavIDRIFF)
	    return(WavErrorNotWave);

	/* Internal filesize. */
	fread(&internal_filesize, 1, 4, fp);
	internal_filesize += ftell(fp);

	/* Check for Wave ID code. */
	fread(&uix, 1, 4, fp);
	if(uix != WavIDWave)
	    return(WavErrorNotWave);

	/* All checks passed, this is a wav file. */
	return(WavSuccess); 
}

/*
 *      Checks if the given wav file specified by filename really
 *      is a wav file by opening it and reading it.
 *
 *      Returns WavSuccess if it is or other value if it is not.
 */
int WavIsFileWav(const char *filename)
{
	int status;
	FILE *fp;
	off_t filesize;


	/* Open file. */
	fp = WavOpenFile(filename, "rb", &filesize);
	if(fp == NULL)
	    return(WavErrorNoAccess);
	if(filesize == 0)
	{
	    fclose(fp);
	    return(WavErrorNoAccess);
	}

	/* Check if file is a wav file. */
	status = WavIsFPWav(fp);

	/* Close file. */
	fclose(fp);

	return(status);
}

/*
 *	Reads the header from the stream fp and initializes the given
 *	wd structure if fp is valid and a wav file.
 *
 *	The given filename is used only for referance purposes, it can be
 *	NULL.
 *
 *	If WavSuccess is returned then the given fp will be transfered
 *	to the wd structure and should not be referanced again. For all
 *	other return values, the calling function is responsible for
 *	closing the given fp.
 */
int WavReadHeader(const char *filename, FILE *fp, wav_data_struct *wd)
{
	int chunk_count;
	u_int16_t usx;
	u_int32_t uix;
	u_int32_t nextseek;
	u_int32_t ckid;
	u_int32_t cksize;
	u_int32_t ckpos;

	char idstr[5];
	u_int16_t extra_info_size;

	struct stat stat_buf;
	off_t filesize;
	u_int32_t internal_filesize;	/* Size indicated in file data. */


	/* Error checks. */
	if(wd == NULL)
	    return(WavErrorNoBuffers);
	if(fp == NULL)
	    return(WavErrorBadValue);

	/* Rewind to beginning of wav file. */
	rewind(fp);


	/* Reset values. */
	memset(wd, 0x00, sizeof(wav_data_struct));

	/* Get size of file. */
	if(fstat(fileno(fp), &stat_buf))
	    return(WavErrorNoAccess);

	filesize = stat_buf.st_size;
	if(filesize == 0)
	    return(WavErrorNoAccess);

	/* Is this a wav file? Check for RIFF ID code. */
	fread(&uix, 1, 4, fp);
	if(uix != WavIDRIFF)
	    return(WavErrorNotWave);

	/* Get internal filesize. */
	fread(&internal_filesize, 1, 4, fp);
	internal_filesize += ftell(fp);

	/* Check for Wave ID code. */
	fread(&uix, 1, 4, fp);
	if(uix != WavIDWave)
	    return(WavErrorNotWave);


	/* Begin reading wav file header. */

	/* Record file name. */
	if(filename != NULL)
	    wd->filename = strdup(filename);


	/* Begin reading chunks. */

	nextseek = ftell(fp);
	chunk_count = 0;
	while(nextseek < internal_filesize)
	{
	    fseek(fp, nextseek, SEEK_SET);
	    fread(&ckid, 1, 4, fp);
	    fread(&cksize, 1, 4, fp);
	    ckpos = ftell(fp);
	    nextseek = cksize + ckpos;
	    memcpy(idstr, (void *)&ckid, 4);
	    idstr[4] = '\0';

	    /* Check chunk ID. */
	    switch(ckid)
	    {
	      /* Format (header?) chunk. */
	      case WavFormatChunkCode:

		/* Set header length. */
		wd->header_pos = ckpos;
		wd->header_len = cksize;

		/* Get format tag. */
		fread(&usx, 1, 2, fp);
		wd->format_tag = usx;

		/* Get channels. */
		fread(&usx, 1, 2, fp);
		wd->channels = usx;

		/* Get samples per second. */
		fread(&uix, 1, 4, fp);
		wd->samples_per_sec = uix;

		/* Get average bytes per second. */
		fread(&uix, 1, 4, fp);
		wd->bytes_per_sec = uix;

		/* Get block alignment. */
		fread(&usx, 1, 2, fp);
		wd->block_align = usx;


		/* Format specific fields. */

		/* Get bits per second. */
		fread(&usx, 1, 2, fp);
		wd->bits_per_sample = usx;

		if(wd->format_tag != 0x0001)
		{
		    /* WARNING: THIS IS ALL EXPERIMENTAL!!! */

		    /* Get size of extra info. */
		    fread(&usx, 1, 2, fp);
		    extra_info_size = usx;

		    switch(wd->format_tag)
		    {
		      case 0x0002:    /* MS ADPCM??? */
			/* Get samples per block. */
			fread(&usx, 1, 2, fp);
			wd->samples_per_block = usx;

			/* Get number of coefficients. */
			fread(&usx, 1, 2, fp);
			wd->coefficients = usx;
/* Need to write code to get each coefficient. */

			break;

		      case 0x0011: /* WAVE_FORMAT_DVI_ADPCM??? */
			/* Assuming same as WAVE_FORMAT_DSPGROUP_TRUESPEECH. */

		      case 0x0022: /* WAVE_FORMAT_DSPGROUP_TRUESPEECH */
			/* Get samples per block. */
			fread(&usx, 1, 2, fp);
			wd->samples_per_block = usx;

			break;

		      case 0x0021:  /* WAVE_FORMAT_SONARC */
			/* Get compression type. */
			fread(&usx, 1, 2, fp);
			wd->comp_type = usx;

			break;
		      case 0x0200: /* WAVE_FORMAT_CREATIVE_ADPCM */
			/* Get revision. */
			fread(&usx, 1, 2, fp);
			wd->revision = usx;

			break;

		    }
		}    /* End format specific fields. */
		break;


	      /* Data chunk. */
	      case WavDataChunkCode:
		/* Record data chunk position and size. */
		wd->data_starting_pos = (off_t)ckpos;
		wd->data_chunk_size = (off_t)cksize;
		break;


	      /* Unknown chunk. */
	      default:
/*
		fprintf(stderr,
		    "%s: Chunk %i: Unknown chunk ID 0x%.8x.\n",
		    filename, chunk_count, ckid
		);
 */
		break;

	    }        /* End check chunk ID. */

	    /* Increment chunk count. */
	    chunk_count++;

	}	/* End reading chunks. */


	/* Transfer given fp to the wd structure, the calling
	 * function should not referance it again since we are returning
	 * WavSuccess.
	 */
	wd->fp = fp;

	return(WavSuccess);
}

/*
 *	Reads a segment of data from the wav file reffered to
 *	by the filename member on the given wd structure (which
 *	should have been initialized wuth a prior call to
 *	WavReadHeader().
 *
 *	File will be opened as needed if the fp member is NULL.
 *
 *	The member data and data_len will be reallocated as needed
 *	if the requested max_chunk_size is different than wd->data_len.
 */
int WavReadPartialData(
	wav_data_struct *wd,
	long offset,            /* In bytes. */
	long max_chunk_size,    /* In bytes. */
	int read_opt		/* Reading format. */
)
{
	int i;
	char *buf_ptr;


	if(wd == NULL)
	    return(WavErrorBadValue);
	if(offset < 0)
	    return(WavErrorBadValue);

	/* Read nothing? */
	if(max_chunk_size <= 0)
	{
	    free(wd->data);
	    wd->data = NULL;
	    wd->data_len = 0;

	    return(WavSuccess);
	}

	/* Sanitize offset and max_chunk_size. */
	if(offset >= wd->data_chunk_size)
	{
	    return(WavErrorEndOfData);
	}
	if((offset + max_chunk_size) > wd->data_chunk_size)
	{
	    /* Sanitize max_chunk_size. */
	    max_chunk_size = wd->data_chunk_size - offset;
	}
	if(max_chunk_size <= 0)
	{
	    return(WavErrorEndOfData);
	}


	/* Open file as needed. */
	if(wd->fp == NULL)
	{
	    off_t filesize;

	    if(wd->filename == NULL)
		return(WavErrorBadValue);

	    wd->fp = WavOpenFile(wd->filename, "rb", &filesize);
	    if(wd->fp == NULL)
		return(WavErrorNoAccess);
	    if(filesize == 0)
		return(WavErrorNoAccess);
	}


	/* Reallocate buffer for data (as needed). */
	if(wd->data_len != max_chunk_size)
	{
	    wd->data_len = max_chunk_size;

	    wd->data = (char *)realloc(
		wd->data,
		wd->data_len * sizeof(char)
	    );
	}
	if(wd->data == NULL)
	{
	    wd->data_len = 0;

	    fclose(wd->fp);
	    wd->fp = NULL;

	    return(WavErrorNoBuffers);
	}

	/* Set file pointer position. */
	fseek(wd->fp, wd->data_starting_pos + offset, SEEK_SET);

	/* Read the data from file, the wav data format is in
	 * unsigned 8 values.
	 */
	buf_ptr = wd->data;
	switch(read_opt)
	{
	  /* Shift byte value by - 128 to make it signed 8. */
	  case WavReadSigned8:
	    for(i = 0; i < max_chunk_size; i++)
		*buf_ptr++ = (char)(fgetc(wd->fp) - 128);
	    break;

	  /* Read data as is (unsigned 8). */
	  default:
	    for(i = 0; i < max_chunk_size; i++)
		*buf_ptr++ = (char)(fgetc(wd->fp));
	    break;
	}

	return(WavSuccess);
}
