#include "tags.h"
#include "tagprivate.h"
#include <string.h>
#include <stdlib.h>
#include <assert.h>
#include <ctype.h>
#include "crc_blam.h"
#include "byte_swapping.h"
#include "strings.h"
#include <XStringUtil.h>
#include <XError.h>
#include "mesh.h"



static byte_swap_code tag_file_header_bs_codes[]=
{
	_begin_bs_array, 1,

	// short type;
	// word pad;
	_2byte,
	_2byte,

	// char name[TAG_FILE_NAME_LENGTH+1]; // taken from the filename, like local tags
	// char url[TAG_FILE_URL_LENGTH+1];
	TAG_FILE_NAME_LENGTH+1,
	TAG_FILE_URL_LENGTH+1,

	// short entry_point_count;
	// short tag_count;
	// unsigned long checksum; 
	// unsigned long flags;
	// long size; // of entire file, including this header (zero until header is rewritten)
	_2byte,
	_2byte,
	_4byte,
	_4byte,
	_4byte,

	// unsigned long header_checksum; // with this field set to zero
	_4byte,

	// unsigned long unused[1];
	sizeof(unsigned long),

	// tag signature;
	_4byte,

	_end_bs_array,
};

static byte_swap_code tag_file_entry_point_bs_codes[]=
{
	_begin_bs_array, 1,

	// tag subgroup_tag;
	_4byte,
	
	_4byte,

	// long unused[2];
	1*sizeof(long),

	// unsigned long user_data;
	// char name[TAG_FILE_NAME_LENGTH+1];
	_4byte,
	TAG_FILE_NAME_LENGTH+1,

	// char printed_name[ENTRY_POINT_NAME_LENGTH+1];
	ENTRY_POINT_NAME_LENGTH+1,

	_end_bs_array
};

tag_file *tag_file_list;

static long get_file_length( FILE* fp )
{
	assert( fp );
	long cur_pos = ftell(fp);
	fseek(fp, 0, SEEK_END);
	long end_pos = ftell(fp);
	fseek(fp, cur_pos, SEEK_SET);
	return end_pos;
}

tag_type_info *typeinfo( tag type )
{
	tag_type_info *t = tagtypelist;
	for (;;) {
		if (t->type == type) return t;
		if (t->type == 0) return NULL;
		++t;
	}
}

static bool tag_is_entry_point( tag_entry* e )
{
	assert( e && e->group );
	
	if(e->group->type != 'mesh')
		return FALSE;
	
	return TRUE;
}

static bool get_entry_point_display_name( tag_entry* e, char* display_name )
{
	tag_entry* stli_entry;
	const void* stli_data;
	unsigned stli_length;
	mesh_definition header;
		
	assert( e && e->group );
	
	if( !tag_is_entry_point( e ) )
		return FALSE;
	
//	if( !read_mesh_definition( &header, e->id ) )
	if( !read_mesh_definition( &header, e ) )
		return FALSE;
	
	// now we have to go find the damn string resource that has the display name in it.
	stli_entry = get_entry( STRING_LIST_DEFINITION_GROUP_TAG, header.map_description_string_list_tag );
	if( !stli_entry )
		stli_data = 0;	
	else
	{
		get_entry_data( stli_entry, &stli_data, &stli_length );
		if( stli_length == 0 )
			stli_data = 0;
	}
	
	if( stli_data == 0 )
	{
		stli_data = e->name;
		stli_length = strlen(e->name);
		
		memset( display_name, 0, ENTRY_POINT_NAME_LENGTH + 1 );
		strcpy( display_name, "map name string not found" );
//		display_name[stli_length] = 0;
//		strcat(display_name, "\r\n");
	}
	else
	{
		memset( display_name, 0, ENTRY_POINT_NAME_LENGTH + 1 );
		stli_length = MIN( stli_length, ENTRY_POINT_NAME_LENGTH );
		strncpy( display_name, static_cast<const char*>(stli_data), stli_length );
		bool found_end = false;
		for (int i = 0; i < ENTRY_POINT_NAME_LENGTH+1; ++i)
		{
			if (display_name[i] == '\r' || display_name[i] == '\n' || display_name[i] == '\t' || display_name[i] == 0)
			{
				found_end = true;
			}
			if (found_end)
			{
				display_name[i] = 0;
			}
		}
	}
	
	return TRUE;
}

static tag_file_entry_point* build_entry_points( tag_file* file, int* num_entries )
{
	int i;
	tag_group* g;
	tag_entry* e;
	tag_file_entry_point* pts;
	
	assert( file );
	assert( num_entries );
	
	*num_entries = 0;
	
	g = get_group_in_file( file, 'mesh' );
	if( g == 0 )
		return 0;
	
	*num_entries = get_entry_count( g );
	if( *num_entries == 0 )
		return 0;
	
	pts = (tag_file_entry_point*)malloc(sizeof(tag_file_entry_point)*(*num_entries));
	if( pts == 0 )
	{
		*num_entries = 0;
		return 0;
	}
	
	memset( pts, 0, sizeof(tag_file_entry_point) * (*num_entries) );
	
	for( i=0, e = get_next_entry_in_group( g, 0 ); e; i++, e = get_next_entry_in_group( g, e ) )
	{
		assert(i < (*num_entries));
		pts[i].subgroup_tag = e->id;
		pts[i].user_data = 0;
		pts[i].team_count = ((const struct mesh_definition *)get_entry_data( e, NULL, NULL ))->team_count;
		byte_swap_memory_be( "team count", &pts[i].team_count, 1, _4byte );
		strcpy(pts[i].name, e->name);
		if( !get_entry_point_display_name( e, pts[i].printed_name ) )
		{
			*num_entries = 0;
			free(pts);
			return 0;
		}
	}
	
	return pts;
}

