/*
 *	pkkrnv - main.c
 *
 *	「こころナビ」PGDコンバータ
 *	Copyright (C) 2004 Naoyuki Sawa
 *
 *	* Sat Jul 12 06:03:00 JST 2004 Naoyuki Sawa
 *	- 作成開始。
 */
#include "app.h"

/****************************************************************************
 *	マクロ・定数
 ****************************************************************************/

#define VERSION		"20040612"

#define REG_KEY		"Software\\Q-X\\こころナビ"
#define REG_VALUE	"install"
#define EPK_NAME	"cg.epk"

#define ORIGINAL_WIDTH	640

/***** アーカイブ形式 (*.epk) *****/

typedef struct _EPKHEAD {
	char signature[4];	/* "EPK " */
	int data_offset;	/* 最初のデータ開始位置へのオフセット(アーカイブ先頭からの絶対位置) */
	int file_count;		/* ファイル数 */
	int _reserved1;
} EPKHEAD;
/* この後に EPKFILE files[file_count] が続きます */

typedef struct _EPKFILE {
	int name_offset;	/* ファイル名へのオフセット(アーカイブ先頭からの絶対位置) */
	int data_offset;	/* データ本体へのオフセット(アーカイブ先頭からの絶対位置) */
	int data_size;		/* データ本体のサイズ */
	int _reserved1;
} EPKFILE;

/***** ファイル形式 ("EZip") *****/

/* EPKアーカイブに格納されている各ファイルです。
 * EZIPHEADの前に、シグネチャ"EGD "が常に存在します。
 */
typedef struct _EZIPHEAD {
	char signature[4];	/* "EZip" */
	int compressed_size;	/* 圧縮データサイズ */
	int uncompressed_size;	/* 展開データサイズ */
	int _reserved1;
} EZIPHEAD;
/* この後にZlib圧縮データが続きます。 */

/***** ファイル形式 ("EDib") *****/

/* EZipファイルを展開するとできる、RGBAビットマップです。
 */
typedef struct _EDIBHEAD {
	char signature[4];	/* "EDib" */
	int width;		/* 幅  [ピクセル] */
	int height;		/* 高さ[ピクセル] */
	int _reserved1;
} EDIBHEAD;
/* この後に unsigned char pixel[4 * width * head] が続きます。 */

/****************************************************************************
 *	グローバル変数
 ****************************************************************************/

char app_dir[_MAX_PATH];	/* 「こころナビ」のインストールフォルダ名 */
char extract_name[_MAX_PATH];	/* 抽出するファイル名("S001"など)、または全展開("*") */

int opt_bmp;			/* BMP出力(テスト用) */
int opt_chr;			/* 立ち絵モード(下切り取り) */
int opt_all;			/* 抽出するファイル名に"*"が指定されたら1になります */

/****************************************************************************
 *	関数宣言
 ****************************************************************************/

void err(const char* fmt, ...);
void usage();
void extract();
BITMAPFILEHEADER* make_bitmap(const char* data);
unsigned char* bmp32to8(unsigned char* in_buf);
unsigned char* reduce(unsigned char* in_buf);

/****************************************************************************
 *	err
 ****************************************************************************/

void
err(const char* fmt, ...)
{
	va_list ap;

	va_start(ap, fmt);
	vfprintf(stderr, fmt, ap);
	va_end(ap);

	exit(1);
}

/****************************************************************************
 *	usage
 ****************************************************************************/

