// Game_Music_Emu https://bitbucket.org/mpyne/game-music-emu/

#include "Nsfe_Emu.h"

#include "blargg_endian.h"
#include <string.h>
#include <ctype.h>
#include <algorithm>

/* Copyright (C) 2005-2006 Shay Green. This module is free software; you
can redistribute it and/or modify it under the terms of the GNU Lesser
General Public License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version. This
module 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 Lesser General Public License for more
details. You should have received a copy of the GNU Lesser General Public
License along with this module; if not, write to the Free Software Foundation,
Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */

#include "blargg_source.h"

using std::min;
using std::max;

Nsfe_Info::Nsfe_Info() { playlist_disabled = false; }

Nsfe_Info::~Nsfe_Info() { }

inline void Nsfe_Info::unload()
{
	track_name_data.clear();
	track_names.clear();
	playlist.clear();
	track_times.clear();
}

// TODO: if no playlist, treat as if there is a playlist that is just 1,2,3,4,5... ?
void Nsfe_Info::disable_playlist( bool b )
{
	playlist_disabled = b;
	info.track_count = playlist.size();
	if ( !info.track_count || playlist_disabled )
		info.track_count = actual_track_count_;
}

int Nsfe_Info::remap_track( int track ) const
{
	if ( !playlist_disabled && (unsigned) track < playlist.size() )
		track = playlist [track];
	return track;
}

// Read multiple strings and separate into individual strings
static blargg_err_t read_strs( Data_Reader& in, long size, blargg_vector<char>& chars,
		blargg_vector<const char*>& strs )
{
	RETURN_ERR( chars.resize( size + 1 ) );
	chars [size] = 0; // in case last string doesn't have terminator
	RETURN_ERR( in.read( &chars [0], size ) );

	RETURN_ERR( strs.resize( 128 ) );
	int count = 0;
	for ( int i = 0; i < size; i++ )
	{
		if ( (int) strs.size() <= count )
			RETURN_ERR( strs.resize( count * 2 ) );
		strs [count++] = &chars [i];
		while ( i < size && chars [i] )
			i++;
	}

	return strs.resize( count );
}

// Copy in to out, where out has out_max characters allocated. Truncate to
// out_max - 1 characters.
static void copy_str( const char* in, char* out, int out_max )
{
	out [out_max - 1] = 0;
	strncpy( out, in, out_max - 1 );
}

struct nsfe_info_t
{
	byte load_addr [2];
	byte init_addr [2];
	byte play_addr [2];
	byte speed_flags;
	byte chip_flags;
	byte track_count;
	byte first_track;
	byte unused [6];
};

blargg_err_t Nsfe_Info::load( Data_Reader& in, Nsf_Emu* nsf_emu )
{
	int const nsfe_info_size = 16;
	blaarg_static_assert( offsetof (nsfe_info_t,unused [6]) == nsfe_info_size, "NSFE Info header layout incorrect!" );

	// check header
	byte signature [4];
	blargg_err_t err = in.read( signature, sizeof signature );
	if ( err )
		return (err == in.eof_error ? gme_wrong_file_type : err);
	if ( memcmp( signature, "NSFE", 4 ) )
		return gme_wrong_file_type;

	// free previous info
	track_name_data.clear();
	track_names.clear();
	playlist.clear();
	track_times.clear();

	// default nsf header
	static const Nsf_Emu::header_t base_header =
	{
		{'N','E','S','M','\x1A'},// tag
		1,                  // version
		1, 1,               // track count, first track
		{0,0},{0,0},{0,0},  // addresses
		"","","",           // strings
		{0x1A, 0x41},       // NTSC rate
		{0,0,0,0,0,0,0,0},  // banks
		{0x20, 0x4E},       // PAL rate
		0, 0,               // flags
		{0,0,0,0}           // unused
	};
	Nsf_Emu::header_t& header = info;
	header = base_header;

	// parse tags
	int phase = 0;
	while ( phase != 3 )
	{
		// read size and tag
		byte block_header [2] [4];
		RETURN_ERR( in.read( block_header, sizeof block_header ) );
		blargg_long size = get_le32( block_header [0] );
		blargg_long tag  = get_le32( block_header [1] );

		if ( size < 0 )
			return "Corrupt file";

		//debug_printf( "tag: %c%c%c%c\n", char(tag), char(tag>>8), char(tag>>16), char(tag>>24) );

		switch ( tag )
		{
			case BLARGG_4CHAR('O','F','N','I'): {
				check( phase == 0 );
				if ( size < 8 )
					return "Corrupt file";

				nsfe_info_t finfo;
				finfo.track_count = 1;
				finfo.first_track = 0;

				RETURN_ERR( in.read( &finfo, min( size, (blargg_long) nsfe_info_size ) ) );
				if ( size > nsfe_info_size )
					RETURN_ERR( in.skip( size - nsfe_info_size ) );
				phase = 1;
				info.speed_flags = finfo.speed_flags;
				info.chip_flags  = finfo.chip_flags;
				info.track_count = finfo.track_count;
				this->actual_track_count_ = finfo.track_count;
				info.first_track = finfo.first_track;
				memcpy( info.load_addr, finfo.load_addr, 2 * 3 );
				break;
			}

			case BLARGG_4CHAR('K','N','A','B'):
				if ( size > (int) sizeof info.banks )
					return "Corrupt file";
				RETURN_ERR( in.read( info.banks, size ) );
				break;

			case BLARGG_4CHAR('h','t','u','a'): {
				blargg_vector<char> chars;
				blargg_vector<const char*> strs;
				RETURN_ERR( read_strs( in, size, chars, strs ) );
				int n = strs.size();

				if ( n > 3 )
					copy_str( strs [3], info.dumper, sizeof info.dumper );

				if ( n > 2 )
					copy_str( strs [2], info.copyright, sizeof info.copyright );

				if ( n > 1 )
					copy_str( strs [1], info.author, sizeof info.author );

				if ( n > 0 )
					copy_str( strs [0], info.game, sizeof info.game );

				break;
			}

			case BLARGG_4CHAR('e','m','i','t'):
				RETURN_ERR( track_times.resize( size / 4 ) );
				RETURN_ERR( in.read( track_times.begin(), track_times.size() * 4 ) );
				break;

			case BLARGG_4CHAR('l','b','l','t'):
				RETURN_ERR( read_strs( in, size, track_name_data, track_names ) );
				break;

			case BLARGG_4CHAR('t','s','l','p'):
				RETURN_ERR( playlist.resize( size ) );
				RETURN_ERR( in.read( &playlist [0], size ) );
				break;

			case BLARGG_4CHAR('A','T','A','D'): {
				check( phase == 1 );
				phase = 2;
				if ( !nsf_emu )
				{
					RETURN_ERR( in.skip( size ) );
				}
				else
				{
					Subset_Reader sub( &in, size ); // limit emu to nsf data
					Remaining_Reader rem( &header, Nsf_Emu::header_size, &sub );
					RETURN_ERR( nsf_emu->load( rem ) );
					check( rem.remain() == 0 );
				}
				break;
			}

			case BLARGG_4CHAR('D','N','E','N'):
				check( phase == 2 );
				phase = 3;
				break;

			default:
				// tags that can be skipped start with a lowercase character
				check( islower( (tag >> 24) & 0xFF ) );
				RETURN_ERR( in.skip( size ) );
				break;
		}
	}

	return 0;
}