void copy_file_data_into_tag( tag_file* tf, XGFileSpecifier& fs )
{
	FILE* fp = fs.OpenFile("rb");
	long len = get_file_length( fp );
	if( len < 1 )
	{
		fclose(fp);
		return;
	}
	
	void* data = malloc(len);
	assert( data );
	
	long r = fread( data, 1, len, fp);
	fclose( fp );
	if( r != len )
	{
		free(data);
		return;
	}
	
	tag grp = 'rawd';
	
	//
	// hack for scott.
	//
	{
		char buff[6];
		fs.GetExtension(buff);
		if(!strcmp(buff, "jpg"))
			grp = 'jpeg';
	}
		
		
	char name[256];
	fs.GetName( name );
	assert( name[0] != 0 );
	tag id = new_tag_id_from_string( name, grp, 0 );
	assert( id );
	
	add_tag_entry( tf, grp, id, 0, name, 0, len, data, true );
}

bool tag_data_needs_update( char* string, tag_entry* e, void* struct_ptr, unsigned struct_length, unsigned struct_count, byte_swap_code* bs_codes, int size_must_match )
{
	const void* tag_data;
	unsigned tag_length;
	void* test_data;
	
	assert( string && e && struct_ptr && struct_length && struct_count && bs_codes );

	get_entry_data( e, &tag_data, &tag_length );
	if( !tag_data || !tag_length )
		return TRUE;

	if(size_must_match != 0)
	{	
		if( tag_length != (struct_length*struct_count) )
			return TRUE;
	}
	else
	{
		if(tag_length < (struct_length*struct_count) )
			return TRUE;
			
		tag_length = (struct_length*struct_count);
	}
	
	test_data = malloc(struct_length*struct_count);
	
	byte_swap_move_be( string, test_data, struct_ptr, struct_length, struct_count, bs_codes );
	int n = memcmp( test_data, tag_data, tag_length );
	free(test_data);
	
	return (n != 0) ? TRUE : FALSE;	
}

bool load_tag_data_into_struct( char* string, tag_entry* e, void* struct_ptr, unsigned struct_length, unsigned struct_count, byte_swap_code* bs_codes )
{
	const void* tag_data;
	unsigned tag_length;
	
//	assert( string && e && struct_ptr && struct_length && struct_count && bs_codes );
	assert( string && e && struct_ptr && struct_count && bs_codes );

	get_entry_data( e, &tag_data, &tag_length );
	if( (!tag_data || !tag_length) && struct_length )
		return FALSE;
	
	if( tag_length < (struct_length*struct_count) )
		return FALSE;
	
	byte_swap_move_be( string, struct_ptr, const_cast<void*>(tag_data), struct_length, struct_count, bs_codes );
	return TRUE;
}

bool save_tag_data_from_struct( char* string, tag_entry* e, void* struct_ptr, unsigned struct_length, unsigned struct_count, byte_swap_code* bs_codes )
{
	const void* tag_data;
	unsigned tag_length;
	void* new_data;
	unsigned new_length;
	unsigned default_tag_size;
		
	assert( string && e && struct_ptr && struct_length && struct_count && bs_codes );

	get_entry_data( e, &tag_data, &tag_length );
	if( !tag_data || !tag_length )
		return FALSE;
	
	// preserve old tag data.
	default_tag_size = struct_length*struct_count;
	new_length = ( tag_length < default_tag_size ) ? default_tag_size : tag_length;
	
	new_data = malloc(new_length);
	if(new_data == 0)
		return FALSE;
		
	if( tag_length > default_tag_size )
		memcpy( new_data, tag_data, tag_length );
	
	byte_swap_move_be( string, new_data, struct_ptr, struct_length, struct_count, bs_codes );
	set_entry_data( e, new_data, new_length, TRUE );
	return TRUE;
}

int get_group_count(tag_file* f, tag type_filter)
{
	tag_group* g;
	int count=0;
	
	for( g = get_next_group_in_file( f, 0 ); g; g = get_next_group_in_file( f, g ) )
	 count += (type_filter != 0) ? g->type == type_filter : 1;
	
	return count;
}

int get_entry_count(tag_group* g, tag id_filter) // id_filter == 0 for all.
{
	tag_entry*  e;
	int count=0;
	
	for( e = get_next_entry_in_group( g, 0 ); e; e = get_next_entry_in_group( g, e ) )
		count += (id_filter != 0) ? e->id == id_filter : 1;
		
	return count;
}

void tag_type_to_string( tag type, char* string)
{
	strcpy( string, tagstr(type) );
}

tag tag_type_from_string( const char *string )
{
	unsigned char s[4];
	strncpy( (char *)s, string, 4 );
	for (int i = 0; i < 4; ++i)
	{
		if (s[i] < 32 || s[i] > 127) s[i] = '_';
	}
	tag id = (s[0] << 24) + (s[1] << 16) + (s[2] << 8) + (s[3] << 0);
	if (id == '____') id = -1;
	return id;
}

tag get_group_type( tag_group* g)
{
	if(g == 0)
		return 0;
	return g->type;
}

tag_group *get_group_in_file( tag_file *f, tag t, bool create )
{
	if (!f) return 0;
	tag_group *g;
	for (g = f->groups; g; g = g->next)
	{
		if (g->type == t) return g;
	}
	
	if (!create) return NULL;
	
	g = new tag_group;
	g->entries = 0;
	g->type = t;
	g->file = f;
	g->info = typeinfo( t );
	LLADD( f->groups, g );
	
	return g;
}


tag_entry *get_entry( tag type, tag id )
{
	for (tag_file *f = tag_file_list; f; f = f->next) {
		tag_group *g = get_group_in_file( f, type );
		if (g) {
			tag_entry *e = get_entry_in_group( g, id );
			if (e) return e;
		}
	}
	return NULL;
}


tag_entry *get_entry_in_group( tag_group *group, tag id )
{
	if (!group) return 0;
	
	tag_entry *e = group->entries;
	while (e != 0 && e->id != id) {
		e = e->next;
	}
	return e;
}


tag_entry *get_entry_in_file( tag_file *f, tag type, tag id )
{
	return get_entry_in_group( get_group_in_file( f, type, false ), id );
}


tag_file *get_next_tag_file( tag_file *f )
{
	return f ? f->next : tag_file_list;
}


tag_group* get_next_group_in_file(tag_file* f, tag_group* g)
{
	if(g == 0)
	{
		if(f == 0)
			return 0;
		else
			return f->groups;
	}
	
	return g->next;
}