void
usage()
{
	fprintf(stderr, "pkkrnv - 「こころナビ(Q-X)」PGDコンバータ(%s)\n", VERSION);
	fprintf(stderr, "Copyright (C) 2004 Naoyuki Sawa\n");
	fprintf(stderr, "使い方：\n");
	fprintf(stderr, "  pkkrnv [options] <file-name>\n");
	fprintf(stderr, "\n");
	fprintf(stderr, "  ファイル名に「*」と指定すると、抽出可能な全てのファイルを抽出します。\n");
	fprintf(stderr, "  <例> pkkrnv -b *\n");
	fprintf(stderr, "       全てのファイルをBMPファイルとして抽出します。\n");
	fprintf(stderr, "\n");
	fprintf(stderr, "オプション：\n");
	fprintf(stderr, "  -b        -b2と同じです。\n");
	fprintf(stderr, "  -b1       ビットマップファイルをそのまま出力します。\n");
	fprintf(stderr, "  -b2       ビットマップファイルを縮小して出力します。\n");
	fprintf(stderr, "  -b3       ビットマップファイルを縮小・減色して出力します。\n");
	fprintf(stderr, "  -b/-b1/-b2/-b3のどれも指定しなければ、PGDファイルを出力します。\n");
	fprintf(stderr, "  -c        縮小時、縦にはみ出す場合は、下側を切り取ります。\n");
	fprintf(stderr, "  -d<path>  「こころナビ」フォルダ指定(初期値=レジストリから取得)\n");
	exit(1);
}

/****************************************************************************
 *	main
 ****************************************************************************/

int
main(int argc, char* argv[])
{
	int retval, i, n;
	char* p;
	HKEY hKey;

	/* コマンドライン解析。 */
	for(i = 1; i < argc; i++) {
		p = argv[i];
		if(p[0] == '-') { /* オプション */
			switch(p[1]) {
			case 'b': /* BMP出力 */
				if(opt_bmp) usage(); /* 二重指定 */
				if(p[2]) {
					opt_bmp = p[2] - '0';
				} else {
					opt_bmp = 2; /* -bのみなら2 */
				}
				if(opt_bmp < 1 || 3 < opt_bmp) usage();
				break;
			case 'c': /* 立ち絵モード */
				if(opt_chr) usage(); /* 二重指定 */
				opt_chr = 1;
				break;
			case 'd': /* 「こころナビ」インストールフォルダ指定 */
				if(strlen(app_dir) != 0) usage(); /* 二重指定 */
				strncpy(app_dir, &p[2], sizeof app_dir);
				break;
			default:
				usage();
			}
		} else {
			/* 抽出するファイル名 */
			if(strlen(extract_name) != 0) usage(); /* 二重指定 */
			strncpy(extract_name, p, sizeof extract_name);
		}
	}

	/* 抽出するファイル名を検査。 */
	if(strlen(extract_name) == 0) usage();
	if(strcmp(extract_name, "*") == 0) opt_all = 1; /* 全展開? */

	/* 「こころナビ」インストールフォルダを調べます。 */
	if(strlen(app_dir) == 0) {
		retval = RegOpenKeyEx(HKEY_CURRENT_USER, REG_KEY, 0, KEY_QUERY_VALUE, &hKey);
		if(retval == 0) {
			n = sizeof app_dir;
			retval = RegQueryValueEx(hKey, REG_VALUE, 0, NULL, (unsigned char*)app_dir, (unsigned long*)&n);
			RegCloseKey(hKey);
		}
		if(retval != 0) err("「こころナビ」がインストールされていないようです。");
	}

	/* ファイル展開。 */
	extract();

	return 0;
}

/****************************************************************************
 *	extract
 ****************************************************************************/

