/*
------------------------------------------------------------
   Kobo Deluxe - An enhanced SDL port of XKobo
------------------------------------------------------------
 * Copyright (C) 1995, 1996 Akira Higuchi
 * Copyright (C) 2002 Jeremy Sheeley
 * Copyright (C) 2001-2003, 2007 David Olofson
 * 
 * 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; either version 2 of the License, or (at your
 * option) any later version.
 * 
 * This program 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
 * General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 * 675 Mass Ave, Cambridge, MA 02139, USA.
 */

#define	DBG(x)
#define	DBG2(x)
#define	DBG3(x)

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>

#if HAVE_DIRENT_H
# include <dirent.h>
# define NAMLEN(dirent) strlen((dirent)->d_name)
#else
# define dirent direct
# define NAMLEN(dirent) (dirent)->d_namlen
# if HAVE_SYS_NDIR_H
#  include <sys/ndir.h>
# endif
# if HAVE_SYS_DIR_H
#  include <sys/dir.h>
# endif
# if HAVE_NDIR_H
#  include <ndir.h>
# endif
#endif

#if !defined(WIN32) && !defined(MACOS)
#include <pwd.h>
#endif

#include "kobolog.h"
#include "kobo.h"
#include "score.h"

score_manager_t scorefile;


/*----------------------------------------------------------
	s_hiscore_t
----------------------------------------------------------*/

s_hiscore_t::s_hiscore_t()
{
	profile = NULL;
	name[0] = 0;
	clear();
}

void s_hiscore_t::clear()
{
	start_date = 0;
	end_date = 0;
	skill = -1;
	score = 0;
	start_scene = -1;
	end_scene = -1;
	end_lives = -1;
	end_health = -1;
	playtime = 0;
	saves = loads = -1;
	gametype = -1;
}

void s_hiscore_t::read(pfile_t &pf)
{
	pf.read(start_date);
	pf.read(end_date);
	pf.read(skill);
	pf.read(score);
	pf.read(start_scene);
	pf.read(end_scene);
	pf.read(end_lives);
	pf.read(end_health);
	pf.read(playtime);
	pf.read(saves);
	pf.read(loads);
	pf.read(gametype);
#if 0
	log_printf(DLOG, "--------\n");
	log_printf(DLOG, "  start_date: %u\n", start_date);
	log_printf(DLOG, "  end_date: %u\n", end_date);
	log_printf(DLOG, "  skill: %d\n", skill);
	log_printf(DLOG, "  gametype: %d\n", gametype);
	log_printf(DLOG, "  score: %u\n", score);
	log_printf(DLOG, "  start_scene: %d\n", start_scene);
	log_printf(DLOG, "  end_scene: %d\n", end_scene);
	log_printf(DLOG, "  end_lives: %d\n", end_lives);
	log_printf(DLOG, "  end_health: %d\n", end_health);
	log_printf(DLOG, "  playtime: %u\n", playtime);
	log_printf(DLOG, "  saves: %d\n", saves);
	log_printf(DLOG, "  loads: %d\n", loads);
	log_printf(DLOG, "--------\n");
#endif
}

void s_hiscore_t::write(pfile_t &pf)
{
	pf.write(start_date);
	pf.write(end_date);
	pf.write(skill);
	pf.write(score);
	pf.write(start_scene);
	pf.write(end_scene);
	pf.write(end_lives);
	pf.write(end_health);
	pf.write(playtime);
	pf.write(saves);
	pf.write(loads);
	pf.write(gametype);
}


/*----------------------------------------------------------
	s_profile_t
----------------------------------------------------------*/

s_profile_t::s_profile_t()
{
	for(int i = 0; i < HISCORE_SAVE_MAX; ++i)
		hiscoretab[i].profile = this;
	filename = NULL;
}

s_profile_t::~s_profile_t()
{
	free(filename);
}

void s_profile_t::clear()
{
	best_score = 0;
	last_scene = 0;
	name[0] = 0;
	version = 0;
	skill = SKILL_CLASSIC;
	handicap = 0;
	color1 = -1;
	color2 = -1;
	hiscores = 0;
	for(int i = 0; i < HISCORE_SAVE_MAX; ++i)
		hiscoretab[i].clear();
}

