/*	
 *	clipdfrg.c
 *
 *	実行時のファイルデフラグ機能を実装します。
 *
 *	CLiP - Common Library for P/ECE
 *	Copyright (C) 2001-2003 Naoyuki Sawa
 *
 *	* Wed Aug 20 06:00:00 JST 2003 Naoyuki Sawa
 *	- 1st リリース。
 *	* Tue Oct 28 20:03:00 JST 2003 Naoyuki Sawa
 *	- file_defrag()にサイズを受け取る引数を追加。
 *	* Sun Feb  6 06:09:00 JST 2005 Naoyuki Sawa
 *	- これまでは、defrag_sub()関数が処理を返すとき、画面に"ファイルを整列しています"表示が残ったままでした。
 *	  defrag_sub()成功後、画面更新前に別の要因でエラー終了すると、Defragに失敗したかのような誤解を招きます。
 *	  そこで、defrag_sub()関数が処理を返す前に、画面をクリアしておくことにしました。
 *	* Sun Jan 14 17:37:13 JST 2007 Naoyuki Sawa
 *	- アプリケーションがpceLCDDispStart()を呼ぶ前にfile_defrag()を呼び、実際にデフラグが発生した場合に、
 *	  画面表示がOffのままで、進行状況表示が見えないという不具合を修正しました。
 *	  具体的には、defrag_sub()内Syの進行状況表示セットアップ処理に、pceLCDDispStart()を追加しただけです。
 */
#include "clip.h"

/* 作業用ワークエリア
 * * フラッシュメモリからフラッシュメモリへは、直接書き込めません。
 *   sct1→バッファ, sct2→sct1, バッファ→sct2 という手順で入れ替えることはできません。
 *                   ~~~~~~~~~~これが不可
 *   従って、セクタ入れ替え用バッファが二つ必要です。
 */
typedef struct _DEFRAG {
	const SYSTEMINFO* sip;		/* システム情報 */
	pffsMASTERBLOCK msb;		/* 管理ブロック読み込み用 */
	unsigned char buf1[BLOCKSIZE];	/* セクタ入れ替え用バッファ */
	unsigned char buf2[BLOCKSIZE];	/* (同上) */
	//
	int mode;			/* 0:フラッシュメモリ書き込みなし/1:あり */
	int fatcnt;			/* ファイルシステムのセクタ数 */
	int wr_cnt;			/* フラッシュメモリに書き込んだ回数(mode=0時もカウントアップします) */
} DEFRAG;

/* セクタ番号が有効であるか、検査します。
 * [in]
 *	d		作業用ワークエリア。
 *	sct		セクタ番号。
 * [note]
 *	* セクタ番号が有効なら、何もせずに帰ります。
 *	  セクタ番号が不正なら、エラーメッセージを表示して停止します。
 *	* FAT_FREE/END/INVALID/SYSTEMも、不正なセクタ番号と見なします。
 */
#define verify_sct(d, sct)					\
	do {							\
		if((sct) < 0 || (d)->fatcnt - 1 < (sct)) DIE();	\
	} while(0)

/* セクタ番号に対応するメモリアドレスを計算します。
 * [in]
 *	d		作業用ワークエリア。
 *	sct		セクタ番号。
 * [out]
 *	戻り値		メモリアドレス。
 */
static unsigned char*
sct_address(DEFRAG* d, int sct)
{
	verify_sct(d, sct);
	return d->sip->pffs_top + BLOCKSIZE * sct;
}

/* セクタチェイン内の次のセクタを取得します。
 * [in]
 *	d		作業用ワークエリア。
 *	sct		セクタ番号。
 * [out]
 *	戻り値		次のセクタ番号。
 */
