/*  Sarien - A Sierra AGI resource interpreter engine
 *  Copyright (C) 1999-2001 Stuart George and Claudio Matsuoka
 *
 *  $Id: agi_v4.c,v 1.8 2001/09/06 18:52:14 cmatsuoka Exp $
 *
 *  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; see docs/COPYING for further details.
 */

#include <stdio.h>
#include <string.h>
#include "sarien.h"
#include "agi.h"

#if defined PALMOS

static int agi_v4_init (void);
static int agi_v4_deinit (void);
static int agi_v4_detect_game (char *);
static int agi_v4_load_resource (int, int);
static int agi_v4_unload_resource (int, int);
static int agi_v4_load_objects(char *);
static int agi_v4_load_words(char *);

UINT32 pdb_read_dword(UINT8 *mem);
UINT16 pdb_read_word(UINT8 *mem);
int load_v2_dir(struct agi_dir *agid, int offset, UINT32 flen, FILE *fp);
int load_v3_dir(struct agi_dir *agid, int offs, UINT32 len, FILE *fp);

extern struct agi_picture pictures[];
extern struct agi_logic logics[];
extern struct agi_view views[];
extern struct agi_sound sounds[];

struct agi_loader agi_v4 = {
	4,
	0,
	agi_v4_init,
	agi_v4_deinit,
	agi_v4_detect_game,
	agi_v4_load_resource,
	agi_v4_unload_resource,
	agi_v4_load_objects,
	agi_v4_load_words
};


/* this must be kept synchronous with the AGI2PDB headers */
#define	PDB_OPT_AGDS		0x0001
#define PDB_OPT_AMIGA		0x0002

#ifdef __WATCOMC__
#pragma pack(1)
#endif

struct pdb_pdbheader
{
	UINT8 name[32];
				/*
				   String - This is the name of the database
				    on the PalmPilot device.
				    It need not match the name of the PDB file
				    in the environment in which it is created.
				*/
	UINT16 fileAttributes;
				/*
				   Attributes of the pdb file.
				    0x0002 Read-Only
				    0x0004 Dirty AppInfoArea
				    0x0008 Backup this database
				           (i.e. no conduit exists)
				    0x0010 Okay to install newer over existing
				           copy, if present on PalmPilot
				    0x0020 Force the PalmPilot to reset after
				           this database is installed
				*/
	UINT16 version;
				/*
				   Defined by the application.
				*/
	UINT32 creationDate;
				/*
				   Expressed as the number of seconds since
				    January 1, 1904. The database will not
				    install if this value is zero. (PalmOS
				    1.0.6)
				*/
	UINT32 modificationDate;
				/*
				   Expressed as the number of seconds since
				    January 1, 1904.
				    The database will not install if this
				    value is zero. (PalmOS 1.0.6)
				*/
	UINT32 lastBackupDate;
				/*
				   Expressed as the number of seconds since
				    January 1, 1904. This can be left at zero
				    and the database will install.
				*/
	UINT32 modificationNumber;
				/*
				   Set to zero.
				*/
	UINT32 appInfoArea;
				/*
				   The byte number in the PDB file (counting
				    from zero) at which the AppInfoArea is
				    located. This must be the first entry
				    in the Data portion of the PDB file.
				    If this database does not have an
				    AppInfoArea, set this value to zero.
				*/
	UINT32 sortInfoArea;
				/*
				   The byte number in the PDB file (counting
				    from zero) at which the SortInfoArea is
				    located. This must be placed immediately
				    after the AppInfoArea, if one exists,
				    within the Data portion of the PDB file.
				    If this database does not have a
				    SortInfoArea, set this value to zero.
				    Do not use this. See Note C below for
				    further details.
				*/
	UINT8 databaseType[4];
				/*
				   String - Set this to the desired value.
				    Generally it should match the Database Type
				    used by the corresponding application.
				    This is 4 characters long and does not have
				    a terminating null.
				*/
	UINT8 creatorID[4];
				/*
				   String - Set this to the desired value.
				    Generally it should match the Creator ID
				    used by the corresponding application. In
				    all cases, you should always register your
				    Creator ID before using it. This is 4
				    characters long and does not have a
				    terminating null.
				*/
	UINT32 uniqueIDSeed;
				/*
				   This is used to generate the Unique ID
				    number of subsequent records. Generally,
				    this should be set to zero.
				*/
	UINT32 nextRecordListID;
				/*
				   Set this to zero. (This does not appear
				    to be used, but that has not been
				    verified by a third party.)
				*/
	UINT16 numberOfRecords;
				/*
				   This contains the number of records
				*/
}
#ifdef __GNUC__
__attribute__((packed))
#endif
;

