//
// Author: Andy Goodwin
// contact: andy@thesett.co.uk
//
//
// All code from www.thesett.co.uk may NOT be sold for any
// financial gain by any organisation with out prior agreement.
// Any modifications made must acknowledge the author.
//
// All code is unsupported and the author accepts no responsibility
// for any damage caused to any application or data held on any storage media.
//
//
// This application reads the given .DDF file used by Musicmatch Jukebox.
// It generates an .M3U format output to the console or file if specified
// The generated.M3U playlist file can be used as an import to iTunes
//
//
// Acknowledgement must go to the creator of the web page
//
// http://www.in2words.org/projects/MMJBLib/ddfparse.html
// contact: tim@in2words.org
//
//
// for the invaluable information on the format of the DDF file format
//
// Also thanks to the creator of the web page
//
// http://hanna.pyxidis.org/tech/m3u.html
// contact: xunker@pyxidis.org
//
// for the format of the M3U file format
//
//

#include <memory.h>
#include <string.h>
#include <stdio.h>

//
// Define the meaning of each field in a record
//

enum
{
	SONG = 0 ,
	ALBUM ,
	ARTIST ,
	GENRE ,
	TEMPO ,
	MOOD ,
	SITUATION ,
	PREFERENCE ,
	FIELD8_UNKNOWN ,
	FILE_PATH ,
	FIELD10_UNKNOWN ,
	TRACK ,
	LENGTH ,
	BITRATE ,
	URL_TO_ARTIST ,
	URL_TO_BUY_CD ,
	URL_TO_AUDIO ,
	RELEASE_YEAR ,
	FILE_DATE_TIME ,
	VOLUME_LEVELED ,
	FILE_FORMAT ,
	GUID ,

	MAX_FIELDS							// Always last in list
} ;

//
// Now define a class for text storage
// I didn't use the CString supplied by Microsoft so
// the code is totally portable
//

class CField
{
public:
	CField() ;
	~CField() ;

	Assign( const char *pszText, int nLen ) ;
	const char *Get() const { return m_pszBuffer ; }


private:
	char	*m_pszBuffer ;				// Pointer to string
	int		m_nLen ;					// Length of current string
	int		m_nMax ;					// Maximum length of string
} ;

//
// Constructor
//

CField::CField()
{
	m_pszBuffer = NULL ;				// No Memory allocated
	m_nLen = 0 ;						// Zero Length
	m_nMax = 0 ;						// Ditto
}

//
// Destructor
//

CField::~CField()
{
	if ( m_pszBuffer )					// Any memory allocation ?
		delete m_pszBuffer ;			// Yes
}

//
// Assign a new character string
//

CField::Assign( const char *pszText, int nLen )
{
	if ( nLen >= m_nMax )				// Fits in ?
	{									// No
		if ( m_pszBuffer )				// allocated ?
			delete m_pszBuffer ;		// Yes free

		if ( nLen < 128 )				// Too small ?
			m_nMax = 128 ;				// Yes - optimise allocation
		else
			m_nMax = nLen + 1 ;			// Use as it's bigger

		m_pszBuffer = new char[ m_nMax ] ;

	}

	m_nLen = nLen ;						// Setup new length

	memcpy( m_pszBuffer, pszText, nLen ) ;
	*( m_pszBuffer + nLen ) = '\0' ;	// Null terminate for ease of use
}