tag_group* get_prev_group_in_file(tag_file* f, tag_group* g)
{
	if(g == 0)
	{
		if(f == 0)
			return 0;
		else
		{
			tag_group* g;
			g = f->groups;
			if(g == 0)
				return 0;
			while(g->next) { g = g->next; }
			return g;
		}
	}
	
	return g->prev;
}

tag_entry* get_next_entry_in_group(tag_group* g, tag_entry* e)
{
	if(e == 0)
	{
		if(g == 0)
			return 0;
		else
			return g->entries;
	}
	
	return e->next;
}

tag_entry* get_prev_entry_in_group(tag_group* g, tag_entry* e)
{
	if(e == 0)
	{
		if(g == 0)
			return 0;
		else
		{
			tag_entry* e;
			e = g->entries;
			if(e == 0)
				return 0;
			while(e->next) { e = e->next; }
			return e;
		}
	}
	
	return e->prev;
}

tag_entry* get_next_plugin_entry( tag_entry* e )
{
	tag_file *tf = tag_file_list;
	tag_group *tg = NULL;
	tag_entry *te = NULL;
	
	if( !e )
	{
		// find the first entry
		while( tf && !tf->groups )
		{
			tf = tf->next;
		}
		
		return tf->groups->entries;
	}
	else
	{
		if( !e->next )
		{
			if( !e->group->next )
			{
				if( !e->file->next )
				{
					// we're at the end
					return NULL;
				}
				else
				{
					// return the first entry in the next file
					assert( e->file->next->groups );
					assert( e->file->next->groups->entries );
					return e->file->next->groups->entries;
				}
			}
			else
			{
				// return the first entry in the next group in this file
				assert( e->group->next->entries );
				return e->group->next->entries;
			}
		}
		else
		{
			// return the next entry in this group in this file
			return e->next;
		}
	}
}

char* get_entry_name(tag_entry* e)
{
	if(e == 0)
		return 0;
	return e->name;
}

int set_entry_name(tag_entry* e, char* name)
{
	int l;
	
	if(name == 0)
		return 0;
	l = strlen(name);
	if(l >= TAG_NAME_LENGTH-1)
		l = TAG_NAME_LENGTH;
	if(e == 0)
		return 0;
	strncpy(e->name, name, l);
	e->name[l] = 0; // terminate it.
	
	if(e->file)
	{
		e->file->needrewrite = true;
		e->file->needupdate = true;
	}
	
	announce_tag_change(e);
		
	return l;
}

void set_entry_group(tag_entry* e, tag id)
{
	tag_group* g = get_group_in_file( e->file, id, true );
	if( e->group == g )
		return;
	if( e->group )
	{
		LLDEL( e->group->entries, e );
	}
	
	e->group = g;
	LLADD( g->entries, e );
	announce_tag_change( e );
}

tag_group* get_entry_group(tag_entry* e)
{
	if(e == 0)
		return 0;
	assert(e && e->file && e->group && e->file == e->group->file );
	return e->group;
}

tag_file *get_file( tag type, tag id )
{
	tag_entry *e = get_entry( type, id );
	
	if( !e )
		return 0;
		
	return get_entry_file( e );
}

tag_file *get_entry_file( tag_entry *e )
{
	if (e == 0) return 0;
	assert(e && e->file && e->group && e->file == e->group->file );
	return e->file;
}

tag_file *get_group_file( tag_group *g )
{
	if (g == 0) return 0;
	assert( g && g->file && (!g->entries || g->entries->file == g->file) );
	return g->file;
}

tag get_entry_type( tag_entry *e )
{
	if (e == 0 || e->group == 0) return 0;
	assert(e && e->file && e->group && e->file == e->group->file );
	return e->group->type;
}

tag get_entry_id(tag_entry* e)
{
	if(e == 0)
		return 0;
		
	assert(e && e->file && e->group && e->file == e->group->file );
	return e->id;
}

void set_entry_id(tag_entry* e, tag id)
{
	if(e == 0)
		return;
		
	assert(e && e->file && e->group && e->file == e->group->file );
	if (e->id == id) return;
	announce_tag_change( e );
	e->id = id;
	announce_tag_change( e );
	if(e->file)
	{
		e->file->needrewrite = true;
		e->file->needupdate  = true;
	}
}

short get_entry_version( tag_entry* e )
{
	if(e == 0)
		return -1;
	
	assert(e && e->file && e->group && e->file == e->group->file );
	return e->version;
}

void set_entry_version( tag_entry* e, short version)
{
	if(e == 0)
		return;
		
	assert(e && e->file && e->group && e->file == e->group->file );
	if (e->version == version) return;
	
	e->version = version;
	if(e->file)
	{
		e->file->needupdate = true;
		e->file->needrewrite = true;
	}
	
	announce_tag_change( e );
}

tag_entry *add_tag_entry( tag_file *f, tag group, tag id, short version, const char *name,
					unsigned offset, unsigned length, const void *data, bool becomeowner )
{
	tag_group *g = get_group_in_file( f, group, true );
	if (!g) return 0;
	
	tag_entry *e = new tag_entry;
	char buffname[256];
	
	if (!name) {
		sprintf( buffname, "Untitled %s", get_singular_name_for_type( group ) );
		name = buffname;
	}
	if (!id) {
		id = new_tag_id_from_string( name, group, 0 );
	}
	
	e->file = f;
	e->group = g;
	e->id = id;
	e->offset = offset;
	e->length = length;
	e->data = data;
	e->form = 0;
	e->version = version;
	//assert( offset || data );
	//assert( !offset || !data );
	assert( data || !becomeowner );
	e->data = data;
	if (data && !becomeowner) {
		e->data = malloc( length );
		if (!e->data) {
			delete e;
			return 0;
		}
		memcpy( const_cast<void *>(e->data), data, length );
	}
	if (data || !offset) {
		e->dirty = true;
		f->needupdate = true;
		f->needrewrite = true;
	} else {
		e->dirty = false;
	}
	if (!name) name = tagstr( id );
	strncpy( e->name, name, TAG_NAME_LENGTH );
	e->name[TAG_NAME_LENGTH] = 0;
	LLADD( g->entries, e );
	
	announce_tag_change( e );
	
	return e;
}