struct pdb_recheader{
	UINT32 recordDataOffset;
				/*
				   The byte number in the PDB file (counting
				    from zero) at which the record is located.
				*/
	UINT8 recordAttributes;
				/*
				   The records attributes.
 				    0x10 Secret record bit.
				    0x20 Record in use (busy bit).
				    0x40 Dirty record bit.
				    0x80 Delete record on next HotSync.
				    The least significant four bits are used
				     to represent the category values.
				*/
	UINT8	uniqueID[3];
				/*
				   Set this to zero and do not try to
				    second-guess what PalmOS will do with
				    this value.
				*/
}
#ifdef __GNUC__
__attribute__((packed))
#endif
;

struct agi3vol {
	UINT32 sddr;
	UINT32 len;
};

/* a agi v2.xxx game format */
struct PalmOSAGI2
{
	UINT32	logdir[2];
	UINT32	picdir[2];
	UINT32	viewdir[2];
	UINT32	snddir[2];

	UINT32	object[2];
	UINT32	words[2];

	UINT32	vol[16*2];		/* 0=offs, 1=len */
};

/* an agi v3.xx.yyy game format */
struct PalmOSAGI3
{
	UINT32	dir[2];
	UINT32	object[2];
	UINT32	words[2];
	UINT32	vol[16*2];		/* 0=offs, 1=len */
};

union PalmOSAGI4
{
	struct PalmOSAGI2 v2;
	struct PalmOSAGI3 v3;
};

/* PalmOS AGI Header */
struct PalmOSAGI
{
	char	name[32];
	UINT16	version;
	UINT16	emulation;
	UINT32	options;
	UINT32	crc;

	union PalmOSAGI4 v4;
};

#ifdef __WATCOMC__
#pragma pack()
#endif

struct PalmOSAGI palmagi;
struct pdb_pdbheader PDBHeader;

/* our PDB file that contains everything */
char	pdb_file[MAX_PATH];
UINT32	pdb_start;

static int agi_v4_detect_game (char *gn)
{
	FILE *fp;
	int ec= err_InvalidAGIFile;
	int offset;
	struct pdb_recheader rh;

	/* gn is filename for a .pdb! */

	memset(&PDBHeader, 0x0, sizeof(struct pdb_pdbheader));
	memset(&palmagi, 0x0, sizeof(struct PalmOSAGI));
	strcpy(pdb_file, gn);

	fp = fopen (pdb_file, "rb");
	if(fp!=NULL)
	{
		fread(&PDBHeader, 1, sizeof(struct pdb_pdbheader), fp);

		if(PDBHeader.creatorID[0]=='F' && PDBHeader.creatorID[1]=='A' &&
		   PDBHeader.creatorID[2]=='G' && PDBHeader.creatorID[3]=='I')
		{
			/* valid palm database with Sarien Creator ID */
			/* now skip record headers, read in v4 Header */

			fread(&rh, 1, sizeof(struct pdb_recheader), fp);

			offset=(int)pdb_read_dword((UINT8*)&rh.recordDataOffset);

			/* our data all starts from here. add this to all offsets */
			pdb_start=offset;

			fseek(fp, offset, SEEK_SET);
			fread(&palmagi, 1, sizeof(struct PalmOSAGI), fp);

			palmagi.crc=pdb_read_dword((UINT8*)&palmagi.crc);
			palmagi.options=pdb_read_word((UINT8*)&palmagi.options);
			palmagi.emulation=pdb_read_word((UINT8*)&palmagi.version);
			palmagi.version=pdb_read_word((UINT8*)&palmagi.version);

			/* setup our options before we call setup game! */
			if((palmagi.options&PDB_OPT_AGDS))
				opt.agds=1;

			if((palmagi.options&PDB_OPT_AMIGA))
				opt.amiga=1;

			v4id_game(palmagi.crc);

			/* if the game is a v2 */
			
			/* use agi_get_release instead of palmagi.emulation */			
			if((agi_get_release()>>12)==0x2)
				agi_v4.int_version = 0x2917;		/* setup for 2.917 */
			else
				agi_v4.int_version = 0x3149;		/* setup for 3.002.149 */

			ec=err_OK;
		}

		fclose(fp);
	}
	
	return ec;
}