static int
next_sct(DEFRAG* d, int sct)
{
	/* 指定されたセクタ番号が、有効なセクタ番号であること。 */
	verify_sct(d, sct);

	/* 次のセクタ番号を取得。 */
	sct = d->msb.fat[sct].chain;

	/* 特別なセクタ番号か、または有効なセクタ番号であること。 */
	switch(sct) {
	case FAT_FREE:
	case FAT_END:
	case FAT_INVALID:
	case FAT_SYSTEM:
		break;
	default:
		verify_sct(d, sct);
		break;
	}

	return sct;
}

/* セクタを読み込みます。
 * [in]
 *	d		作業用ワークエリア。
 *	sct		セクタ番号。
 *	memp		読み込み先バッファ。
 */
static void
read_sct(DEFRAG* d, int sct, unsigned char* memp)
{
	unsigned char* romp;

	romp = sct_address(d, sct); /* verify_sct()を含む */
	memcpy(memp, romp, BLOCKSIZE);
}

/* セクタを書き込みます。
 * [in]
 *	d		作業用ワークエリア。
 *	sct		セクタ番号。
 *	memp		書き込み元バッファ。
 */
static void
write_sct(DEFRAG* d, int sct, const unsigned char* memp)
{
	unsigned char* romp;

	romp = sct_address(d, sct); /* verify_sct()を含む */
	if(memcmp(romp, memp, BLOCKSIZE) == 0) return; /* 既に同じ内容なら書き換え不要（滅多にないけど） */
	if(d->mode) {
		if(pceFlashErase(romp) != 0) DIE(); /* これ忘れないで！ */
		if(pceFlashWrite(romp, memp, BLOCKSIZE) != 0) DIE();
	}
	d->wr_cnt++; /* mode=0時も書き込み回数はカウントアップする（書き込みが必要かどうか判断するため） */
}

/* 指定ファイル名のディレクトリエントリを検索します。
 * [in]
 *	d		作業用ワークエリア。
 *	fname		ファイル名
 * [out]
 *	戻り値		ディレクトリエントリ。
 */
static DIRECTORY*
find_dir(DEFRAG* d, const char* fname)
{
	int i;
	DIRECTORY* dir;

	dir = &d->msb.dir[0];
	for(i = 0; i < MAXDIR; i++) {
		if(dir->name[0] != '\xff') {
			if(strcmp(dir->name, fname) == 0) return dir;
		}
		dir++;
	}
	DIE();
	/* NO RETURN */
}

/* 指定セクタを含むファイルのディレクトリエントリを検索します。
 * [in]
 *	d		作業用ワークエリア。
 *	sct		セクタ番号。
 * [out]
 *	戻り値		ディレクトリエントリ。
 */
static DIRECTORY*
find_sctdir(DEFRAG* d, int sct)
{
	int i, sct2;
	DIRECTORY* dir;

	verify_sct(d, sct);
	dir = &d->msb.dir[0];
	for(i = 0; i < MAXDIR; i++) {
		if(dir->name[0] != '\xff') {
			sct2 = dir->chain;
			while(sct2 != FAT_END) {
				if(sct2 == sct) return dir;
				sct2 = next_sct(d, sct2);
			}
		}
		dir++;
	}
	DIE();
	/* NO RETURN */
}

/* 指定セクタがセクタチェイン内で何番目に位置するかを調べます。
 * [in]
 *	d		作業用ワークエリア。
 *	dir		ディレクトリエントリ。
 *	sct		セクタ番号。
 * [out]
 *	戻り値		順番。
 */
static int
find_sct(DEFRAG* d, DIRECTORY* dir, int sct)
{
	int sct2, ord;

	verify_sct(d, sct);
	sct2 = dir->chain;
	ord = 0;
	while(sct2 != FAT_END) {
		if(sct2 == sct) return ord;
		sct2 = next_sct(d, sct2);
		ord++;
	}
	DIE();
	/* NO RETURN */
}

/* セクタチェイン内で指定された順番のセクタを取得します。
 * [in]
 *	d		作業用ワークエリア。
 *	dir		ディレクトリエントリ。
 *	ord		順番。
 * [out]
 *	戻り値		セクタ番号。
 */