int s_profile_t::load(const char *fn)
{
	log_printf(D3LOG, "s_profile_t::load('%s')\n", fn);

	free(filename);
	filename = strdup(fn);

	FILE *f = fopen(filename, "rb");
	if(!f)
	{
		log_printf(ELOG, "Failed to open player profile '%s'!\n", fn);
		return -1;
	}

	pfile_t pf(f);

	clear();

	// Check if this is an old broken 73 byte file written by a
	// previous version for Win32. If it is, skip the first byte.
	fseek(f, 0, SEEK_END);
	if(ftell(f) == 73)
	{
		log_printf(ELOG, "Broken score file '%s' detected and fixed.\n", fn);
		fseek(f, 1, SEEK_SET);
	}
	else
		fseek(f, 0, SEEK_SET);

	pf.read(best_score);
	pf.read(last_scene);
	pf.read(name, sizeof(name));
	log_printf(DLOG, "name: %s\n", name);
	log_printf(DLOG, "  best_score: %u\n", best_score);
	log_printf(DLOG, "  last_scene: %u\n", last_scene);
	pf.buffer_close();

	if(pf.status() < 0)
	{
		log_printf(ELOG, "Error reading player profile '%s'!\n", fn);
		fclose(f);
		return -1;
	}

	//Kobo Deluxe profile file format 1+
	int chunks = 0;
	while(!feof(f))
	{
		if(pf.chunk_read() < 0)
		{
			pf.status();	// Just EOF - no big deal...
			break;
		}
		switch(pf.chunk_type())
		{
		  case MAKE_4CC('P', 'R', 'O', 'F'):
			log_printf(D2LOG, "Chunk 'PROF'; %d bytes.\n",
					pf.chunk_size());
			if(pf.chunk_size() >= 20)
			{
				pf.read(version);
				pf.read(skill);
				pf.read(handicap);
				pf.read(color1);
				pf.read(color2);
			}
			else
			{
				log_printf(WLOG, "WARNING: Truncated 'PROF' chunk"
						" in player profile '%s'!\n", fn);
			}
			break;
		  case MAKE_4CC('H', 'I', 'S', 'C'):
			log_printf(D2LOG, "Chunk 'HISC'; %d bytes.\n",
					pf.chunk_size());
			if(hiscores < HISCORE_SAVE_MAX)
				hiscoretab[hiscores++].read(pf);
			break;
		  default:
			{
				char tp[5];
				int t = pf.chunk_type();
				tp[0] = (t >> 24) & 0xff;
				tp[1] = (t >> 16) & 0xff;
				tp[2] = (t >> 8) & 0xff;
				tp[3] = t & 0xff;
				tp[4] = 0;
				log_printf(D2LOG, "Unknown chunk '%s'; %d bytes.\n",
						tp, pf.chunk_size());
			}
			break;
		}
		++chunks;
		pf.chunk_end();
	}

	if(!chunks)
	{
		log_printf(D2LOG, "Old scorefile.\n");
		//Construct a "fake" highscore entry from the old data.
		hiscores = 1;
		hiscoretab[0].skill = SKILL_CLASSIC;	//Can't tell speed.
		hiscoretab[0].gametype = GAME_SINGLE;	//No others available.
		hiscoretab[0].score = best_score;
		hiscoretab[0].end_scene = last_scene;	//Likely, but...
		hiscoretab[0].saves = 0;		//Not implemented.
		hiscoretab[0].loads = 0;		//Not implemented.
	}

	fclose(f);
	return pf.status();
}