void
extract()
{
	int retval;
	int done;
	int i_file;
	FILE* fp_in;
	FILE* fp_out;
	char* file_path;
	char epk_path[_MAX_PATH];
	char bmp_path[_MAX_PATH];
	char file_fname[_MAX_FNAME];
	char cmd[_MAX_PATH];
	unsigned char* head_buffer;
	unsigned char* data_buffer;
	EPKHEAD head;
	EPKFILE file;
	BITMAPFILEHEADER* bf;

	done = 0; /* 単独展開時、指定ファイル展開後に1になります */

	/* アーカイブファイルを開きます。 */
	_makepath(epk_path, NULL, app_dir, EPK_NAME, NULL);
	fp_in = fopen(epk_path, "rb");
	if(!fp_in) err("%s: ファイルが開けません。", epk_path);

	/* まず、アーカイブヘッダだけを読み込んでみます。 */
	retval = fread(&head, sizeof(EPKHEAD), 1, fp_in);
	if(retval != 1) err("%s: アーカイブヘッダが読み込めません。", epk_path);

	/* シグネチャを検査します。 */
	if(memcmp(head.signature, "EPK ", 4) != 0) err("%s: シグネチャが不正です。", epk_path);

	/* アーカイブヘッダとファイル情報を全て読み込むための、メモリを確保します。 */
	head_buffer = (unsigned char*)calloc(head.data_offset, 1);
	if(!head_buffer) err("%s: メモリ不足です。", epk_path);

	/* アーカイブヘッダとファイル情報を全て読み込みます。 */
	rewind(fp_in); /* アーカイブヘッダももう一度読む!! */
	retval = fread(head_buffer, 1, head.data_offset, fp_in);
	if(retval != head.data_offset) err("%s: アーカイブヘッダとファイル情報が読み込めません。", epk_path);

	/* 各ファイルについて... */
	for(i_file = 0; !done && i_file < head.file_count; i_file++) {
		memcpy(&file, &head_buffer[sizeof(EPKHEAD) + sizeof(EPKFILE) * i_file], sizeof(EPKFILE));
		file_path = (char*)&head_buffer[file.name_offset];

		/* ファイル名は相対パス(あれば)と拡張子付きなので、ベース名だけを抽出します。 */
		_splitpath(file_path, NULL, NULL, file_fname, NULL);

		/* 全展開か、または指定ファイル名に一致したら... */
		if(opt_all || strcmpi(file_fname, extract_name) == 0) {

			/* 全展開なら、処理中のファイルを表示します。 */
			if(opt_all) printf("%4d: %s\n", i_file, file_path);

			/* データ本体を読み込むためのバッファを確保します。 */
			data_buffer = (unsigned char*)calloc(file.data_size, 1);
			if(!data_buffer) err("%s: メモリ不足です。", file_path);

			/* データ本体位置へシークします。 */
			retval = fseek(fp_in, file.data_offset, SEEK_SET);
			if(retval != 0) err("%s: シークエラーです。", file_path);

			/* データ本体を読み込みます。 */
			retval = fread(data_buffer, 1, file.data_size, fp_in);
			if(retval != file.data_size) err("%s: データ本体が読み込めません。", file_path);

			/* 先行シグネチャを検査します。 */
			if(memcmp(data_buffer, "EGD ", 4) != 0) err("%s: シグネチャが不正です。", file_path);

			/* ビットマップを展開します。 */
			bf = make_bitmap(&data_buffer[4]);
			if(!bf) err("%s: ビットマップ展開に失敗しました。", file_path);

			if(opt_bmp == 1) goto L10;	/* BMP出力へ */

			/* 縮小。 */
			bf = (BITMAPFILEHEADER*)reduce((unsigned char*)bf);

			if(opt_bmp == 2) goto L10;	/* BMP出力へ */

			/* グレイスケール変換。 */
			bf = (BITMAPFILEHEADER*)bmp32to8((unsigned char*)bf);
L10:
			/* ビットマップを出力します。 */
			_makepath(bmp_path, NULL, NULL, file_fname, ".bmp");
			fp_out = fopen(bmp_path, "wb");
			retval = fwrite(bf, 1, bf->bfSize, fp_out);
			if(retval != (int)bf->bfSize) err("%s: ビットマップ出力に失敗しました。", bmp_path);
			fclose(fp_out);

			/* PGD変換。 */
			if(opt_bmp == 0) {
				if(opt_chr) {	/* 立ち絵: 2bitマスク付き */
					sprintf(cmd, "dpbmpcnv -q -b -f2m \"%s\"", bmp_path); /* -q必要! */
				} else {	/* 背景: 2bitマスク無し */
					sprintf(cmd, "dpbmpcnv -q -b -f2  \"%s\"", bmp_path); /* -q必要! */
				}
				if(system(cmd) != 0) err("%s: PGD変換に失敗しました。", bmp_path);
				remove(bmp_path); /* BMPはもういらないので削除 */
			}

			/* ビットマップを開放します。 */
			free(bf);

			/* データ本体を読み込んだバッファを開放します。 */
			free(data_buffer);

			/* 単独展開なら、完了マーク。 */
			if(!opt_all) done = 1;
		}
	}

	/* アーカイブヘッダとファイル情報を読み込んだバッファを開放します。 */
	free(head_buffer);

	/* 単独展開で、指定されたエントリが見つからなければ、エラー。 */
	if(!opt_all && !done) err("%sが見つかりません。", extract_name);
}