static int agi_v4_load_dir (struct agi_dir *agid, int intDirType)
{
	int ec=err_OK;

#ifndef PALMOS
	FILE *fp;
	struct agi3vol agi_vol3[4];
	UINT32	offs, flen;
	UINT16 xd[4];
	int	i;

	if ((fp = fopen (pdb_file, "rb")) == NULL) {
		return err_BadFileOpen;
	}

	if((palmagi.emulation>>12)==2)
	{
		/* load singledir v2 style */
		switch(intDirType)
		{
			case rLOGIC: ec=load_v2_dir(agid, pdb_start + palmagi.v4.v2.logdir[0], palmagi.v4.v2.logdir[1], fp); break;
			case rPICTURE: ec=load_v2_dir(agid, pdb_start + palmagi.v4.v2.picdir[0], palmagi.v4.v2.picdir[1], fp); break;
			case rSOUND: ec=load_v2_dir(agid, pdb_start + palmagi.v4.v2.snddir[0], palmagi.v4.v2.snddir[1], fp); break;
			case rVIEW: ec=load_v2_dir(agid, pdb_start + palmagi.v4.v2.viewdir[0], palmagi.v4.v2.viewdir[1], fp); break;
		}
	}
	else
	{
		/* load v3 gamedir style */
		offs=pdb_start + palmagi.v4.v3.dir[0];
		flen=palmagi.v4.v3.dir[1];

		fseek(fp, offs, SEEK_SET);

		/* FIXME: Not Endian Aware */
		fread (&xd, 1, 8, fp);

		for(i = 0; i < 4; i++)
			agi_vol3[i].sddr = offs + xd[i];

		agi_vol3[0].len = agi_vol3[1].sddr - agi_vol3[0].sddr;
		agi_vol3[1].len = agi_vol3[2].sddr - agi_vol3[1].sddr;
		agi_vol3[2].len = agi_vol3[3].sddr - agi_vol3[2].sddr;
		agi_vol3[3].len = ftell(fp) - agi_vol3[3].sddr;

		if (agi_vol3[3].len > 256 * 3)
			agi_vol3[3].len = 256 * 3;

		/* read in all directory file info */
		ec=load_v3_dir(game.dir_logic, agi_vol3[0].sddr, agi_vol3[0].len, fp);
		if(ec==err_OK)
			ec=load_v3_dir(game.dir_pic, agi_vol3[1].sddr, agi_vol3[1].len, fp);
		if(ec==err_OK)
			ec=load_v3_dir(game.dir_view, agi_vol3[2].sddr, agi_vol3[2].len, fp);
		if(ec==err_OK)
			ec=load_v3_dir(game.dir_sound, agi_vol3[3].sddr, agi_vol3[3].len, fp);
	}

	fclose(fp);
#endif

	return ec;
}