static int
get_sct(DEFRAG* d, DIRECTORY* dir, int ord)
{
	int sct;

	sct = dir->chain;
	while(ord != 0) {
		sct = next_sct(d, sct);
		ord--;
	}
	return sct;
}

/* ファイルのセクタ数を取得します。
 * [in]
 *	d		作業用ワークエリア。
 *	dir		ディレクトリエントリ。
 * [out]
 *	戻り値		セクタ数。
 */
static int
get_sctcnt(DEFRAG* d, DIRECTORY* dir)
{
	int sct, sctcnt;

	sct = dir->chain;
	sctcnt = 0;
	while(sct != FAT_END) {
		sct = next_sct(d, sct);
		sctcnt++;
	}
	return sctcnt;
}

/* セクタチェイン内から、指定されたセクタを削除します。
 * [in]
 *	d		作業用ワークエリア。
 *	dir		ディレクトリエントリ。
 *	sct		セクタ番号。
 */
static void
remove_sct(DEFRAG* d, DIRECTORY* dir, int sct)
{
	int ord, sct2, sct3;

	ord = find_sct(d, dir, sct); /* verify_sct()を含む */
	sct2 = next_sct(d, sct);
	if(ord == 0) {
		dir->chain = sct2;
	} else {
		sct3 = get_sct(d, dir, ord - 1);
		d->msb.fat[sct3].chain = sct2;
	}
	d->msb.fat[sct].chain = FAT_FREE;
}

/* セクタチェイン内の指定された位置に、セクタを追加します。
 * [in]
 *	d		作業用ワークエリア。
 *	dir		ディレクトリエントリ。
 *	ord		順番。
 *	sct		セクタ番号。
 */
static void
insert_sct(DEFRAG* d, DIRECTORY* dir, int ord, int sct)
{
	int sct2, sct3;

	verify_sct(d, sct);
	if(d->msb.fat[sct].chain != FAT_FREE) DIE();
	sct2 = get_sct(d, dir, ord);
	if(ord == 0) {
		dir->chain = sct;
	} else {
		sct3 = get_sct(d, dir, ord - 1);
		d->msb.fat[sct3].chain = sct;
	}
	d->msb.fat[sct].chain = sct2;
}

/* デフラグの初期化処理を行います。
 * [in]
 *	mode		0を指定するとフラッシュメモリへの書き込みは行わず、テストのみとなります。
 *			1を指定するとフラッシュメモリへの書き込みを行います。
 * [out]
 *	戻り値		作業用ワークエリア。
 */
static DEFRAG*
defrag_init(int mode)
{
	DEFRAG* d;

	/* 作業用ワークエリアを確保します。 */
	d = (DEFRAG*)calloc(1, sizeof(DEFRAG));
	if(d == NULL) DIE();

	/* モードを格納します。 */
	d->mode = mode;

	/* システム情報を取得します。 */
	d->sip = pceSystemGetInfo();

	/* ファイルシステムのセクタ数を求めます。 */
	d->fatcnt = (d->sip->pffs_end - d->sip->pffs_top) / BLOCKSIZE;
	if(d->fatcnt <      0) DIE();
	if(d->fatcnt > MAXFAT) DIE();

	/* 管理ブロックを読み込みます。 */
	if(sizeof(pffsMASTERBLOCK) != BLOCKSIZE) DIE(); /* BIOS1.18(2MB)またはBIOS1.20(512KB/2MB)以降、こうなっています */
	read_sct(d, 0, (unsigned char*)&d->msb);

	/* 管理ブロックを検査します。 */
	if((int)d->msb.mark.ptr != (int)d->sip->pffs_top + 4) DIE(); /* /usr/PIECE/tools/isd/isdsub.c ismPFFSInit() 参照 */
	if(strcmp(d->msb.mark.signature, "PFFS Master Block") != 0) DIE();

	return d;
}

/* デフラグの終了処理を行います。
 * [in]
 *	d		作業用ワークエリア。
 */