/****************************************************************************
 *	make_bitmap
 ****************************************************************************/

BITMAPFILEHEADER*
make_bitmap(const char* data)
{
	unsigned char* uncompress_buffer = NULL;
	BITMAPFILEHEADER* bf = NULL;
	//
	int retval;
	int size;
	int x;
	int y;
	int b;
	int g;
	int r;
	int a;
	EZIPHEAD* ezip_head;
	EDIBHEAD* edib_head;
	BITMAPINFOHEADER* bi;
	unsigned char* pixel_in;
	unsigned char* pixel_out;

	/***** EZip展開 *****/

	ezip_head = (EZIPHEAD*)data;   /* ヘッダと... */
	data += sizeof(EZIPHEAD); /* その直後に圧縮データ本体があります */

	/* シグネチャを検査します。 */
	if(memcmp(ezip_head->signature, "EZip", 4) != 0) goto L_ERR;

	/* 展開バッファを確保します。 */
	uncompress_buffer = (unsigned char*)calloc(ezip_head->uncompressed_size, 1);
	if(!uncompress_buffer) goto L_ERR;

	/* 展開します。 */
	retval = pzlib_uncompress(uncompress_buffer, ezip_head->uncompressed_size, data, ezip_head->compressed_size);
	if(retval != ezip_head->uncompressed_size) goto L_ERR;

	/***** EDib展開 *****/

	edib_head = (EDIBHEAD*)uncompress_buffer;
	pixel_in = uncompress_buffer + sizeof(EDIBHEAD);

	/* シグネチャを検査します。 */
	if(memcmp(edib_head->signature, "EDib", 4) != 0) goto L_ERR;

	/* ビットマップ用メモリを確保します。 */
	size = sizeof(BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER) + 4 * edib_head->width * edib_head->height;
	bf = (BITMAPFILEHEADER*)calloc(size, 1);
	if(!bf) goto L_ERR;
	bi = (BITMAPINFOHEADER*)(bf + 1);
	pixel_out = (unsigned char*)(bi + 1);

	/* ビットマップファイルヘッダを作成します。 */
	bf->bfType = 'MB';
	bf->bfSize = size;
	bf->bfOffBits = sizeof(BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER);

	/* ビットマップヘッダを作成します。 */
	bi->biSize = sizeof(BITMAPINFOHEADER);
	bi->biWidth = edib_head->width;
	bi->biHeight = edib_head->height;
	bi->biPlanes = 1;
	bi->biBitCount = 32;

	/* ピクセルデータを格納します。 */
	for(y = 0; y < edib_head->height; y++) {
		for(x = 0; x < edib_head->width; x++) {
			/* ※NOTE: キャラ絵の透過部分は必ずしもα＝０ではなく、結構ゴミが残ってます。
			 *   αの値が非常に小さいので、ゲーム画面上では気付かなかったのだと思います。
			 *   透過/非透過の2値変換する場合はαのしきい値を16程度にする必要があるかも知れません。
			 */
			b = *pixel_in++;
			g = *pixel_in++;
			r = *pixel_in++;
			a = *pixel_in++;
			*pixel_out++ = b;
			*pixel_out++ = g;
			*pixel_out++ = r;
			*pixel_out++ = a;
		}
	}

	return bf;
L_ERR:
	free(bf);
	free(uncompress_buffer);
	return NULL;
}

/****************************************************************************
 *	bmp32to8
 ****************************************************************************/

const int DITHER[8][8] = {
         0,32, 8,40, 2,34,10,42,
        48,16,56,24,50,18,58,26,
        12,44, 4,36,14,46, 6,38,
        60,28,52,20,62,30,54,22,
         3,35,11,43, 1,33, 9,41,
        51,19,59,27,49,17,57,25,
        15,47, 7,39,13,45, 5,37,
        63,31,55,23,61,29,53,21,
};