int main(int argc, char* argv[])
{
	if ( argc < 2 )						// Any parameters ?
	{									// No
		printf( "MM2M3U ddf_file <m3u_file>\n" ) ;
		return 1 ;
	}

	FILE *fpIn = fopen( argv[1] , "rb" ) ;

	if ( fpIn == NULL )					// Input file ok ?
	{									// No
		printf( "Can not open ddf file %s\n", argv[1] ) ;
		return 2 ;
	}

	char szBuffer[ 2048 ] ;

	//
	// Read the header info
	//

	if ( fread( szBuffer, 1024, 1, fpIn ) != 1 )
	{
		printf( "Invalid ddf file %s\n", argv[1]  ) ;
		return 3 ;
	}

	int nRecordSeparator = 0 ;

	if ( memcmp( szBuffer, "8.", 2 ) == 0 )
		nRecordSeparator = 176 ;		// Version 8.x of DDF file
	else
	{
		if ( memcmp( szBuffer, "7.", 2 ) == 0 )
			nRecordSeparator = 160 ;	// Version 7.x of DDF file
	}

	if ( nRecordSeparator == 0 )		// Known DDF file format ?
	{									// No
		printf( "Unknown ddf file format\n" ) ;
		return 4 ;
	}

	CField fields[ MAX_FIELDS ] ;		// All the fields of a DDF record

	bool bError = false ;				// No errors

	int nRecord = 0 ;
	int nField = 0 ;

	FILE *fpOut = stdout ;

	if ( argc >= 3 )									// Output .m3u ?
	{													// Yes
		fpOut = fopen( argv[2] , "wt" ) ;

		if ( fpOut == NULL )							// Ok ?
		{												// No
			printf( "Error oping m3u_file %s\n", argv[2] ) ;
			fclose( fpIn ) ;
			return 5 ;
		}
	}

	fprintf( fpOut, "#EXTM3U\n" ) ;

	//
	// Now for each record with in the DDF file
	//

	while ( !bError && fread( szBuffer, 4, 1, fpIn ) == 1 )
	{
		nRecord++ ;										// One more record

		for ( int i = 0 ; !bError && i < MAX_FIELDS ; i++ )
		{
			nField = i + 1 ;							// Processing field

			switch ( i )								// type of field
			{
			case LENGTH :
				//
				// The length field is 9 bytes holding a value
				// in milliseconds.
				//

				if ( fread( szBuffer, 9, 1, fpIn ) != 1 )
					bError = true ;
				else
				{
					unsigned long ulMs = 0 ;

					char *pPtr = szBuffer ;

					int nShift = 0 ;

					//
					// Process just the first 4 bytes
					// as this fits into an unsigned long
					//
					// Besides to use the 5th byte
					// the song would have to be over 49 days long!
					//

					for ( int j = 0 ; j < 4 ; j++ )
					{
						ulMs = ulMs + ( ( ( unsigned long )*pPtr++ & 0xff ) << nShift ) ;
						nShift += 8 ;
					}

					//
					// Convert to seconds as the .M3U format always
					// has the duration in seconds
					//

					sprintf( szBuffer, "%lu", ulMs / 1000 ) ;

					fields[ LENGTH ].Assign( szBuffer, strlen( szBuffer ) ) ;
				}
				break ;

			case BITRATE :
				//
				// The bit rate field consists of 3 bytes
				//

				if ( fread( szBuffer, 3, 1, fpIn ) != 1 )
					bError = true ;
				else
				{
					unsigned long ulRate = 0 ;

					char *pPtr = szBuffer ;

					int nShift = 0 ;

					for ( int j = 0 ; j < 3 ; j++ )
					{
						ulRate = ulRate + ( ( ( unsigned long )*pPtr++ & 0xff ) << nShift ) ;
						nShift += 8 ;
					}

					//
					// Scale the bit rate for the correct value
					//

					sprintf( szBuffer, "%lu", ( ulRate << 8 ) ) ;

					fields[ BITRATE ].Assign( szBuffer, strlen( szBuffer ) ) ;
				}
				break ;

			default :
				//
				// All other fields consist of a 4 byte length
				// followed by unicode characters
				//

				if ( fread( szBuffer, 4, 1, fpIn ) != 1 )
					bError = true ;
				else
				{
					//
					// Just use the first two bytes
					// Who's going to have a field description over
					// 64Kbytes long ?
					//
					// In fact the length is restricted to the length
					// of the buffer szBuffer, which is currently 2K
					//

					int nX = ( ( int )szBuffer[0] & 0xff ) +
							 ( ( ( int )szBuffer[1] & 0xff ) << 8 ) ;

					if ( nX )
					{
						if ( nX > sizeof( szBuffer ) ||
							 fread( szBuffer, nX, 1, fpIn ) != 1 )
							bError = true ;
						else
						{
							//
							// OK this isn't a genuine unicode to non-unicode
							// routine.
							// I apologise to all unicode users of musicmatch.
							//
							// English always has the 2nd byte as zero
							//

							char *pPtr = szBuffer+1 ;

							for ( int j = 2 ; j < nX - 3 ; j += 2 )
								*pPtr++ = szBuffer[ j ] ;

							fields[ i ].Assign( szBuffer, ( ( nX - 3 ) >> 1 ) + 1 ) ;
						}
					}
				}
				break ;
			}
		}

		if ( !bError )
		{
			//
			// The Length will always have a string assigned
			//
			fprintf( fpOut, "#EXTINF:%s,", fields[ LENGTH ].Get() ) ;

			const char *pSong = fields[ SONG ].Get() ;

			if ( pSong == NULL )					// Any Song title ?
				fprintf( fpOut, "No Title\n" ) ;	// No
			else
				fprintf( fpOut, "%s\n", fields[ SONG ].Get() ) ;

			// File path must have an entry as MusicMatch added the file
			// unless the database is corrupted

			fprintf( fpOut, "%s\n", fields[ FILE_PATH ].Get() ) ;
			nField = 0 ;

			//
			// Now read the record separator
			//

			if ( fread( szBuffer, nRecordSeparator, 1, fpIn ) != 1 )
				break ;
		}
	}

	if ( bError )
		printf( "Error in ddf file: Record %d, Field %d\n", nRecord, nField ) ;


	fclose( fpIn ) ;

	if ( fpOut != stdout )							// Output file ?
		fclose( fpOut ) ;							// Yes - close it 

	return 0;
}

