/* seborrhea.m,
 *
 * Seborrhea datafile system thingie.
 */

#include <assert.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <time.h>
#ifdef __MINGW__
# include <allegro.h>
#endif
#include "alrand/alrand.h"
#include "container-animation.h"
#include "container-file.h"
#include "container-lump.h"
#include "dirlist-recursive.h"
#include "dirlist-zzip.h"
#include "seborrhea-debug.h"
#include "seborrhea.h"


#ifndef ABS				/* Allegro defines ABS. */
# define ABS(x)			(x >= 0) ? (x) : (-(x))
#endif


#ifndef PATH_MAX			/* Win */
# define PATH_MAX		MAX_PATH
#endif


metafile_command_handler_t metafile_command_handlers[MAX_METAFILE_COMMANDS];
metafile_property_handler_t metafile_property_handlers[MAX_METAFILE_PROPERTY_HANDLERS];
filename_extension_handler_t filename_extension_handlers[MAX_FILETYPE_HANDLERS];


void delimited_strdup(char *dest, const char *start, const char *end, size_t size)
{
    int n = end - start;

    assert(n > 0 && (unsigned int)n < size - 1);
    strncpy(dest, start, n);
    dest[n] = '\0';
}

/*--------------------------------------------------------------*/
/* A list of directories to try when loading data. */

#define MAX_ROOT_DIRECTORIES	16
char *seborrhea_data_root_directories[MAX_ROOT_DIRECTORIES] = {};

void seborrhea_add_root_directory(const char *dir)
{
    int i;
    char *dirname;
    assert(dir);

    /* Make sure 'dir' has a trailing slash. */
    i = strlen(dir);
    if (i > 0 && dir[i-1] == '/') {
	dirname = strdup(dir);
    }
    else {
	dirname = malloc(sizeof(char) * (i+2));
	snprintf(dirname, i+2, "%s/", dir);
    }

    for (i = 0; i < MAX_ROOT_DIRECTORIES; i++) {
	if (not seborrhea_data_root_directories[i]) {
	    seborrhea_data_root_directories[i] = dirname;
	    SEBORRHEA_MESSAGE(stdout, "[Seborrhea] Added data directory %s\n", dirname);
	    return;
	}
	if (streq(dirname, seborrhea_data_root_directories[i])) {
	    free(dirname);
	    return;
	}
    }

    free(dirname);
    assert(NO);
}

static int seb_container_filter(const char *filename)
{
    /* Ignore hidden files, =meta, =contents */
    return (filename[0] != '.' &&
	    strneq(filename, "=contents") &&
	    strneq(filename, "=ignore") &&
	    strneq(filename, "=meta"));
}

void seborrhea_free_root_directories(void)
{
    int i;

    for (i = 0; i < MAX_ROOT_DIRECTORIES; i++) {
	if (seborrhea_data_root_directories[i]) {
	    free(seborrhea_data_root_directories[i]);
	    seborrhea_data_root_directories[i] = NULL;
	}
	else
	    break;
    }

    /* XXX: this shouldn't go here, but I'm lazy. */
    alrand_exit();
}

/*--------------------------------------------------------------*/

@implementation Sebum
- initWithName: (const char *)name_
{
    [self init];
    name = strdup(name_);
    return self;
}

- free
{
    if (name) {
	free(name);
	name = NULL;
    }
    return [super free];
}

- (const char *) name { return name; }
@end

/*--------------------------------------------------------------*/
/* Abstract classes.						*/
/*--------------------------------------------------------------*/

@implementation AbstractSebContainer
- init
{
    [super init];
    process_directories = NO;
    nelements = 0;
    element = NULL;
    return self;
}

- free
{
    while (nelements > 0) {
	nelements--;
	[element[nelements] free];
    }

    free(element);
    element = NULL;

    return [super free];
}