void delete_tag_entry( tag_entry *e, bool delete_data )
{
	if (!e || !e->group || !e->file || e->file != e->group->file) return;
	
	tag type = get_entry_type(e);
	tag id = get_entry_id(e);
	
	tag_group *g = e->group;
	tag_file *f = e->file;
	LLDEL( g->entries, e );
	if (delete_data && e->data) free(const_cast<void *>(e->data));
	delete e;
	if (!g->entries) {
		LLDEL( f->groups, g );
		delete g;
	}
	f->needupdate = true;
	f->needrewrite = true;
	
	announce_tag_change( type, id );
}



inline unsigned int get_u4( FILE *f )
{
	int a = getc(f);
	int b = getc(f);
	int c = getc(f);
	int d = getc(f);
	return (a << 24) + (b << 16) + (c << 8) + (d << 0);
}

inline signed int get_s4( FILE *f )
{
	int a = getc(f);
	int b = getc(f);
	int c = getc(f);
	int d = getc(f);
	return (a << 24) + (b << 16) + (c << 8) + (d << 0);
}

inline unsigned short get_u2( FILE *f )
{
	int a = getc(f);
	int b = getc(f);
	return (a << 8) + b;
}

#define get_data( D, f ) fread( &D, sizeof(D), 1, f )

inline signed short get_s2( FILE *f )
{
	int a = getc(f);
	int b = getc(f);
	return (a << 8) + b;
}

inline void put4( FILE *f, unsigned u )
{
	int a = u >> 24;
	int b = u >> 16;
	int c = u >> 8;
	putc( a, f );
	putc( b, f );
	putc( c, f );
	putc( u, f );
}

inline void put2( FILE *f, unsigned u )
{
	int a = u >> 8;
	putc( a, f );
	putc( u, f );
}

#define put_data( D, f ) fwrite( &D, sizeof(D), 1, f )

inline void skip( int size, FILE *f )
{
	fseek( f, size, SEEK_CUR );
}

const char *tagstr( tag t )
{
	static char names[32][5];
	static int index = 0;
	
	index = (index + 1) & 31;
	names[index][0] = t >> 24;
	names[index][1] = t >> 16;
	names[index][2] = t >> 8;
	names[index][3] = t >> 0;
	names[index][4] = 0;
	for (int i = 0; i < 4; ++i) {
		if (names[index][i] == 0) names[index][i+1] = 0;
		if (!isprint(names[index][i])) names[index][i] = '_';
	}
	return names[index];
}

bool copy_tag_file_entries_to_another_file( tag_file* src_file, tag_file* dst_file )
{
	tag_group* g;
	tag_entry* e, *test;
	
	for(g = get_next_group_in_file( src_file, 0 ) ; g ; g = get_next_group_in_file( src_file, g ) )
	{
		for( e = get_next_entry_in_group( g, 0 ); e ; e = get_next_entry_in_group( g, e ) )
		{
			// duplicate it if not already present:
			test = get_entry_in_file( dst_file, get_entry_type(e), get_entry_id(e) );
			if(test == 0)
			{
				unsigned length;
				const void* data;
				
				get_entry_data( e, &data, &length);
				if(data != 0)
					add_tag_entry( dst_file, get_entry_type(e), get_entry_id(e), get_entry_version(e), get_entry_name(e), 0, length, data, FALSE );
			}
		}
	}
	
	return TRUE;
}

tag_file *get_tag_file( const XGFileSpecifier &fs, bool create )
{
	FILE *f;
	tag_file *tf;
	bool readonly = false;
	
	for (tag_file *tf = tag_file_list; tf; tf = tf->next) {
		if (tf->filespec && *(tf->filespec) == fs) {
			return tf;
		}
	}
	
	f = fs.OpenFile( "r+b" );
	if( !f )
	{
		f = fs.OpenFile( "rb" );
		if( f )
			readonly = true;
	}
	if( !f )
	{
		if( !create )
			return 0;
		
		tag_file *tf = new tag_file;
		tf->groups = 0;
		tf->filespec = new XGFileSpecifier(fs);
		tf->readonly = false;
		tf->needupdate = true;
		tf->needrewrite = true;
		tf->loaded_type = _tag_file_type_plugin;
		tf->patch_version = 0;
		tf->form = 0;
		LLADD( tag_file_list, tf );
		return tf;
	}
	
	tag_file_header fh;
	tf = new tag_file;
	tf->groups = 0;
	tf->filespec = new XGFileSpecifier(fs);
	tf->readonly = readonly;
	tf->needupdate = false;
	tf->needrewrite = false;
	tf->form = 0;
		
	rewind( f );
	
	fh.type = get_u2(f);
	fh.version = get_u2(f);
	get_data( fh.name, f );
	get_data( fh.url, f );
	fh.entry_point_count = get_u2(f);
	fh.tag_count = get_u2(f);
	fh.checksum = get_u4(f);
	fh.flags = get_u4(f);
	fh.size = get_u4(f);
	fh.header_checksum = get_u4(f);
	skip( 4, f );
	fh.signature = get_u4(f);
	
	if (fh.signature != 'dng2') {
		fseek( f, 0x3c, SEEK_SET );
		if (get_u4(f) == 'mth2') {
			fh.type = _tag_file_type_local;
			fh.signature = 'mth2';
			fh.entry_point_count = 0;
			fh.tag_count = 1;
			rewind( f );
		} else {
			fclose( f );
			delete tf;
			return 0;
		}
	}
	
	tf->loaded_type = fh.type;
	tf->patch_version = fh.version;
	
	skip( sizeof( tag_file_entry_point ) * fh.entry_point_count, f );
	
	for (int i = 0; i < fh.tag_count; ++i)
	{
		tag_header t;
		
		t.identifier = get_u2(f);
		t.flags = getc(f);
		t.type = getc(f);
		get_data( t.name, f );
		t.group_tag = get_u4(f);
		t.subgroup_tag = get_u4(f);
		t.offset = get_u4(f);
		t.size = get_u4(f);
		t.user_data = get_u4(f);
		t.version = get_u2(f);
		t.foundation_tag_file_index = getc(f);
		t.owner_index = getc(f);
		t.signature = get_u4(f);
		
		if(fh.signature == 'mth2')
		{
			assert(fh.tag_count == 1);
			t.size = get_file_length(f) - sizeof(tag_header);
		}
		
		add_tag_entry( tf, t.group_tag, t.subgroup_tag, t.version, t.name, t.offset, t.size, NULL, false );
	}
	
	fclose( f );
	
	LLADD( tag_file_list, tf );
	
	return tf;
}


