/*
 * IMPLEMENTATION NOTES.
 * 
 * WAV files use the RIFF format.
 * 
 * A RIFF formatted file contains one or more chunks.
 * 
 * A chunk contains an header with type and size, and a body:
 * 
 * - The chunk type as a readable ASCII string (4 bytes).
 * - The length of the following body section (unsigned 4 bytes, little-endian).
 * - The body section.
 * 
 * A WAV file contains a single chunk of type "RIFF"; its body section contains
 * the string "WAVE" and the inner chunks up to the end of the file.
 * There are several types of chunks, but those we support here are only the
 * "fmt " chunk containing the sampling parameters, and the "data" chunks
 * containing the actual sampled data. All numbers are assumed in little-endian
 * ordering, including the sampled audio data.
 * 
 * This implementation requires exactly one "fmt " chunk be present.
 * This implementation allows zero or more "data" chunks.
 * This implementation ignores and skips any other type of chunk.
 * 
 * For more details about the internal structure of each chunk type, see the
 * code below.
 */


#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <string.h>

#include "memory.h"
#include "error.h"

#define wav_IMPORT
#include "wav.h"

#ifdef WINNT

/** Converts x from LE byte ordering to host byte ordering. */
static uint16_t le16toh(uint16_t x)
{
	if( *(uint16_t *) "AB" == 0x4241 ){
		return x;
	} else {
		char s[2];
		s[0] = x & 255;
		s[1] = (x >> 8) & 255;
		return *(uint16_t *) s;
	}
}

/** Converts x from LE byte ordering to host byte ordering. */
static uint32_t le32toh(uint32_t x)
{
	if( *(uint32_t *) "ABCD" == 0x44434241 ){
		return x;
	} else {
		char s[4];
		s[0] = x & 255;
		s[1] = (x >> 8) & 255;
		s[2] = (x >> 16) & 255;
		s[3] = (x >> 24) & 255;
		return *(uint32_t *) s;
	}
}
	
#else
	#include <endian.h>
#endif

typedef struct {
	/** Zero-terminated RIFF chunk type. */
	char id[5];
	/** Length of the following data section (header bytes NOT included). */
	uint32_t length;
} wav_chunkHdr;


static void wav_skipBytes(char *fn, FILE *f, int no_bytes)
{
	if( fseek(f, no_bytes, SEEK_CUR) != 0 )
		error_system("failed skipping %d bytes on file %s", no_bytes, fn);
}


static void wav_readBytes(char *fn, FILE *f, int no_bytes, void *bytes)
{
	if( no_bytes < 0 )
		error_internal("trying to read %d bytes: undefined", no_bytes);
	if( no_bytes == 0 )
		return;
	int n = fread(bytes, no_bytes, 1, f);
	if( n == 1 )
		return;
	if( feof(f) )
		error_external("premature end of the file reading %s", fn);
	else
		error_external("unexpected error reading %s", fn);
}


static uint16_t wav_readUInt16LE(char *fn, FILE *f)
{
	uint16_t x;
	wav_readBytes(fn, f, 2, &x);
	return le16toh(x);
}


static uint32_t wav_readUInt32LE(char *fn, FILE *f)
{
	uint32_t x;
	wav_readBytes(fn, f, 4, &x);
	return le32toh(x);
}


static int wav_readChunkHdr(char *fn, FILE *f, wav_chunkHdr *hdr)
{
	int n = fread(&hdr->id, 1, 4, f);
	if( n < 4 ){
		if( feof(f) ){
			if( n == 0 )
				return 0;
			else
				error_external("unexpected end of the file reading %s", fn);
		} else {
			error_external("unexpected error reading %s", fn);
		}
	}
	hdr->id[4] = 0;
	hdr->length = wav_readUInt32LE(fn, f);
	return 1;
}


static void wav_destruct(void *p)
{
	wav_Type *this = p;
	memory_dispose(this->filename);
	memory_dispose(this->data);
}