unsigned char*
bmp32to8(unsigned char* in_buf)
{
	int w, h, x, y, r, g, b, a, c;

	BITMAPFILEHEADER* bf_in;
	BITMAPINFOHEADER* bi_in;
	int stride_in;
	unsigned char *bits_in, *pin;

	int out_size;
	unsigned char* out_buf;
	BITMAPFILEHEADER* bf_out;
	BITMAPINFOHEADER* bi_out;
	RGBQUAD* rgb_out;
	int stride_out;
	unsigned char *bits_out, *pout;

	/* 入力ビットマップ情報を取得。 */
	bf_in   = (BITMAPFILEHEADER*)(in_buf);
	bi_in   = (BITMAPINFOHEADER*)(in_buf + sizeof(BITMAPFILEHEADER));
	bits_in = (unsigned char   *)(in_buf + bf_in->bfOffBits);
	w = bi_in->biWidth;
	h = bi_in->biHeight;
	stride_in = 4 * w;

	/* 出力ビットマップ情報を作成。 */
	stride_out = (w + 3) & ~3;
	out_size = sizeof(BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER) + sizeof(RGBQUAD) * 256 + stride_out * h;
	out_buf = (unsigned char*)calloc(out_size, 1);
	if(out_buf == NULL) err("ビットマップ変換用バッファが確保できません。");
	bf_out   = (BITMAPFILEHEADER*)(out_buf);
	bi_out   = (BITMAPINFOHEADER*)(out_buf + sizeof(BITMAPFILEHEADER));
	rgb_out  = (RGBQUAD         *)(out_buf + sizeof(BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER));
	bits_out = (unsigned char   *)(out_buf + sizeof(BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER) + sizeof(RGBQUAD) * 256);

	bf_out->bfType = 'MB';
	bf_out->bfSize = out_size;
	bf_out->bfOffBits = sizeof(BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER) + sizeof(RGBQUAD) * 256;

	bi_out->biSize = sizeof(BITMAPINFOHEADER);
	bi_out->biWidth = w;
	bi_out->biHeight = h;
	bi_out->biPlanes = 1;
	bi_out->biBitCount = 8;

	for(c = 0; c < 4; c++) {
		rgb_out[c].rgbRed       = 85 * (3 - c);
		rgb_out[c].rgbGreen     = 85 * (3 - c);
		rgb_out[c].rgbBlue      = 85 * (3 - c);
		rgb_out[c].rgbReserved  = 0;
	}
	for(c = 4; c < 256; c++) {
		rgb_out[c].rgbRed       = 0;
		rgb_out[c].rgbGreen     = 255;
		rgb_out[c].rgbBlue      = 0;
		rgb_out[c].rgbReserved  = 0;
	}

	for(y = 0; y < h; y++) {
		for(x = 0; x < w; x++) {
			pin  = bits_in  + stride_in  * (h - 1 - y) + 4 * x;
			pout = bits_out + stride_out * (h - 1 - y) + 1 * x;
			;
			b = *pin++;
			g = *pin++;
			r = *pin++;
			a = *pin++;
			;
			if(a >= 16/*「こころナビ」では、キャラ絵の透過部にゴミがあるので、少ししきい値を高めにする必要あり*/) {
				c = (r * 3 + g * 5 + b * 2) / 10;
				c = (c + DITHER[y & 7][x & 7] + (85 - 63) / 2) / 85;
				if(c > 3) c = 3;
				c = 3 - c;
			} else {
				c = 4;
			}
			;
			*pout++ = c;
		}
	}

	/* 入力ビットマップを開放し、代わりに出力ビットマップを返します。 */
	free(in_buf);
	return out_buf;
}

/****************************************************************************
 *	reduce
 ****************************************************************************/

#define WIDTH_LIMIT	128
#define HEIGHT_LIMIT	 88