void close_tag_file( tag_file *f )
{
	if (!f) return;
	
	while (f->groups)
	{
		assert( f->groups->file == f );
		assert( f->groups->entries );
		assert( f->groups->entries->group == f->groups && f->groups->entries->file == f );
		delete_tag_entry( f->groups->entries, true );
	}
	
	delete f->filespec;
	f->filespec = 0;
	
	assert( f->form == 0 );
	
//	delete f->form;
//	f->form = 0;
	
	LLDEL( tag_file_list, f );
	delete f;
}


tag_file *revert_tag_file( tag_file *f )
{
	XGFileSpecifier *spec = f->filespec;
	f->filespec = 0;
	close_tag_file( f );
	if (!spec) return 0;
	
	tag_file *newfile = get_tag_file( *spec, false );
	delete spec;
	return newfile;
}


tag_file *new_empty_tag_file()
{
	tag_file *tf = new tag_file;
	tf->groups = 0;
	tf->filespec = 0;
	tf->readonly = false;
	tf->needupdate = false;
	tf->needrewrite = false;
	tf->loaded_type = -1;
	tf->form = 0;
	tf->patch_version = 0;
	
	LLADD( tag_file_list, tf );
	return tf;
}


void scan_directory_for_tag_files( XGDirectory &dir, bool recurse )
{
	bool isdir;
	XGFileSpecifier fs;
	if (!dir.GetFirstFile( fs, &isdir )) return;
	do {
		if (isdir && recurse) {
			XGDirectory ndir(fs);
			scan_directory_for_tag_files( ndir, recurse );
		} else {
			/*char ext[4];
			fs.GetExtension( ext, 3 );
			if (!strcmp( ext, "tag" ))
			{
				get_tag_file( fs, false );
			}
			#if OPT_MACOS
			if (fs.GetType() == 'File')
			{
				get_tag_file( fs, false );
			}
			#endif*/
			get_tag_file(fs, false);	// will fail if not a tag file.
		}
	} while (dir.GetNextFile( fs, &isdir ));
	
	return;
}


TTagFileForm *get_file_form( tag_file *tf )
{
	if (!tf) return 0;
	return tf->form;
}


void set_file_form( tag_file *tf, TTagFileForm *form )
{
	if (!tf) return;
	assert( tf->form == 0 || form == 0 || tf->form == form );
	tf->form = form;
}


const void *get_entry_data( tag_entry *e, const void **data, unsigned *length )
{
	int size;
	assert(e && e->file && e->group && e->file == e->group->file );
	
	if (!e->data)
	{
		if (e->file && e->file->filespec) {
			FILE *f = e->file->filespec->OpenFile("rb");
			if (f)
			{
				e->data = malloc(e->length);
				if (e->data)
				{
					fseek( f, e->offset, SEEK_SET );
					size = fread( const_cast<void *>(e->data), 1, e->length, f );
					if( size != e->length )
					{
						free(const_cast<void*>(e->data));
						e->data = 0;
					}
				}
				fclose(f);
			}
		}
	}
	
	if (data)
	{
		*data = e->data;
	}
	
	if (length)
	{
		*length = e->length;
	}
	
	return e->data;
}


void release_entry_data( tag_entry *e )
{
	if (e->dirty || !(e->file && e->file->filespec) || !e->offset) return;
	if (e->data)
	{
		free(const_cast<void *>(e->data));
		e->data = 0;
	}
}


unsigned get_entry_size( tag_entry *e )
{
	assert(e && e->file && e->group && e->file == e->group->file );
	
	return e ? e->length : 0;
}

void set_entry_data( tag_entry *e, const void *data, unsigned length, bool becomeowner )
{
	assert( e && e->file && e->group );
	assert( data && data != e->data );
	if (e->data) {
		free(const_cast<void *>(e->data));
	}
	if (becomeowner) {
		e->data = const_cast<void *>(data);
		if (e->length != length) {
			e->length = length;
			e->file->needrewrite = true;
		}
		e->dirty = true;
		e->file->needupdate = true;
	} else {
		e->data = malloc(length);
		if (e->data) {
			memcpy( const_cast<void *>(e->data), data, length );
			if (e->length != length) {
				e->length = length;
				e->file->needrewrite = true;
			}
			e->dirty = true;
			e->file->needupdate = true;
		}
	}
	
	announce_tag_change( e );
}

TTagEditForm *get_entry_form( tag_entry *e )
{
	if (!e) return 0;
	return e->form;
}


void set_entry_form( tag_entry *e, TTagEditForm *form )
{
	if (!e) return;
	assert( e->form == 0 || form == 0 || e->form == form );
	e->form = form;
}