static void
defrag_exit(DEFRAG* d)
{
	/* 管理ブロックを書き戻します。 */
	if(sizeof(pffsMASTERBLOCK) != BLOCKSIZE) DIE(); /* BIOS1.18(2MB)またはBIOS1.20(512KB/2MB)以降、こうなっています */
	write_sct(d, 0, (unsigned char*)&d->msb);

	/* 作業用ワークエリアを開放します。 */
	free(d);
}

/* ファイルを整列します。(file_defrag()から利用)
 * [in]
 *	fname		ファイル名。
 *	mode		0を指定するとフラッシュメモリへの書き込みは行わず、テストのみとなります。
 *			1を指定するとフラッシュメモリへの書き込みを行います。
 * [out]
 *	戻り値		既にファイルが整列されていたら、0を返します。
 *			ファイルが整列されていなければ、1を返します。
 * [note]
 *	* まずmode=0(テスト)でdefrag_sub()を呼び出し、ファイルシステムが壊れていないことを確認してください。
 *	  いきなりmode=1(実行)で呼び出すと、デフラグ作業の途中でアサートした場合に危険だからです。
 *	* mode=0(テスト)でdefrag_sub()を呼び出した後、戻り値が0以外(整列されていない)だった場合のみ、
 *	  mode=1(実行)を指定してもう一度defrag_sub()を呼び出してください。
 */