wav_Type * wav_fromFile(char *filename)
{
	wav_Type *this = memory_allocate(sizeof(wav_Type), wav_destruct);
	this->filename = memory_strdup(filename);
	
	FILE *f = fopen(filename, "rb");
	if( f == NULL )
		error_system("failed opening file %s", filename);

	// Read 'RIFF' chunk.
	wav_chunkHdr hdr;
	if( ! wav_readChunkHdr(filename, f, &hdr) )
		error_external("%s: not a WAV file, too short", filename);
	if( strcmp(hdr.id, "RIFF") != 0 )
		error_external("%s: not a WAV file, missing 'RIFF' chunk", filename);
	if( hdr.length == 0 )
		error_external("%s: WAV file is empty, no audio samples at all", filename);
	
	char WAVE[5];
	wav_readBytes(filename, f, 4, WAVE);
	WAVE[4] = 0;
	if( strcmp(WAVE, "WAVE") != 0 )
		error_external("%s: not a WAV file, missing 'WAVE'", filename);
	
	// Allocate audio data buffer estimating a length of file size - 8 bytes.
	uint32_t capacity = hdr.length;
	this->data = memory_allocate(capacity, NULL);
	this->data_len = 0;
	
	// Scan all chunks, parsing format and data only:
	int fmtChunkFound = 0;
	while( wav_readChunkHdr(filename, f, &hdr) ){
		
		//printf("WAV %s: found '%s' chunk, %d bytes\n", fn, hdr.id, hdr.length);
		
		if( strcmp(hdr.id, "fmt ") == 0 ){
			if( fmtChunkFound )
				error_external("%s: bad WAV format, multiple 'fmt ' chunks", filename);
			fmtChunkFound = 1;
			if( hdr.length != 16 )
				error_external("%s: bad WAV format, invalid length of the 'fmt ' chunk: %d", filename, hdr.length);
			this->wFormatTag      = wav_readUInt16LE(filename, f);
			this->nChannels       = wav_readUInt16LE(filename, f);
			this->nSamplesPerSec  = wav_readUInt32LE(filename, f);
			this->nAvgBytesPerSec = wav_readUInt32LE(filename, f);
			this->nBlockAlign     = wav_readUInt16LE(filename, f);
			this->wBitsPerSample  = wav_readUInt16LE(filename, f);
			
		} else if( strcmp(hdr.id, "data") == 0 ){
			if( hdr.length > capacity - this->data_len )
				error_external("%s: bad WAV format: file contains more data than stated in the header", filename);
			wav_readBytes(filename, f, hdr.length, this->data + this->data_len);
			this->data_len += hdr.length;
			
		} else {
			// Unknown or unsupported RIFF chunk. Skip.
			wav_skipBytes(filename, f, hdr.length);
		}
		
	}
	fclose(f);
	
	if( ! fmtChunkFound )
		error_external("%s: bad WAV format, missing 'fmt ' chunk", filename);
	
	if( this->wFormatTag != 1 )
		error_external("%s: unknown or unsupported format tag in WAV file (possibly compressed?): %d",
			filename, this->wFormatTag);
	
	this->frameLength = this->nChannels * this->nBlockAlign;
	
	if( this->nChannels == 0 )
		error_external("%s: bad format, zero channels", filename);
	
	if( this->nSamplesPerSec == 0 )
		error_external("%s: bad format, zero samples per seconds!", filename);
	
	if( this->nBlockAlign == 0  )
		error_external("%s: bad format, zero bytes of block alignment!", filename);
	
	if( this->wBitsPerSample == 0  )
		error_external("%s: bad format, zero bits per sample!", filename);
	
	if( 8 * this->nBlockAlign < this->wBitsPerSample )
		error_external("%s: bad format, blocks alignment too small for samples length", filename);
	
	// Data must contain an integral number of frames, each frame being a sample
	// for all the channels. If not, silently trim the odd trailing bytes.
	uint32_t odd_trail_length = this->data_len % this->frameLength;
	if( odd_trail_length > 0 )
		this->data_len -= odd_trail_length;
	
	// Note that checks above prevent division by zero below.
	if( this->data_len % this->nBlockAlign != 0 )
		error_external("%s: bad WAV format, odd sample block alignment", filename);
	
	return this;
}