- (BOOL) addToList:(Sebum *)child
{
    void *new_elements;
    assert(child);

    new_elements = realloc(element, (nelements+1) * sizeof(Sebum *));
    if (not new_elements)
	return NO;

    element = new_elements;
    element[nelements] = child;
    nelements++;
    return YES;
}

- (BOOL) file:(char *)filename IsInCurrentDirectory:(const char *)current_dir
{
    char *slashie;
    unsigned int l;
    assert(filename && current_dir);

    l = strlen(current_dir);
    if (l >= strlen(filename))
	return NO;

    if (strncmp(filename, current_dir, l) != 0)
	return NO;

    filename = filename + l;
    slashie = strchr(filename, '/');
    if (slashie && (*(slashie+1) != '\0'))
	return NO;
    else
	return YES;
}

- (BOOL) loadContentsOfDirectory:(const char *)top_dir :(const char *)sub_dir WithDirList:(DirectoryList *)dirlist
{
    unsigned int ent;
    assert(top_dir && sub_dir && dirlist);

    for (ent = 0; ent < [dirlist numEntries]; ent++) {
	BOOL is_file, is_directory;
	dirent_t *entry = [dirlist getNthEntry:ent];

	/* This entry might have been loaded already (by metafiles). */
	if ([dirlist entryIsLoaded:ent] || not entry)
	    continue;

	/* If this ent is not in the current directory, skip it. */
	if (not [self file:entry->d_name IsInCurrentDirectory:sub_dir])
	    continue;

	/* Mark this entry as loaded. */
	if (seb_container_filter(entry->d_name + strlen(sub_dir)) == 0) {
	    [dirlist markEntryLoaded:ent];
	    continue;
	}

	is_file = [dirlist isFile:ent];
	is_directory = (process_directories) ? [dirlist isDirectory:ent] : NO;

	if (is_file) {
	    const char *ext;
	    char name_sans_ext[PATH_MAX];
	    char dir_plus_name[PATH_MAX];

	    {				/* Get filename extension. */ 
		unsigned int n;
		char *file_start = strrchr(entry->d_name, '/');
		const char *dot = strrchr(entry->d_name, '.');
		if (not dot)
		    goto try_directory;
		if (not file_start)
		    file_start = entry->d_name;
		else
		    file_start++;
		ext = dot+1;

		/* Get name without extension. */
		delimited_strdup(name_sans_ext, file_start, dot, sizeof name_sans_ext);

		/* Get directory + name. */ 
		n = snprintf(dir_plus_name, sizeof dir_plus_name, "%s%s", top_dir, entry->d_name);
		assert(n < sizeof dir_plus_name);
	    }

	    {				/* Determine file type. */
		int i;
		for (i = 0; i < MAX_FILETYPE_HANDLERS && filename_extension_handlers[i].proc; i++) {
		    if (strcaseeq(ext, filename_extension_handlers[i].ext)) {
			filename_extension_handlers[i].proc(dir_plus_name, name_sans_ext, ent, self, dirlist);
			is_directory = NO;
			break;
		    }
		}
		if (i >= MAX_FILETYPE_HANDLERS ||
		    not filename_extension_handlers[i].proc) {
		    fprintf(stderr, "Don't know what to do with %s\n", dir_plus_name);
		}
	    }
	}

    try_directory:
	if (is_directory) {
	    Sebum<SebContainer> *child;
	    char *type = strrchr(entry->d_name, '=');

	    if (type) {
		char new_name[1024];

		delimited_strdup(new_name, entry->d_name + strlen(sub_dir), type, sizeof new_name);
		type++;

		if (streq(type, "anim/"))
		    child = [SebAnimation alloc];
		else if (streq(type, "lump/"))
		    child = [SebLump alloc];
		else {
		    fprintf(stderr, "[%s] Unknown type (%s) for directory\n", [[self class] name], type);
		    return NO;
		}

		[child initWithName:new_name];
	    }
	    else {
		char new_name[1024];
		strncpy(new_name, entry->d_name + strlen(sub_dir), sizeof new_name);
		new_name[strlen(new_name) - 1] = '\0';
		child = [[SebFile alloc] initWithName:new_name];
	    }

	    if (not [child startLoadingSebumDirectory:top_dir :entry->d_name WithDirList:dirlist] ||
		not [self addToList:child]) {
		[child free];
		return NO;
	    }
	}
    }

    return YES;
}