int s_profile_t::save()
{
	log_printf(D3LOG, "s_profile_t::save('%s')\n", filename);
#ifndef	WIN32
	umask(022);
#endif
	if(!filename)
	{
		log_printf(ELOG, "Failed to save player profile - no file name!\n");
		return -1;
	}

//The "safe list"; platforms that do not have symlinks:
#if !defined(WIN32)
#ifdef HAVE_STAT
	// We will not write score files via symlinks!
	struct stat statbuf;
	if(lstat(filename, &statbuf) < 0)
	{
		log_printf(ELOG, "Cannot stat player profile '%s'! "
				"Could be a symlink - will NOT write.\n",
				filename);
		return -1;	
	}
	if( (statbuf.st_mode & S_IFLNK) == S_IFLNK )
	{
		log_printf(ELOG, "Player profile '%s' is a symlink! "
				"I will NOT write through symlinks.\n",
				filename);
		return -1;	
	}
#else
#warning ================= SECURITY HAZARD =================
#warning If this platform has symlinks or similar, please
#warning add an appropriate test. If not, add your platform
#warning to the "safe list". Either way, if you read this,
#warning post a bug report to the Kobo Deluxe maintainer.
#warning ================= SECURITY HAZARD =================
#error (Remove this line to compile anyway.)
#endif	/* HAVE_STAT */
#endif	/* "Safe list" */

	FILE *f = fopen(filename, "wb");
	if(!f)
	{
		log_printf(ELOG, "Failed to create player profile '%s'!\n",
				filename);
		return -1;
	}

	fseek(f, 0, SEEK_SET);

	pfile_t pf(f);

	// Write old XKobo stuff
	pf.write(best_score);
	pf.write(last_scene);
	pf.write(name, sizeof(name));
	pf.buffer_write();

	// Write profile header
	if(pf.chunk_write(MAKE_4CC('P', 'R', 'O', 'F')) < 0)
	{
		fclose(f);
		return pf.status();
	}
	pf.write((unsigned int)PROFILE_VERSION);
	pf.write(skill);
	pf.write(handicap);
	pf.write(color1);
	pf.write(color2);
	pf.chunk_end();

	// Write highscores
	for(unsigned int i = 0; i < hiscores; ++i)
	{
		if(pf.chunk_write(MAKE_4CC('H', 'I', 'S', 'C')) < 0)
		{
			fclose(f);
			return pf.status();
		}
		hiscoretab[i].write(pf);
		pf.chunk_end();
	}

	fclose(f);
	return pf.status();
}


s_hiscore_t *s_profile_t::best_hiscore()
{
	if(!hiscores)
		return NULL;

	s_hiscore_t *hs = hiscoretab;
	for(unsigned int i = 1; i < hiscores; ++i)
		if(hiscoretab[i].score > hs->score)
			hs = hiscoretab + i;

	return hs;
}


/*----------------------------------------------------------
	score_manager_t
----------------------------------------------------------*/

score_manager_t::score_manager_t()
{
}


score_manager_t::~score_manager_t()
{
}


void score_manager_t::init()
{
	const char *p;

	// The logic to this is:
	// 1.  Check to see if they have an old score file.  
	// If they do, rename it to follow the new naming format
	// which is X.UID.  On Win32 (which doesn't have a UID), the
	// UID is set to 42.
	// 2.  Load all of the X.UID scores into our array.
	// 3.  Determine what the last used file was.

	// Change as of 0.4-pre7: "score files" are upgraded
	// to "player profile files", which contain IFF style
	// chunks beyond the 74 XKobo compatible bytes.

#ifdef WIN32
/*
FIXME: This should be properly implemented on Windoze - if we
FIXME: decide that it's actually much point. (Users, security etc
FIXME: are rather foreign matters to the average winuser...)
*/
	int nUID = 42;
#else
	uid_t nUID = getuid();
#endif
	// Now load all of the files.  Start with trying to get the old
	// file name first. That file's information will become games[0],
	// and then the old file name will be deleted.

	int numberSaveGamesFound = 0;
	for(int i = 0; i < MAX_PROFILES; i++)
		profiles[i].clear();

	char buf[1024];
	snprintf(buf, sizeof(buf), "SCORES>>%u", nUID);
	p = fmap->get(buf);
	if(p)
	{
		if(profiles[numberSaveGamesFound].load(p) < 0)
			log_printf(WLOG, "WARNING: Can't read score file!\n");
		else
		{
			numberSaveGamesFound = 1;
			log_printf(ULOG, "Old score file \"%s\" found"
					" and imported.\n", p);
		}
	}

	// Search through all registered score directories
	// for all of the files that end in .UID
	struct stat st_buffer;
	fmap->get_all("SCORES>>", FM_DIR);
	char UIDbuf[12];
	snprintf(UIDbuf, sizeof(UIDbuf), ".%u", nUID);
	while(1)
	{
		const char *path = fmap->get_next();
		if(!path)
			break;

		// if the last characters in the file name is the string
		// UIDbuf...
		char *tmpptr = strstr((char *)path, UIDbuf);
		if(tmpptr != NULL && (*(tmpptr + strlen(UIDbuf)) == '\0'))
		{
			if(stat(path, &st_buffer))
				continue;
			if(!S_ISREG(st_buffer.st_mode))
				continue;

			if(profiles[numberSaveGamesFound].load(path) < 0)
				continue;

			if((++numberSaveGamesFound) >= MAX_PROFILES)
				break;
		}
	}

	numProfiles = numberSaveGamesFound;

	// If there are no save files, introbase in states.cpp will take
	// them to the enter_name screen.

	gather_high_scores();
	print_high_scores();
}