bool write_tags_to_file( XGFileSpecifier &fs, tag_file *f, bool update_internal, int tag_file_type )
{
	if( f->filespec && fs == *f->filespec )
		return false;
		
	if (f->loaded_type == _tag_file_type_patch || f->loaded_type == _tag_file_type_foundation)
	{
		return false;
	}
	
	int tagcount;
	int entry_point_count;
	unsigned size;
	tag_group *g;
	tag_entry *e;
	unsigned long checksum;
	tag_file_entry_point* entry_points;
	tag_file_header fh;
	
	FILE *out = fs.OpenFile( "wb" );
	if( !out )
		return false;

	// test this file for reading and writing as we shouldn't write a readonly file		
	FILE *in = 0;
	if( f->filespec )
	{
		char inName[256];
		char outName[256];

		f->filespec->GetName( inName );
		fs.GetName( outName );
		
		if( XGCompareStrings( inName, outName ) != 0 )
			in = f->filespec->OpenFile( "rb" );
		else
			in = f->filespec->OpenFile( "r+b" );

		if( !in )
		{
			f->readonly = true;
			fclose( out );
			return false;
		}
		
		// reopen this file in read only mode as there is a chance tag_get would want to open it as well during the save
		f->readonly = false;
		fclose( in );
		in = f->filespec->OpenFile( "rb" );
	}

	// count the tags and calculate the size of the data
	tagcount = 0;
	size = sizeof(tag_file_header);
	g = 0;
	while( (g = get_next_group_in_file(f,g)) != 0 )
	{
		e = 0;
		while( (e = get_next_entry_in_group(g,e)) != 0 )
		{
			++tagcount;
			size += sizeof(tag_header) + e->length;
		}
	}
	
	switch( tag_file_type )
	{
		case TAG_FILE_TYPE_AUTO:
		{
			XGDirectory dir;
			fs.GetDirectory( dir );
			char path_strings[1024];
			
			// phook - this is so f'ing cool, on my system ::SetCurrentDirectory( ".." ); when the current
			// path is c:\, returns true.  jeezus shit
			// I'm gonna set a hard limit of paths so I can build the beta tomorrow
			strcpy( path_strings, ":" );
			for( int ii = 0; ii < 30; ii++ )
			{
				dir.GetDirectoryName( path_strings + strlen(path_strings) );
				strcat( path_strings, ":" );

#if OPT_WINOS
				if( strlen( path_strings ) > (sizeof(path_strings) - 256) )
					break;
#endif

				if( !dir.ChangeDirectory( NULL ) )
					break;
			}
			
			// we need to determine the type of tag we're supposed to save
			if( strstr( path_strings, ":local:" ) )
			{
				if( tagcount == 1 )
				{
					// assume raw local tag
#if 0
					dir.ChangeDirectory( 0 );
					XGDirectory appdir;
					appdir.SetAppLocation();
					if( dir == appdir )
#endif
					{
						tag_file_type = TAG_FILE_TYPE_LOCAL;
						break;
					}
				}
			}
			else if( strstr( path_strings, ":tags:" ) )
			{
				// assume monolithic
#if 0
				dir.ChangeDirectory( 0 );
				XGDirectory appdir;
				appdir.SetAppLocation();
				if( dir == appdir )
#endif
				{
					tag_file_type = TAG_FILE_TYPE_MONOLITHIC;
				}
			}
			else
			{
				// assume plugin
				tag_file_type = TAG_FILE_TYPE_PLUGIN;
			}
		}

		break;
			
		case TAG_FILE_TYPE_LOCAL:
		{
			if( tagcount != 1 )
				return FALSE;
		}
		
		break;
			
		case TAG_FILE_TYPE_PLUGIN:
		{
		}
		
		break;
			
		case TAG_FILE_TYPE_MONOLITHIC:
		{
		}
		
		break;
	}
	
	assert( tag_file_type != TAG_FILE_TYPE_AUTO );
	
	// build entry points.
	if( tag_file_type != TAG_FILE_TYPE_LOCAL )
	{
		tag_group *tg;
		
		entry_points = build_entry_points( f, &entry_point_count );
		if( entry_point_count != 0 && entry_points == 0 )
			goto error;

		tg = get_group_in_file( f, 'mesh' );
		if( tg && (entry_point_count == 0 || entry_points == 0 ) )
			goto error;

		// write file header
		size += sizeof(tag_file_entry_point)*entry_point_count;
		
		if( tag_file_type == TAG_FILE_TYPE_MONOLITHIC )
		{
			fh.type = _tag_file_type_foundation;
			fh.version = 0;
		}
		else
		{
			fh.type = _tag_file_type_plugin;
			fh.version = f->patch_version;
		}
		
		if( f->filespec )
			f->filespec->GetBaseName( fh.name );
		else
			fs.GetBaseName( fh.name );
			
		fh.url[0] = 0;
		fh.entry_point_count = entry_point_count;
		fh.tag_count = tagcount;
		fh.checksum = 0;
		fh.flags = 0;	// fix this!
		fh.size = size;
		fh.header_checksum = 0;
		fh.signature = 'dng2';
		
		put2(out,fh.type);
		put2(out,fh.version);
		put_data( fh.name, out );
		put_data( fh.url, out );
		put2(out,fh.entry_point_count);
		put2(out,fh.tag_count);
		put4(out,fh.checksum);
		put4(out,fh.flags);
		put4(out,fh.size);
		put4(out,fh.header_checksum);
		put4(out,fh.unused[0]);
		put4(out,fh.signature);
		
		// write entry points HERE!
		if(entry_point_count > 0)
		{
			byte_swap_data_be( "entry points", entry_points, sizeof(tag_file_entry_point), entry_point_count, tag_file_entry_point_bs_codes );
			if( fwrite( entry_points, sizeof(tag_file_entry_point), entry_point_count, out ) != entry_point_count )
			{
				free(entry_points);
				goto error;
			}
			
			free(entry_points);
		}
		
		size = sizeof(tag_file_header) + (tagcount * sizeof(tag_header)) + (entry_point_count * sizeof(tag_file_entry_point));
	}
	else
	{
		entry_points = 0;
		entry_point_count = 0;
		size = sizeof(tag_header);
	}
		
	// write tag headers
	g = 0;
	while ((g = get_next_group_in_file(f,g)) != 0) {
		e = g->entries;
		// write the entries backwards so that we seek forwards through
		// the input file
		if (e) {
			while (e->next) e = e->next;
			do {
				put2( out, 0 ); // identifier
				putc( 0, out ); // flags
				
				switch( tag_file_type )
				{
					case TAG_FILE_TYPE_LOCAL:
						putc( 0, out );
						break;
						
					case TAG_FILE_TYPE_PLUGIN:
					case TAG_FILE_TYPE_MONOLITHIC:
						putc( 3, out );
						break;
				}
					
				put_data( e->name,out );
				put4( out, g->type );
				put4( out, e->id );
				put4( out, size );
				put4( out, e->length );
				put4( out, 0 );	// user data
				put2( out, e->version );	// version
				putc( 0, out );	// foundation index
				putc( -1, out );	// owner index
				put4( out, 'mth2' );
				size += e->length;
				e = e->prev;
			} while (e);
		}
	}
	
	// write tag data
	crc_new(&checksum);
	
	g = 0;
	while ((g = get_next_group_in_file(f,g)) != 0) {
		e = g->entries;
		// write the entries backwards so that we seek forwards through
		// the input file
		if (e)
		{
			while (e->next) e = e->next;
			do {
			
				// load the tag data here if necessary.
				if( (e->data == 0) && (e->length > 0) )
				{
					e->data = malloc(e->length);
					if(!e->data)
						goto error;
					if(ftell(in) != e->offset)
						fseek(in, (long)e->offset, SEEK_SET);
					if(fread(const_cast<void*>(e->data), 1, e->length, in) != e->length) goto error;
				}
				
				// crc the entry data.
				crc_checksum_buffer(&checksum, const_cast<void*>(e->data), e->length);
					
				if (fwrite( e->data, 1, e->length, out ) != e->length) goto error;
				free(const_cast<void *>(e->data));	
				e->data = 0;		
				e = e->prev;
			} while (e);
		}
	}
	
	if (in) { fclose(in); in = 0; }
	
	if( tag_file_type != TAG_FILE_TYPE_LOCAL )
	{
		// write file header
		// backup and fill in real stuff.
		fseek(out, 0, SEEK_SET);
		
		fh.checksum = checksum;
		fh.header_checksum = 0;
			
		// we crc from be.
		crc_new(&checksum);
		byte_swap_data_be( "tag header", &fh, sizeof(fh), 1, tag_file_header_bs_codes);
		crc_checksum_buffer(&checksum, &fh, sizeof(fh));
		byte_swap_data_be( "tag header", &fh, sizeof(fh), 1, tag_file_header_bs_codes);
		
		fh.header_checksum = checksum;
		
		put2(out,fh.type);
		put2(out,fh.version);
		put_data( fh.name, out );
		put_data( fh.url, out );
		put2(out,fh.entry_point_count);
		put2(out,fh.tag_count);
		put4(out,fh.checksum);
		put4(out,fh.flags);
		put4(out,fh.size);
		put4(out,fh.header_checksum);
		put4(out,fh.unused[0]);
		put4(out,fh.signature);
	}
	
	if (fclose(out) != 0) {
		out = 0;
		goto error;
	}
	
#if OPT_MACOS
	fs.SetTypeCreator( 'File', 'M2SB' );
#endif
	out = 0;
	
	// if we get here, we saved without error, so update things if desired
	if (update_internal)
	{
		if( tag_file_type != TAG_FILE_TYPE_LOCAL )
		{
			size = sizeof(tag_file_header) + (tagcount * sizeof(tag_header)) + (entry_point_count * sizeof(tag_file_entry_point));
		}
		else
		{
			size = sizeof(tag_header);
		}
		
		g = 0;
		while ((g = get_next_group_in_file(f,g)) != 0) {
			e = g->entries;
			// write the entries backwards so that we seek forwards through
			// the input file
			if (e)
			{
				while (e->next) e = e->next;
				do {
					e->data = 0;
					e->dirty = false;
					e->offset = size;
					size += e->length;
					e = e->prev;
				} while (e);
			}
		}
		f->needupdate = false;
		f->needrewrite = false;
		switch (tag_file_type)
		{
			case TAG_FILE_TYPE_LOCAL:
				f->loaded_type = _tag_file_type_local;
				break;
			default:
			case TAG_FILE_TYPE_PLUGIN:
				f->loaded_type = _tag_file_type_plugin;
				break;
			case TAG_FILE_TYPE_MONOLITHIC:
				f->loaded_type = _tag_file_type_foundation;
				break;
		}
		
		delete f->filespec;
		f->filespec = new XGFileSpecifier(fs);
	}
	
	return true;
	
error:
	if (in) fclose(in);
	if (out) fclose(out);
	return false;
}

