/***************************************************************************
 *   Copyright (C) 2007 by Sergio Pistone                                  *
 *   sergio_pistone@yahoo.com.ar                                           *
 *                                                                         *
 *   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 "waveencoder.h"

#define WAVE_HEADER_SIZE 44
#define PCM_BUFFER_SIZE  0x10000 // 64KB
//#define PCM_BUFFER_SIZE  0x100000 // 1MB

#define MAX( a, b ) ((a) > (b) ? (a) : (b))
#define MIN( a, b ) ((a) < (b) ? (a) : (b))

WaveEncoder::WaveEncoder( std::string path, bool verbose ):
	m_path( path ),
	m_verbose( verbose ),
	m_file( 0 ),
	m_bytesWritten( 0 )
{
	m_buffer = new unsigned char[PCM_BUFFER_SIZE];
}

WaveEncoder::~WaveEncoder()
{
	delete [] m_buffer;
}

std::string WaveEncoder::getPath()
{
	return m_path;
}

void WaveEncoder::setPath( std::string path )
{
	m_path = path;
}

bool WaveEncoder::stdoutMode()
{
	return m_path.empty();
}

void WaveEncoder::setVerbose( bool verbose )
{
	m_verbose = verbose;
}

bool WaveEncoder::isVerbose()
{
	return m_verbose;
}

bool WaveEncoder::open( Input& input )
{
	if ( ! input.open() )
		return false;

	m_file = stdoutMode() ? stdout : fopen( m_path.c_str(), "wb+" );
	if ( ! m_file )
	{
		input.close();
		return false;
	}

	if ( stdoutMode() )
		// Write the header with the received (and possibly inaccurate) length
		writeHeader( input );
	else
	{
		// Reserve WAVE_HEADER_SIZE bytes of space for the WAVE header
		unsigned char headerFiller[WAVE_HEADER_SIZE];
		fwrite( headerFiller, 1, WAVE_HEADER_SIZE, m_file );
	}

	return true;
}

bool WaveEncoder::close( Input& input )
{
	if ( ! stdoutMode() )
	{
		fseek( m_file, 0, SEEK_SET );
		writeHeader( input );
		fclose( m_file );
	}

	m_file = 0;
	m_bytesWritten = 0;

	input.close();

	return true;
}

bool WaveEncoder::play( Input& input )
{
	if ( ! open( input ) )
		return false;

	float length = input.getTotalTime() / 100.0;
	FILE* msgFile = stdoutMode() ? stderr : stdout;
	if ( m_verbose )
	{
		if ( length > 0 )
			fprintf( msgFile, "Duration: %.1f secs\n", length/10 );
		else
			fprintf( msgFile, "Duration: unknown\n" );
		fprintf(
			msgFile, "Channels: %d\nSample rate: %d Hz\nBits per sample: %d bits\n",
			input.getChannels(),
			input.getSampleRate(),
			input.getBitsPerSample()
		);
	}

	unsigned long chunkSize;
	bool moreData;
	do
	{
		moreData = input.getPCMChunk( m_buffer, PCM_BUFFER_SIZE, chunkSize );

		if ( chunkSize > 0 )
		{
			fwrite( m_buffer, chunkSize, 1, m_file );
			m_bytesWritten += chunkSize;
		}

		if ( m_verbose )
		{
			if ( length > 0 )
				fprintf( msgFile, "\rProgress: %.1f%%", input.getCurrentTime() / length );
			else
				fprintf( msgFile, "\rProgress: %.1f secs", input.getCurrentTime() / 1000.0 );
			fflush( stdout );
		}
	}
	while ( moreData );

	if ( m_verbose )
		fprintf( msgFile, "\n" );

	close( input );

	return true;
}

void WaveEncoder::writeHeader( Input& input )
{
	unsigned long bitsPerSample = input.getBitsPerSample();
	unsigned long channels = input.getChannels();
	unsigned long sampleRate = input.getSampleRate();
	unsigned long blockAlign = input.getBlockAlign();

	unsigned long byteRate = sampleRate * blockAlign;
	unsigned long samples = stdoutMode() ? (unsigned long)(input.getTotalTime()/1000.0 * sampleRate) : m_bytesWritten/blockAlign;
	unsigned long subchunk2Size = samples * blockAlign;

	// RIFF header
	fwrite( "RIFF", 1, 4, m_file );			// ChunkID
	writeNumber( 36 + subchunk2Size, 4 );	// chunkSize
	fwrite( "WAVE", 1, 4, m_file );			// Format

	// fmt sub-chunk
	fwrite( "fmt ", 1, 4, m_file );			// Subchunk1ID
	writeNumber( 16, 4 );					// Subchunk1Size
	writeNumber( 1, 2 );					// AudioFormat
	writeNumber( channels, 2 );				// NumChannels
	writeNumber( sampleRate, 4 );			// SampleRate
	writeNumber( byteRate, 4 );				// ByteRate
	writeNumber( blockAlign, 2 );			// BlockAlign
	writeNumber( bitsPerSample, 2 );		// BitsPerSample

	// data sub-chunk
	fwrite( "data", 1, 4, m_file );			// Subchunk2ID
	writeNumber( subchunk2Size, 4 );		// Subchunk1Size
}

void WaveEncoder::writeNumber( unsigned long number, unsigned long writeSize, bool littleEndian )
{
	unsigned char* writeData = new unsigned char[writeSize];
	int idx, idx2, divisor;

	// get the little endian number representation
	for ( idx = writeSize-1; idx >= 0 ; --idx )
	{
		divisor = 1;
		for ( idx2 = 0; idx2 < idx; ++idx2 )
			divisor *= 256;
		writeData[idx] = (unsigned char)(number / divisor);
		number = number - (divisor*(number%divisor));
	}

	if ( ! littleEndian )
	{
		unsigned char aux;
		for ( unsigned int idx = 0; idx < writeSize; ++idx )
		{
			aux = writeData[writeSize-idx];
			writeData[writeSize-idx] = writeData[idx];
			writeData[idx] = aux;
		}
	}

	fwrite( writeData, 1, writeSize, m_file );

	delete[] writeData;
}