int score_manager_t::addPlayer(const char *name)
{
	log_printf(D3LOG, "creating profile %d\n", numProfiles);
	if(numProfiles == MAX_PROFILES)
		return -1;
	int ret = 0;
#ifdef WIN32
	int userid = 42;
#else
	uid_t userid = getuid();
#endif
	profiles[numProfiles].clear();
	strncpy(profiles[numProfiles].name, name, SCORE_NAME_LEN);

// TODO: Somewhere around here would be a nice place to check if the
// TODO: scoredir exists, and if not, try to create one.

	// This means that we always create new profiles in the first
	// scoredir registered. (Loaded score files are written back
	// where they were read from when changed.)
	char buf[1024];
	snprintf(buf, sizeof(buf), "SCORES>>%s.%u", name, userid);
	const char *fn = fmap->get(buf, FM_FILE_CREATE);
	free(profiles[numProfiles].filename);
	if(fn)
		profiles[numProfiles].filename = strdup(fn);
	else
	{
		profiles[numProfiles].filename = NULL;
		log_printf(ELOG, "ERROR: Couldn't resolve player profile"
				" path '%s'!\n", buf);
		ret = -2;
	}

	log_printf(ULOG, "Created new player profile '%s' for player '%s'\n",
			fn ? fn : "<NO FILE!>", profiles[numProfiles].name);
	currentProfile = numProfiles;

	// Try to create the high score file.
	if(profiles[numProfiles].save() < 0)
		ret = -3;

	numProfiles++;
	return ret;
}


void score_manager_t::select_profile(int prof)
{
	log_printf(D3LOG, "score_manager_t::select_profile(%d)\n", prof);
	currentProfile = prof;
}


// Rules:
//	* Highest score gets highest priority.
//	* If the maximum number of entries is
//	  exceeded, the lowest score entry is
//	  overwritten.
//	* best_score and last_scene of the
//	  profile are bumped if exceeded.
void score_manager_t::record(s_hiscore_t *entry, int force)
{
	log_printf(D3LOG, "score_manager_t::record\n");
	s_profile_t *p = profiles + currentProfile;
	int write;
	if(force)
		write = 1;
	else
		write = 0;

	if(entry->score > p->best_score)
	{
		p->best_score = entry->score;
		write = 1;
	}

	if(entry->end_scene > p->last_scene)
	{
		p->last_scene = entry->end_scene;
		write = 1;
	}

	int si = -1;
	if(p->hiscores < HISCORE_SAVE_MAX)
	{
		// Grab next free slot
		si = p->hiscores++;
	}
	else
	{
		// Find worst score
//FIXME: Pick old or new when scores are equal?
		unsigned int worst_score = entry->score;
		for(int i = 0; i < HISCORE_SAVE_MAX; ++i)
			if(p->hiscoretab[i].score <= worst_score)
			{
				si = i;
				worst_score = p->hiscoretab[i].score;
			}
	}
	if(si >= 0)
	{
		// Copy and "fix" entry...
		log_printf(DLOG, "Saving in slot %d.\n", si);
		p->hiscoretab[si] = *entry;
		p->hiscoretab[si].profile = p;
		memcpy(p->hiscoretab[si].name, p->name, SCORE_NAME_LEN);
		write = 1;
	}
	else
		log_printf(DLOG, "Not saved.\n");

	if(write)
	{
		log_printf(DLOG, "Writing file %s for player %s...\n",
				p->filename, p->name);
		p->save();
		gather_high_scores();
		print_high_scores();
		log_printf(DLOG, "  Done!\n");
	}
}