int load_v2_dir(struct agi_dir *agid, int offset, UINT32 flen, FILE *fp)
{
	UINT8 *mem;
	int i;

	if ((mem = malloc (flen + 32)) == NULL) {
		fclose (fp);
		return err_NotEnoughMemory;
	}

	fseek (fp, offset, SEEK_SET);
	fread (mem, 1, flen, fp);

	/* set all directory resources to gone */
	for (i = 0; i < MAX_DIRS; i++) {
		agid[i].volume = 0xff;
		agid[i].offset = _EMPTY;
	}

	/* build directory entries */
	for (i = 0; i < flen; i+=3) {
		agid[i/3].volume = hilo_getbyte (mem+i) >> 4;
		agid[i/3].offset = hilo_getpword (mem+i) & _EMPTY;
	}

	free (mem);

	return err_OK;
}

int load_v3_dir(struct agi_dir *agid, int offs, UINT32 len, FILE *fp)
{
	int ec = err_OK;
	UINT8 *mem;
	int i;

	fseek (fp, offs, SEEK_SET);
	if ((mem = malloc (len + 32)) != NULL) {
		fread(mem, 1, len, fp);

		/* set all directory resources to gone */
		for(i = 0; i < MAX_DIRS; i++) {
			agid[i].volume = 0xFF;
			agid[i].offset = _EMPTY;
		}

		/* build directory entries */
		for(i = 0; i < len; i += 3) {
			agid[i / 3].volume = hilo_getbyte (mem + i) >> 4;
			agid[i / 3].offset = hilo_getpword (mem+i) & _EMPTY;
		}

		free(mem);
	} else {
		ec = err_NotEnoughMemory;
	}

	return ec;
}


static int agi_v4_init ()
{
	int ec = err_OK;

	/* load directory files */
	switch((palmagi.emulation>>12))
	{
		case 2:
			ec = agi_v4_load_dir (game.dir_logic, rLOGIC);
			if (ec == err_OK)
				ec = agi_v4_load_dir (game.dir_pic, rPICTURE);
			if (ec == err_OK)
				ec = agi_v4_load_dir (game.dir_view, rVIEW);
			if (ec == err_OK)
				ec = agi_v4_load_dir (game.dir_sound, rSOUND);
			break;
		case 3:
			/* a v3 game load will load all DIR files,
			   so below is just dummy code */
			ec=agi_v4_load_dir(game.dir_logic, rLOGIC);
			break;
	}

	return ec;
}


static int agi_v4_deinit ()
{
	int ec = err_OK;
	return ec;
}



static int agi_v4_unload_resource (int restype, int resnum)
{
	switch (restype) {
	case rLOGIC:
		unload_logic (resnum);
		break;
	case rPICTURE:
		unload_picture (resnum);
		break;
	case rVIEW:
		unload_view (resnum);
		break;
	case rSOUND:
		unload_sound (resnum);
		break;
	}

	return err_OK;
}


/*
 * This function does noting but load a raw resource into memory,
 * if further decoding is required, it must be done by another
 * routine. NULL is returned if unsucsessfull.
 */

UINT8* agi_v4v2_load_vol_res (struct agi_dir *agid)
{
#ifndef PALMOS
	UINT8 *data = NULL;
	FILE *fp;
	UINT32	offs;
	char	x[16];

	/* fill offs with volume offset*/

	offs = pdb_start + palmagi.v4.v2.vol[(2*agid->volume)];

	/* loading a bad resource */
	if(offs==pdb_start)
		agid->offset=_EMPTY;

	_D ("(agi_dir = [offset:%ld, len:%ld])", agid->offset, agid->len);

	if (agid->offset != _EMPTY && (fp = fopen (pdb_file, "rb")) != NULL) {
		_D ("loading resource");
		fseek (fp, offs + agid->offset, SEEK_SET);
		fread (&x, 1, 5, fp);
		if (hilo_getword (x) == 0x1234) {
			agid->len = lohi_getword (x + 3);
			data = calloc (1, agid->len + 32);
			if (data != NULL)
				fread (data, 1, agid->len, fp);
		} else {
			/* FIXME: call some panic handler instead of
			 *        deiniting directly
			 */
			deinit_video_mode ();
			fprintf (stderr, "ACK! BAD RESOURCE!!!\n");
			exit (0);
		}
		fclose (fp);
	} else {
		/* we have a bad volume resource */
		/* set that resource to NA */
		agid->offset = _EMPTY;
	}

	return data;
#endif
}