static int
defrag_sub(const char* fname, int mode)
{
	DEFRAG* d;
	DIRECTORY *dir1, *dir2;
	int sctorg, sctcnt, sct1, sct2, ord1, ord2, wr_cnt;
	unsigned char* vbuff_save;
	PCEPWRSTAT ps;

	/*{{進行状況表示*/
	vbuff_save = NULL; /* 警告抑制 */
	if(mode) {
		vbuff_save = pceLCDSetBuffer(_def_vbuff);
		pceLCDDispStart(); /*{{2007/01/14追加}}*/
		pceFontSetType(0x80);
		pceFontSetTxColor(3);
		pceFontSetBkColor(-1);
		pcePowerGetStatus(&ps);
	}
	/*}}進行状況表示*/

	/* 初期化処理。 */
	d = defrag_init(mode);

	/* 整列するファイルのディレクトリエントリを検索します。 */
	dir1 = find_dir(d, fname);

	/* 整列後の先頭セクタ番号を決定します。 */
	sctorg = dir1->chain;
	sctcnt = get_sctcnt(d, dir1);
	if(sctorg + sctcnt > d->fatcnt) sctorg = d->fatcnt - sctcnt;

	/* 整列するファイルの各セクタについて… */
	sct1 = dir1->chain;
	ord1 = 0;
	while(sct1 != FAT_END) {
		/*{{進行状況表示*/
		if(mode) {
			memset(_def_vbuff, 0, DISP_X * DISP_Y);
			pceFontSetPos(0, 10);
			// pceFontPrintf()一回の呼び出しで全部書こうとすると、
			// 文字列が長すぎてハングアップします。要注意!!
			//             ０１２３４５６７８９０１２
			pceFontPrintf("ファイルを整列しています。\n");
			pceFontPrintf("\n");
			pceFontPrintf("×%sを抜かないで!\n", ps.status ? "電池" : "USBケーブル");
			pceFontPrintf("×P/ECEをリセットしないで!\n");
			pceFontPrintf("\n");
			pceFontPrintf("    進行状況(%3d/%3d)\n", ord1 + 1, sctcnt);
			pceLCDTrans();
		}
		/*}}進行状況表示*/

		sct2 = sctorg + ord1; /* 整列位置のセクタ番号 */
		if(sct1 != sct2) { /* 未整列？ */
			if(d->msb.fat[sct2].chain == FAT_FREE) {
				/*--- 整列位置のセクタが使用されていない場合 ---*/
				/* セクタを置き換えます。 */
				read_sct(d, sct1, d->buf1);	 /* セクタを読み込む */
				remove_sct(d, dir1, sct1);	 /* セクタを削除する */
				write_sct(d, sct2, d->buf1);	 /* セクタを書き戻す */
				insert_sct(d, dir1, ord1, sct2); /* セクタを追加する */
			} else {
				/*--- 整列位置のセクタが他のファイルに使用されている場合 ---*/
				/* 整列位置のセクタを含むファイルを探します。（自分自身のチェインに含まれている可能性もあります） */
				dir2 = find_sctdir(d, sct2);
				ord2 = find_sct(d, dir2, sct2);
				/* 自分自身のチェインに含まれている場合、それは後方であるはずです。（前方は整列済みのはずだから） */
				if(dir1 == dir2) {
					if(ord1 >= ord2) DIE();
				}
				/* * 二つのセクタを入れ替えます。
				 * - 両方とも自分自身のチェインに含まれている場合、sct1の方がsct2よりも前方に存在するはずです。
				 *   削除／追加時に順番が狂わないよう、削除の順番はsct2→sct1、追加の順番はsct1→sct2 とします。
				 * - 別々のファイルに含まれていた場合は削除・追加の順番は関係ないので、その場合も同じ手順としました。
				 */
				read_sct(d, sct1, d->buf1);	 /* 二つのセクタを読み込む */
				read_sct(d, sct2, d->buf2);
				remove_sct(d, dir2, sct2);	 /* sct2、sct1の順に削除する */
				remove_sct(d, dir1, sct1);
				write_sct(d, sct1, d->buf2);	 /* 二つのセクタを入れ替えて書き戻す */
				write_sct(d, sct2, d->buf1);
				insert_sct(d, dir1, ord1, sct2); /* sct1、sct2の順に追加する */
				insert_sct(d, dir2, ord2, sct1);
			}
		}
		sct1 = next_sct(d, sct2/*要注意!sct1じゃないよ!*/);
		ord1++;
	}
	if(ord1 != sctcnt) DIE();

	/*{{進行状況表示*/
	if(mode) {
		/* * Sun Feb  6 06:09:00 JST 2005 Naoyuki Sawa
		 * - これまでは、defrag_sub()関数が処理を返すとき、画面に"ファイルを整列しています"表示が残ったままでした。
		 *   defrag_sub()成功後、画面更新前に別の要因でエラー終了すると、Defragに失敗したかのような誤解を招きます。
		 *   そこで、defrag_sub()関数が処理を返す前に、画面をクリアしておくことにしました。
		 */
		memset(_def_vbuff, 0, DISP_X * DISP_Y);
		pceLCDTrans();
	}
	/*}}進行状況表示*/

	/* フラッシュメモリへの書き込み回数を取り出しておきます。 */
	wr_cnt = d->wr_cnt;

	/* 終了処理。 */
	defrag_exit(d);

	/*{{進行状況表示*/
	if(mode) {
		pceLCDSetBuffer(vbuff_save);
	}
	/*}}進行状況表示*/

	return wr_cnt;
}

/* ファイルを整列します。
 * [in]
 *	fname		ファイル名。
 *	size		ファイルサイズを格納する変数へのポインタ。
 *			ファイルサイズが必要なければ、NULLを指定してください。
 * [out]
 *	戻り値		整列したファイルの先頭メモリアドレス。
 *	*size		ファイルが存在し、連続セクタに配置されていたら、ファイルサイズを格納します。
 *			ただし、sizeにNULLポインタが指定されていた場合は、格納しません。
 */
const void*
file_defrag(const char* fname, int* size)
{
	int turbo_save;

	turbo_save = turbo(0);		/* pceFlashErase/Write()は48MHzでは動作しないので、確実に通常モードにしておく */

	if(defrag_sub(fname, 0) != 0) {	/* テストが成功し、かつ整列されていなかった場合のみ・・・ */
		defrag_sub(fname, 1);	/* 書き込みありで実行する。                               */
	}

	turbo(turbo_save);		/* 元のスピードに戻す */

	return file_address(fname, size);
}