unsigned char*
reduce(unsigned char* in_buf)
{
	int x_in, y_in, x_out, y_out, r, g, b, a, n, x_base, y_base;
	double REDUCE_LEVEL;

	int w_in;
	int h_in;
	BITMAPFILEHEADER* bf_in;
	BITMAPINFOHEADER* bi_in;
	int stride_in;
	unsigned char *bits_in, *pin;

	int out_size;
	int w_out;
	int h_out;
	unsigned char* out_buf;
	BITMAPFILEHEADER* bf_out;
	BITMAPINFOHEADER* bi_out;
	int stride_out;
	unsigned char *bits_out, *pout;

	/* 入力ビットマップ情報を取得。 */
	bf_in   = (BITMAPFILEHEADER*)(in_buf);
	bi_in   = (BITMAPINFOHEADER*)(in_buf + sizeof(BITMAPFILEHEADER));
	bits_in = (unsigned char   *)(in_buf + bf_in->bfOffBits);
	w_in = bi_in->biWidth;
	h_in = bi_in->biHeight;
	stride_in = 4 * w_in;

	/* 立ち絵モード時は比率固定。 */
	if(opt_chr) {
		REDUCE_LEVEL = (float)ORIGINAL_WIDTH / WIDTH_LIMIT;
	/* 背景モード時は比率可変。 */
	} else {
		double rl1 = (double)w_in / WIDTH_LIMIT;
		double rl2 = (double)h_in / HEIGHT_LIMIT;
		REDUCE_LEVEL = rl1 <= rl2 ? rl1 : rl2; /* 画面いっぱいになるように */
	}

	/* 出力ビットマップ情報を作成。 */
	w_out = (int)(w_in / REDUCE_LEVEL);
	h_out = (int)(h_in / REDUCE_LEVEL);
	x_base = 0;
	y_base = 0;
	if(w_out > WIDTH_LIMIT) {
		x_base = (int)((w_out - WIDTH_LIMIT) / 2 * REDUCE_LEVEL);
		w_out = WIDTH_LIMIT;
	}
	if(h_out > HEIGHT_LIMIT) {
		if(!opt_chr) y_base = (int)((h_out - HEIGHT_LIMIT) / 2 * REDUCE_LEVEL);
		h_out = HEIGHT_LIMIT;
	}
	stride_out = 4 * w_out;
	out_size = sizeof(BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER) + stride_out * h_out;
	out_buf = (unsigned char*)calloc(out_size, 1);
	if(out_buf == NULL) err("ビットマップ変換用バッファが確保できません。");
	bf_out   = (BITMAPFILEHEADER*)(out_buf);
	bi_out   = (BITMAPINFOHEADER*)(out_buf + sizeof(BITMAPFILEHEADER));
	bits_out = (unsigned char   *)(out_buf + sizeof(BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER));

	bf_out->bfType = 'MB';
	bf_out->bfSize = out_size;
	bf_out->bfOffBits = sizeof(BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER);

	bi_out->biSize = sizeof(BITMAPINFOHEADER);
	bi_out->biWidth = w_out;
	bi_out->biHeight = h_out;
	bi_out->biPlanes = 1;
	bi_out->biBitCount = 32;

	/* 縮小処理。 */
	for(y_out = 0; y_out < h_out; y_out++) {
		for(x_out = 0; x_out < w_out; x_out++) {
			/* 縮小元のピクセルを加算。 */
			r = 0;
			g = 0;
			b = 0;
			a = 0;
			n = 0;
			for(y_in = y_base + (int)(y_out * REDUCE_LEVEL); y_in < y_base + (int)((y_out + 1) * REDUCE_LEVEL); y_in++) {
				if(y_in < 0 || h_in - 1 < y_in) continue;
				for(x_in = x_base + (int)(x_out * REDUCE_LEVEL); x_in < x_base + (int)((x_out + 1) * REDUCE_LEVEL); x_in++) {
					if(x_in < 0 || w_in - 1 < x_in) continue;
					pin  = bits_in  + stride_in  * (h_in - 1 - y_in) + 4 * x_in;
					b += *pin++;
					g += *pin++;
					r += *pin++;
					a += *pin++;
					n++;
				}
			}
			/* 平均して縮小後のピクセルへ。 */
			pout  = bits_out + stride_out  * (h_out - 1 - y_out) + 4 * x_out;
			if(n != 0) {
				r = (r + n / 2) / n;
				g = (g + n / 2) / n;
				b = (b + n / 2) / n;
				a = (a + n / 2) / n;
			}
			*pout++ = b;
			*pout++ = g;
			*pout++ = r;
			*pout++ = a;
		}
	}

	/* 入力ビットマップを開放し、代わりに出力ビットマップを返します。 */
	free(in_buf);
	return out_buf;
}