void rewrite_tag_file( tag_file *f )
{
	char message[1024];
	FILE *file;

	if( !f || !f->filespec )
		return;
	
	if (f->loaded_type == _tag_file_type_patch || f->loaded_type == _tag_file_type_foundation)
	{
		char name[256];
		f->filespec->GetName( name );
		sprintf( message, "%s - Cannot modify base tags or patches.", name );
		throw XGPostError(KXGFileWriteError, message, NULL );
	}

	file = f->filespec->OpenFile( "r+b" );
	if( !file )
	{
		file = f->filespec->OpenFile( "rb" );
		if( file )
		{
			f->readonly = true;
			fclose( file );
			char name[256];
			f->filespec->GetName( name );
			sprintf( message, "%s - The file is read-only.", name );
			throw XGPostError(KXGFileWriteError, message, NULL );
		}
	}
	else
	{
		f->readonly = false;
		fclose( file );
	}
		
	XGFileSpecifier newspec = f->filespec->TempInSameDirectory();
	XGFileSpecifier oldspec = *(f->filespec);

	if( write_tags_to_file( newspec, f, true ) )
	{
		if( XGFileSpecifier::ExchangeFileContents( oldspec, newspec ) )
		{
			newspec.DeleteFile();
			delete f->filespec;
			f->filespec = new XGFileSpecifier( oldspec );
		}
	}
}