UINT8* agi_v4v3_load_vol_res (struct agi_dir *agid)
{
#ifndef PALMOS
	UINT8 x[MAX_PATH], *data = NULL, *comp_buffer;
	FILE *fp;
	UINT32 offs;

	_D ("(%p)", agid);

	offs = pdb_start + palmagi.v4.v3.vol[2*agid->volume];
	/* loading a bad resource */
	if(offs==pdb_start)
		agid->offset=_EMPTY;

	if (agid->offset != _EMPTY && (fp = fopen(pdb_file, "rb")) != NULL) {
		fseek (fp, offs + agid->offset, SEEK_SET);
		fread (&x, 1, 7, fp);

		if (hilo_getword(x) != 0x1234) {
			/* FIXME */
			deinit_video_mode();
			printf("ACK! BAD RESOURCE!!!\n");
			exit(0);
		}

		agid->len = lohi_getword (x + 3);	/* uncompressed size */
		agid->clen = lohi_getword (x + 5);	/* compressed len */

		comp_buffer = calloc (1, agid->clen + 32);
		fread (comp_buffer, 1, agid->clen, fp);

		if (x[2] & 0x80 || agid->len == agid->clen) {
			/* do not decompress */
			data = comp_buffer;
		} else {
			/* it is compressed */
			data = calloc (1, agid->len + 32);
			LZW_expand (comp_buffer, data, agid->len);
			free (comp_buffer);
			agid->flags |= RES_COMPRESSED;
		}

		fclose(fp);
	} else {
		/* we have a bad volume resource */
		/* set that resource to NA */
		agid->offset = _EMPTY;
	}

	return data;
#endif
}



/*
 * Loads a resource into memory, a raw resource is loaded in
 * with above routine, then further decoded here.
 */
int agi_v4_load_resource (int restype, int resnum)
{
	int ec = err_OK;
	UINT8 *data = NULL;


	_D (_D_WARN "(restype = %d, resnum = %d)", restype, resnum);
	if (resnum > MAX_DIRS)
		return err_BadResource;

	switch (restype) {
	case rLOGIC:
		if (~game.dir_logic[resnum].flags & RES_LOADED) {
			_D (_D_WARN "loading logic resource %d", resnum);
			agi_v4.unload_resource (rLOGIC, resnum);
			/* load raw resource into data */
			if((palmagi.emulation>>12)==2)
				data = agi_v4v2_load_vol_res (&game.dir_logic[resnum]);
			else
				data = agi_v4v3_load_vol_res(&game.dir_logic[resnum]);

			ec = (logics[resnum].data = data) ?
				decode_logic (resnum) : err_BadResource;

			logics[resnum].sIP = 2;
       	}

   		logics[resnum].cIP = logics[resnum].sIP;
		break;
	case rPICTURE:
		/* if picture is currently NOT loaded *OR* cacheing is off,
		 * unload the resource (caching == off) and reload it
		 */

		_D (_D_WARN "loading picture resource %d", resnum);
		if (game.dir_pic[resnum].flags & RES_LOADED)
			break;

		/* if loaded but not cached, unload it */
		/* if cached but not loaded, etc */
		agi_v4.unload_resource (rPICTURE, resnum);

		if((palmagi.emulation>>12)==2)
		{
			data = agi_v4v2_load_vol_res (&game.dir_pic[resnum]);
    		if (data != NULL) {
    			pictures[resnum].rdata = data;
    			game.dir_pic[resnum].flags |= RES_LOADED;
    		} else {
    			ec = err_BadResource;
    		}
		}
		else
		{
			data = agi_v4v3_load_vol_res(&game.dir_pic[resnum]);
			if (data != NULL) {
				data = convert_v3_pic (data,
					game.dir_pic[resnum].len);
				pictures[resnum].rdata = data;
				game.dir_pic[resnum].flags |= RES_LOADED;
			} else {
				ec=err_BadResource;
			}
		}

		break;
	case rSOUND:
		_D (_D_WARN "loading sound resource %d", resnum);
		if (game.dir_sound[resnum].flags & RES_LOADED)
			break;

		if((palmagi.emulation>>12)==2)
			data = agi_v4v2_load_vol_res (&game.dir_sound[resnum]);
		else
			data = agi_v4v3_load_vol_res(&game.dir_sound[resnum]);

		if (data != NULL) {
			sounds[resnum].rdata = data;
			game.dir_sound[resnum].flags |= RES_LOADED;
			decode_sound (resnum);
		} else {
			ec = err_BadResource;
		}
		break;
	case rVIEW:
		/* Load a VIEW resource into memory...
		 * Since VIEWS alter the view table ALL the time
		 * can we cache the view? or must we reload it all
		 * the time?
		 */
		if (game.dir_view[resnum].flags & RES_LOADED)
			break;

		_D (_D_WARN "loading view resource %d", resnum);
    		agi_v4.unload_resource (rVIEW, resnum);

    		if((palmagi.emulation>>12)==2)
    		{
    			if ((data = agi_v4v2_load_vol_res (&game.dir_view[resnum]))) {
	    			views[resnum].rdata = data;
    				game.dir_view[resnum].flags |= RES_LOADED;
    				ec = decode_view (resnum);
    			} else {
	    			ec=err_BadResource;
				}
	    	}
	    	else
	    	{
	    		if ((data = agi_v4v3_load_vol_res (&game.dir_view[resnum]))) {
    				views[resnum].rdata = data;
    				game.dir_view[resnum].flags |= RES_LOADED;
    				ec = decode_view (resnum);
    			} else {
	    			ec=err_BadResource;
				}
	    	}
		break;
	default:
		ec = err_BadResource;
		break;
	}

	return ec;
}