int s_table_cmp(const void *_a, const void *_b)
{
	s_hiscore_t *a = (s_hiscore_t *)_a;
	s_hiscore_t *b = (s_hiscore_t *)_b;
#if 0
	if(a->gametype < b->gametype)
		return 1;
	else if(a->gametype > b->gametype)
		return -1;
	else
#endif
	if(a->score < b->score)
		return 1;
	else if(a->score > b->score)
		return -1;
	return 0;
}


void score_manager_t::gather_high_scores(int placeholders)
{
	const int sc[] = {
		10000, 9000, 8000, 7000, 6000,
		5000, 4000, 3000, 2000, 1000,
		0
	};
	const int st[] = {
		10, 9, 8, 7, 6,
		5, 4, 3, 2, 1
	};
	const char *nm[] = {
		"DAVID OLOFSON",
		"AKIRA HIGUCHI",
		"MASANAO IZUMO",
		"MAX HORN",
		"SAMUEL HART",
		"ANDREAS",
		"TRICK",
		"RIKI",
		"GEORGIE",
		"JEREMY"
	};

	for(int i = 0; i < MAX_HIGHSCORES; ++i)
		high_tbl[i].clear();

	s_profile_t p;
	highs = 0;
	fmap->get_all("SCORES>>", FM_DIR);
	while(highs < MAX_HIGHSCORES)
	{
		const char *path = fmap->get_next();
		if(!path)
			break;

		if(p.load(path) < 0)
			continue;

		s_hiscore_t *hs = p.best_hiscore();
		if(!hs)
			continue;

		high_tbl[highs] = *hs;
		memcpy(high_tbl[highs].name, p.name, SCORE_NAME_LEN);
		high_tbl[highs].profile = NULL;
		++highs;
	}

	if(placeholders)
	{
		//Throw some nice looking names in, if there's room. ;-)
		int i = 0;
		while(highs < MAX_HIGHSCORES)
		{
			if(!sc[i])
				break;
			memcpy(high_tbl[highs].name, nm[i], SCORE_NAME_LEN);
			high_tbl[highs].skill = SKILL_CLASSIC;
			high_tbl[highs].gametype = GAME_SINGLE;
			high_tbl[highs].score = sc[i];
			high_tbl[highs].start_scene = 0;
			high_tbl[highs].end_scene = st[i];
			++i;
			++highs;
		}
	}

	qsort(high_tbl, highs, sizeof(high_tbl[0]), s_table_cmp);
}


int score_manager_t::highscore(int prof)
{
	if(-1 == prof)
		return profiles[currentProfile].last_scene;
	else if(-2 == prof)
		return high_tbl[0].score;
	else
		return profiles[prof].best_score;
}


void score_manager_t::print_high_scores()
{
	if(!highs)
	{
		log_printf(WLOG, "No hiscore entries found!\n");
		return;
	}

	log_printf(ULOG, "Name                     Score Start End\n");
	for(unsigned int j = 0; j < highs; j++)
	{
		log_printf(ULOG, "%-20s %9u %5d %3d\n", high_tbl[j].name,
				high_tbl[j].score,
				high_tbl[j].start_scene,
				high_tbl[j].end_scene);
	}
}