- (BOOL) startLoadingSebumDirectory:(const char *)top_dir :(const char *)sub_dir WithDirList:(DirectoryList *)dirlist
{
    assert(top_dir && sub_dir);

    SEBORRHEA_MESSAGE(stdout, "[%s] Opening %s%s\n", [[self class] name], top_dir, sub_dir);

    return [self loadContentsOfDirectory:top_dir :sub_dir WithDirList:dirlist];
}

#if 0
- (void) listContents:(unsigned int)depth
{
    unsigned int i, j;

    for (i = 0; i < nelements; i++) {
	for (j = 0; j < depth; j++)
	    printf(" ");

	printf("%s\n", [element[i] name]);

	if ([element[i] isKindOf:[AbstractSebContainer class]])
	    [(AbstractSebContainer *)element[i] listContents:depth+1];
    }
}
#endif

- (BOOL) loadSebumDirectory:(const char *)dirname
{
    int i;
    assert(dirname);

    SEBORRHEA_MESSAGE(stdout, "[%s] Loading %s\n", [[self class] name], dirname);

    for (i = 0; seborrhea_data_root_directories[i]; i++) {
	DirectoryList *dirlist;
	char work[PATH_MAX], work2[PATH_MAX];

	/* Try opening as a directory. */
	snprintf(work, sizeof work, "%s%s/", seborrhea_data_root_directories[i], dirname);
	dirlist = [RecursiveDirectoryList new];
	if (dirlist) {
	    if ([dirlist scanDirectory:work] &&
		[self startLoadingSebumDirectory:work :"" WithDirList:dirlist]) {
		[dirlist free];
		/*[self listContents:0];*/
		return YES;
	    }
	    dirlist = [dirlist free];
	}

#ifndef NO_ZZIP
	/* Try opening as a zip. */
	snprintf(work2, sizeof work2, "%s%s", seborrhea_data_root_directories[i], dirname);
	dirlist = [ZzipDirectoryList new];
	if (dirlist) {
	    if ([dirlist scanDirectory:work2] &&
		[self startLoadingSebumDirectory:work :"" WithDirList:dirlist]) {
		[dirlist free];
		/*[self listContents:0];*/
		return YES;
	    }
	    dirlist = [dirlist free];
	}
#endif
    }

    SEBORRHEA_MESSAGE(stdout, "[%s] Failed to load %s\n", [[self class] name], dirname);
    [self free];
    return NO;
}
@end

/*--------------------------------------------------------------*/

void seborrhea_register_metafile_command(const char *command, command_handler_proc_t proc)
{
    int i;
    assert(command && proc);

    for (i = 0; i < MAX_METAFILE_COMMANDS; i++) {
	if (metafile_command_handlers[i].proc)
	    continue;

	metafile_command_handlers[i].command = command;
	metafile_command_handlers[i].proc = proc;

	SEBORRHEA_MESSAGE(stdout, "[Seborrhea] Added metafile command %s. (%d/%d)\n",
			  command, i+1, MAX_METAFILE_COMMANDS);
	return;
    }
    assert("[Seborrhea] Out of space for command handlers" && NO);
}

void seborrhea_register_metafile_property(const char *prop, property_handler_proc_t proc)
{
    int i;
    assert(prop && proc);

    for (i = 0; i < MAX_METAFILE_PROPERTY_HANDLERS; i++) {
	if (metafile_property_handlers[i].proc)
	    continue;

	metafile_property_handlers[i].prop = prop;
	metafile_property_handlers[i].proc = proc;

	SEBORRHEA_MESSAGE(stdout, "[Seborrhea] Added metafile property %s. (%d/%d)\n",
			  prop, i+1, MAX_METAFILE_PROPERTY_HANDLERS);
	return;
    }
    assert("[Seborrhea] Out of space for property handlers" && NO);
}