blargg_err_t Nsfe_Info::track_info_( track_info_t* out, int track ) const
{
	int remapped = remap_track( track );
	if ( (unsigned) remapped < track_times.size() )
	{
		long length = (int32_t) get_le32( track_times [remapped] );
		if ( length > 0 )
			out->length = length;
	}
	if ( (unsigned) remapped < track_names.size() )
		Gme_File::copy_field_( out->song, track_names [remapped] );

	GME_COPY_FIELD( info, out, game );
	GME_COPY_FIELD( info, out, author );
	GME_COPY_FIELD( info, out, copyright );
	GME_COPY_FIELD( info, out, dumper );
	return 0;
}

Nsfe_Emu::Nsfe_Emu()
{
	loading = false;
	set_type( gme_nsfe_type );
}

Nsfe_Emu::~Nsfe_Emu() { }

void Nsfe_Emu::unload()
{
	if ( !loading )
		info.unload(); // TODO: extremely hacky!
	Nsf_Emu::unload();
}

blargg_err_t Nsfe_Emu::track_info_( track_info_t* out, int track ) const
{
	return info.track_info_( out, track );
}

struct Nsfe_File : Gme_Info_
{
	Nsfe_Info info;

	Nsfe_File() { set_type( gme_nsfe_type ); }

	blargg_err_t load_( Data_Reader& in )
	{
		RETURN_ERR( info.load( in, 0 ) );
		info.disable_playlist( false );
		set_track_count( info.info.track_count );
		return 0;
	}

	blargg_err_t track_info_( track_info_t* out, int track ) const
	{
		return info.track_info_( out, track );
	}
};

static Music_Emu* new_nsfe_emu () { return BLARGG_NEW Nsfe_Emu ; }
static Music_Emu* new_nsfe_file() { return BLARGG_NEW Nsfe_File; }

static gme_type_t_ const gme_nsfe_type_ = { "Nintendo NES", 0, &new_nsfe_emu, &new_nsfe_file, "NSFE", 1 };
extern gme_type_t const gme_nsfe_type = &gme_nsfe_type_;


blargg_err_t Nsfe_Emu::load_( Data_Reader& in )
{
	if ( loading )
		return Nsf_Emu::load_( in );

	// TODO: this hacky recursion-avoidance could have subtle problems
	loading = true;
	blargg_err_t err = info.load( in, this );
	loading = false;
	disable_playlist( false );
	return err;
}

void Nsfe_Emu::disable_playlist( bool b )
{
	info.disable_playlist( b );
	set_track_count( info.info.track_count );
}

void Nsfe_Emu::clear_playlist_()
{
	disable_playlist();
	Nsf_Emu::clear_playlist_();
}

blargg_err_t Nsfe_Emu::start_track_( int track )
{
	return Nsf_Emu::start_track_( info.remap_track( track ) );
}