UINT16 pdb_read_word(UINT8 *mem)
{
	UINT16	w;

	w = (UINT16)mem[1];
	w += (UINT16)mem[0]<<8;

	return w;
}

UINT32 pdb_read_dword(UINT8 *mem)
{
	UINT32 w;

	w = (UINT32)mem[3];
	w += (UINT32)mem[2]<<8;
	w += (UINT32)mem[1]<<16;
	w += (UINT32)mem[0]<<24;
	
	return w;
}

static int agi_v4_load_objects(char *fname)
{
	int ec=err_OK;
#ifndef PALMOS
	FILE *fp;
	UINT8	*mem;
	UINT32	len;
	UINT32	offs;

	game.num_objects=0;
	fp=fopen(pdb_file, "rb");
	if(fp!=NULL)
	{
		if((palmagi.emulation>>12)==2)
		{
			offs=palmagi.v4.v2.object[0];
			len=palmagi.v4.v2.object[1];
		}
		else
		{
			offs=palmagi.v4.v3.object[0];
			len=palmagi.v4.v3.object[1];
		}

		fseek(fp, pdb_start + offs, SEEK_SET);
		mem=(UINT8*)malloc(32+len);
		fread(mem, 1, len, fp);
		ec=decode_objects(mem, len);

		free(mem);
		fclose(fp);
	}
#endif
	return ec;
}

static int agi_v4_load_words(char *fname)
{
	int ec=err_OK;
#ifndef PALMOS
	FILE *fp;
	UINT8	*mem;
	UINT32	len;
	UINT32	offs;

	fp=fopen(pdb_file, "rb");
	if(fp!=NULL)
	{
		if((palmagi.emulation>>12)==2)
		{
			offs=palmagi.v4.v2.words[0];
			len=palmagi.v4.v2.words[1];
		}
		else
		{
			offs=palmagi.v4.v3.words[0];
			len=palmagi.v4.v3.words[1];
		}

		fseek(fp, pdb_start + offs, SEEK_SET);
		mem=(UINT8*)malloc(32+len);
		fread(mem, 1, len, fp);
		ec=decode_words(mem, len);
		free(mem);
		fclose(fp);
	}
#endif
	return ec;
}

#endif /* PALMOS */