void update_tag_file( tag_file *tf )
{
	char message[1024];
	
	if (tf->loaded_type == _tag_file_type_patch || tf->loaded_type == _tag_file_type_foundation)
	{
		char name[256];
		tf->filespec->GetName( name );
		sprintf( message, "%s - Cannot modify base tags or patches.", name );
		throw XGPostError(KXGFileWriteError, message, NULL );
	}
	
	if( !tf->filespec )
		return;
	
	if( tf->needrewrite )
	{
		rewrite_tag_file( tf );
		return;
	}

	FILE *f = tf->filespec->OpenFile("r+b");
	if( !f )
	{
		rewrite_tag_file( tf );
		return;
	}
	
	tag_group *g = 0;
	while ((g = get_next_group_in_file(tf,g)) != 0) {
		tag_entry *e = 0;
		while ((e = get_next_entry_in_group(g,e)) != 0) {
			if (e->dirty) {
				assert( e->data );
				fseek( f, e->offset, SEEK_SET );
				fwrite( e->data, e->length, 1, f );
				e->dirty = false;
			}
		}
	}
	
	fclose( f );
	tf->needupdate = false;
	tf->readonly = false;
}


XGFileSpecifier *get_tag_file_spec( tag_file *f )
{
	return f ? f->filespec : 0;
}


bool is_file_readonly( tag_file *f )
{
	return f->readonly || f->loaded_type == _tag_file_type_patch || f->loaded_type == _tag_file_type_foundation;
}

bool is_file_modified( tag_file *f )
{
	assert( !f->needrewrite || f->needupdate );
	return f->needrewrite || f->needupdate;
}


bool is_entry_modified( tag_entry *e )
{
	assert( !e->dirty || e->file->needupdate );
	return e->dirty;
}


tag get_indexed_type( int index )
{
	for (int i = 0; i < index; ++i) {
		if (!tagtypelist[i].type) return 0;
	}
	return tagtypelist[index].type;
}


const char *get_plural_name_for_type( tag type )
{
	tag_type_info *t = typeinfo(type);
	return t ? t->plural : "UNKNOWN TYPE";
}


const char *get_singular_name_for_type( tag type )
{
	tag_type_info *t = typeinfo(type);
	return t ? t->singular : "UNKNOWN TYPE";
}


tag_editor_proc get_type_editor( tag type, bool mod )
{
	tag_type_info *t = typeinfo(type);
	return t ? ( mod ? t->mod_key_editor : t->editor ) : NULL;
}


tag_editor_proc get_type_creator( tag type )
{
	tag_type_info *t = typeinfo(type);
	return t ? t->creator : NULL;
}

short get_type_version( tag type )
{
	tag_type_info* t = typeinfo(type);
	return t ? t->version : 1;
}

static tag increment_tag_id( tag checkme )
{
	for (int pos = 0; pos < 32; pos += 8) {
		int c = (checkme >> pos) & 0xff;
		checkme &= ~(0xff << pos);
		c += 1;
		if (c == '9' + 1) c = 'a';
		if (isalnum(c)) {
			checkme += c << pos;
			return checkme;
		}
		checkme += '0' << pos;
	}
	return checkme;
}

#define TAG_LENGTH 32

tag new_tag_id_from_string( const char *string, tag unique_in_group, tag_file *unique_in_file )
{
//	const char *separators= " \t\n()<>[]{}?!:;-#";
	const char *separators= " \t\n()[]{}?!:;-_.";
	
	short offset;
	char *token;

	unsigned char build[4] = { '_', '_', '_', '_' };
	char string_copy[TAG_LENGTH+1];
	
	strncpy(string_copy, string, TAG_LENGTH);
	string_copy[TAG_LENGTH]= '\0';
	{
		char *s = string_copy;
		while (*s) {
			*s = tolower(*s);
			++s;
		}
	}
	
	for (offset= 0, token= strtok(string_copy, separators);
		token && offset<sizeof(tag);
		offset= MIN(offset+strlen(token),MAX(offset+1, 2)), token= strtok((char *) NULL, separators))
	{
		memcpy( build + offset, token, MIN(strlen(token), sizeof(tag)-offset));
	}
	
	tag checkme = (build[0] << 24) + (build[1] << 16) + (build[2] << 8) + (build[3] << 0);
	
	if (unique_in_file) {
		if (unique_in_group) {
			tag_group *g = get_group_in_file( unique_in_file, unique_in_group, false );
			if (g) {
				while (get_entry_in_group( g, checkme )) {
					checkme = increment_tag_id(checkme);
				}
			}
		} else {
	retry1:
			for (tag_group *g = unique_in_file->groups; g; g = g->next) {
				if (get_entry_in_group( g, checkme )) {
					checkme = increment_tag_id( checkme );
					goto retry1;
				}
			}
		}
	} else {
		if (unique_in_group) {
			while (get_entry( unique_in_group, checkme )) {
				checkme = increment_tag_id( checkme );
			}
		}
	}
	
	return checkme;
}

tag_entry *find_duplicate_id( tag_entry *e, bool start_at_begining )
{
	tag_file *tf;
	tag_group *tg;
	tag_entry *te;
	bool noset = false;
	
	tag search_group_type = e->group->type;

	// start at the very begining
	if( start_at_begining )
	{
		tf = tag_file_list;
		tg = NULL;
		te = NULL;
	}	
	
	// start looking just after this tag entry
	else
	{
		assert( e->file );
		tf = e->file;
		
		assert( e->group );
		tg = e->group;
		
		te = e->next;
		
		noset = true;
	}

	// search for matching tags
	while( tf )
	{
		// check that this file is in the 'plugins' directory
		// if there is not filespec (unsaved file) check it as well
		XGDirectory directory;
		char strDirectory[256];

		if( tf->filespec )
		{
			tf->filespec->GetDirectory( directory );
			directory.GetDirectoryName( strDirectory );
			
			if( !strstr( "plugins", strDirectory ) )
			{
//				tf = tf->next;
//				tg = NULL;
//				te = NULL;
//				continue;
			}
		}
		
		if( !tg )
			tg = tf->groups;
			
		while( tg )
		{
			if( tg->type == search_group_type )
			{
				if( !te && !noset )
					te = tg->entries;
					
				noset = false;
					
				while( te )
				{
					if( e->id == te->id )
					{
						// we have a winner
						return te;
					}
								
					te = te->next;
				}
				
				// jump to the next file
				break;
			}
			
			tg = tg->next;
		}
		
		tf = tf->next;
		tg = NULL;
		te = NULL;
	}
	
	// found nothing
	return NULL;
}	