void seborrhea_register_filetype_handler(const char *ext, filetype_handler_proc_t proc)
{
    int i;
    assert(ext && proc);

    for (i = 0; i < MAX_FILETYPE_HANDLERS; i++) {
	if (filename_extension_handlers[i].proc)
	    continue;

	filename_extension_handlers[i].ext = ext;
	filename_extension_handlers[i].proc = proc;

	SEBORRHEA_MESSAGE(stdout, "[Seborrhea] Added handler for %s. (%d/%d)\n",
			  ext, i+1, MAX_FILETYPE_HANDLERS);
	return;
    }
    assert("[Seborrhea] Out of space for filetype handlers" && NO);    
}

/*--------------------------------------------------------------*/

@interface SeborrheaInit
@end

@implementation SeborrheaInit
static BOOL seborrhea_ignore_file(const char *_1, unsigned int _2, unsigned int _3, Token *_4[], Sebum<SebContainer> *_5, DirectoryList *_6)
{
    (void)_1, (void)_2, (void)_3, (void)_4, (void)_5, (void)_6;
    return YES;
}

static void seborrhea_animation_properties(Sebum *seb, unsigned int argc, Token *argv[])
{
    enum SEB_ANIMATION_LOOP_METHOD method;
    assert(seb && [seb isKindOf:[SebAnimation class]] && "Received sebum is not an animation!");
    assert(argc == 3 && "Wrong number of arguments received for animation-properties!");
    assert([argv[1] isType:TOKEN_FIXNUM] &&
	   [argv[2] isType:TOKEN_SYMBOL] && "Wrong types of arguments received for animation-properties!");

    method = (streq([argv[2] getSymbol], "loop-forward") ? LOOP_FORWARD :
	      streq([argv[2] getSymbol], "loop-ping-pong") ? LOOP_PING_PONG : LOOP_NONE);
    [(SebAnimation *)seb setFrameDelay:[argv[1] getFixnum] LoopMethod:method];
}

static void seborrhea_image_pivot(Sebum *seb, unsigned int argc, Token *argv[])
{
    assert(seb && [seb conformsTo:@protocol(SebImage)] && "Received sebum is not an image!");
    assert(argc == 3 && "Wrong number of arguments received for image-pivot!");
    assert([argv[1] isType:TOKEN_FIXNUM] &&
	   [argv[2] isType:TOKEN_FIXNUM] && "Wrong types of arguments received for image-pivot!");

    [(<SebImage>)seb setPivotX:[argv[1] getFixnum] Y:[argv[2] getFixnum]];
}

static void seborrhea_sound_properties(Sebum *seb, unsigned int argc, Token *argv[])
{
    assert(seb && [seb conformsTo:@protocol(SebSample)] && "Received sebum is not a sample!");
    assert(argc == 2 && "Wrong number of arguments received for sound-properties!");
    assert([argv[1] isType:TOKEN_FIXNUM] && "Wrong types of arguments received for sound-properties!");

    [(<SebSample>)seb setDefaultVolume:[argv[1] getFixnum]];
}

/*--------------------------------------------------------------*/

+ (void) load
{
    /* Add the "ignore" metafile command. */
    seborrhea_register_metafile_command("ignore", seborrhea_ignore_file);

    /* Some metafile properties. */
    seborrhea_register_metafile_property("animation-properties", seborrhea_animation_properties);
    seborrhea_register_metafile_property("image-pivot", seborrhea_image_pivot);
    seborrhea_register_metafile_property("sound-properties", seborrhea_sound_properties);

    /* Alrand because Windows' rand() is dumb. */
    alrand_init();
    alrand_srand(time(0));
    alrand_set_global_generator_type(alrand_type_mersenne_twister);
}
@end
