/*
 *	dlzss12 - LZSS compression tool
 *	Copyright (C) 2009 Naoyuki Sawa
 *
 *	* Wed Aug 26 12:44:29 JST 2009 Naoyuki Sawa
 *	- 1st リリース。
 *	* Wed Aug 26 17:57:00 JST 2009 Naoyuki Sawa
 *	- 圧縮手順を改善し、高速化しました。
 *	  これまでは、バッファ無しで圧縮処理を行なって圧縮サイズを求めた後、圧縮バッファを再確保して、再圧縮処理を行なっていました。
 *	  そのため、常に二倍の圧縮処理時間がかかってしまっていました。
 *	  今回の変更で、まず圧縮元と同じサイズで圧縮先バッファを確保して圧縮し、圧縮率が100%超過でバッファ不足となった場合のみ、
 *	  実際に必要なサイズの圧縮先バッファを再確保し、再圧縮処理を行なうよう変更しました。
 *	  ほとんどの場合は圧縮率100%以下が達成できるので、ほとんどの場合は圧縮処理一回分だけの時間で済むようになりました。
 *	* Thu Aug 27 02:34:30 JST 2009 Naoyuki Sawa
 *	- ハーフワード単位の圧縮・展開処理において、入力データサイズが奇数だった場合の処理を変更しました。
 *	- これまでは、圧縮元データが奇数だった場合、'-p'オプションが指定された場合のみ許可し、パディングを追加して圧縮していました。
 *	  展開時にはパディングの有無を判断できないため、圧縮元データにパディングが追加された状態で展開する仕様となっていました。
 *	- 今回、仕様を見直し、圧縮元データサイズが奇数か偶数かをユーザーが意識せずに、ハーフワード圧縮できるようにしました。
 *	  圧縮元データサイズが奇数の場合にも、展開後も、圧縮元データと同じ内容の奇数サイズに展開できます。
 *	  具体的な処理内容については、(unit == '2') 時の処理のコメントを参照してください。
 *	- 今回の変更に伴い、'-p'オプションは不要となったので、削除しました。
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
#include <assert.h>

#define VERSION "20090827"

/****************************************************************************
 *	
 ****************************************************************************/

int unit; /* '1' or '2' */
int mode; /* 'd' or 'e' */
char srcname[_MAX_PATH];
char dstname[_MAX_PATH];

int srclen;
void* srcbuf;
int dstlen;
void* dstbuf;
int verlen;
void* verbuf;

int LZSS_compress1(unsigned char* dst_data/*[dst_lim]*/, int dst_lim, const unsigned char* src_data/*[src_len]*/, int src_len);
int LZSS_uncompress1(unsigned char* dst_data/*[dst_lim]*/, int dst_lim, const unsigned char* src_data/*[src_len]*/, int src_len);
int LZSS_compress2(unsigned short* dst_data/*[dst_lim]*/, int dst_lim, const unsigned short* src_data/*[src_len]*/, int src_len);
int LZSS_uncompress2(unsigned short* dst_data/*[dst_lim]*/, int dst_lim, const unsigned short* src_data/*[src_len]*/, int src_len);

void
usage()
{
	printf("dlzss12 - LZSS compression tool (%s) Naoyuki Sawa\n", VERSION);
	printf("Usage: dlzss12 [-1|-2] [-d|-e] <source file name> [<destination file name>]\n");
	printf("Options:\n");
	printf("  -1 : byte     unit mode\n");
	printf("  -2 : halfword unit mode (default)\n");
	printf("  -d : decode source file\n");
	printf("  -e : encode source file (default)\n");
	exit(1);
}

/****************************************************************************
 *	
 ****************************************************************************/

int
main(int argc, char* argv[])
{
	int i;
	int c;
	char drive[_MAX_DRIVE];
	char dir[_MAX_DIR];
	char fname[_MAX_FNAME];
	char ext[_MAX_EXT];
	FILE* fp;

	/* コマンドライン解析。 */
	for(i = 1; i < argc; i++) {
		switch(argv[i][0]) {
		case '-':
		case '/':
			c = tolower(argv[i][1]);
			switch(c) {
			case '1':
			case '2':
				if(!unit) {
					unit = c;
					break;
				}
				usage();
				break;
			case 'd':
			case 'e':
				if(!mode) {
					mode = c;
					break;
				}
				usage();
				break;
			default:
				usage();
				break;
			}
			break;
		default:
			if(!srcname[0]) {
				strcpy(srcname, argv[i]);
				break;
			}
			if(!dstname[0]) {
				strcpy(dstname, argv[i]);
				break;
			}
			usage();
			break;
		}
	}
	/* "-1"も"-2"も指定されていなかったときの判断は、圧縮時と展開時で異なるので、ここではまだ判断しません。 */
	if(!unit) {
		/** no job **/
	}
	/* "-d"も"-e"も指定されていなければ、"-e"とします。 */
	if(!mode) {
		mode = 'e';
	}
	/* 入力ファイル名は必須です。 */
	if(!srcname[0]) {
		usage();
	}
	if(mode == 'd') {
		/* 入力ファイル名の拡張子が省略されてた場合、
		 * - "-1"が指定されていたら、".lz1"とします。
		 * - "-2"が指定されていたら、".lz2"とします。
		 * - "-1"も"-2"も指定されていなければ、".lz2"とします。
		 * - <例>
		 *	入力ファイル名 D:\path\fname -> 入力ファイル名 D:\path\fname.lz1 ("-1"が指定されていた場合)
		 *	入力ファイル名 D:\path\fname -> 入力ファイル名 D:\path\fname.lz2 ("-1"が指定されていた場合)
		 *	入力ファイル名 D:\path\fname -> 入力ファイル名 D:\path\fname.lz2 ("-1"も"-2"も指定されていなかった場合)
		 */
		_splitpath(srcname, drive, dir, fname, ext);
		if(!ext[0]) {
			if(unit == '1') {
				_makepath(srcname, drive, dir, fname, ".lz1");
			} else { /* '2' or 指定無し */
				_makepath(srcname, drive, dir, fname, ".lz2");
			}
		}
		/* 出力ファイル名が省略されていた場合、
		 * - 入力ファイル名の拡張子が".lz1"ならば、".lz1"を取り除いたものとします。
		 * - 入力ファイル名の拡張子が".lz2"ならば、".lz2"を取り除いたものとします。
		 * - 入力ファイル名の拡張子が".lz1"でも".lz2"でもなければ、エラーとします。
		 * - <例>
		 *	入力ファイル名 D:\path\fname.ext.lz1 -> 出力ファイル名 D:\path\fname.ext
		 *	入力ファイル名 D:\path\fname.ext.lz2 -> 出力ファイル名 D:\path\fname.ext
		 *	入力ファイル名 D:\path\fname.ext     -> エラー
		 */
		if(!dstname[0]) {
			_splitpath(srcname, drive, dir, fname, ext);
			if(!stricmp(ext, ".lz1") || !stricmp(ext, ".lz2")) {
				_makepath(dstname, drive, dir, fname, NULL);
			} else {
				fprintf(stderr, "Error: Destination file could not be supposed.\n");
				exit(1);
			}
		}
		/* "-1"も"-2"も指定されていなかった場合、
		 * - 入力ファイル名の拡張子が".lz1"ならば、暗黙に"-1"が指定されたものとします。
		 * - 入力ファイル名の拡張子が".lz2"ならば、暗黙に"-2"が指定されたものとします。
		 * - 入力ファイル名の拡張子が".lz1"でも".lz2"でもなければ、エラーとします。
		 */
		if(!unit) {
			_splitpath(srcname, NULL, NULL, NULL, ext);
			if(!stricmp(ext, ".lz1")) {
				unit = '1';
			} else if(!stricmp(ext, ".lz2")) {
				unit = '2';
			} else {
				fprintf(stderr, "Error: Unit mode could not be supposed.\n");
				exit(1);
			}
		}
	}
	if(mode == 'e') {
		/* 出力ファイル名が省略されていた場合、
		 * - "-1"が指定されていたら、入力ファイル名に".lz1"を追加したものとします。
		 * - "-2"が指定されていたら、入力ファイル名に".lz2"を追加したものとします。
		 * - "-1"も"-2"も指定されていなければ、入力ファイル名に".lz2"を追加したものとします。
		 *   <例>
		 *	入力ファイル名 D:\path\fname.ext -> 出力ファイル名 D:\path\fname.ext.lz1 ("-1"が指定されていた場合)
		 *	入力ファイル名 D:\path\fname.ext -> 出力ファイル名 D:\path\fname.ext.lz2 ("-1"が指定されていた場合)
		 *	入力ファイル名 D:\path\fname.ext -> 出力ファイル名 D:\path\fname.ext.lz2 ("-1"も"-2"も指定されていなかった場合)
		 */
		if(!dstname[0]) {
			if(unit == '1') {
				_makepath(dstname, NULL, NULL, srcname, ".lz1");
			} else { /* '2' or 指定無し */
				_makepath(dstname, NULL, NULL, srcname, ".lz2");
			}
		}
		/* 出力ファイル名の拡張子が省略されていた場合、".tlz"とします。
		 * - "-1"が指定されていたら、".lz1"とします。
		 * - "-2"が指定されていたら、".lz2"とします。
		 * - "-1"も"-2"も指定されていなければ、".lz2"とします。
		 * - <例>
		 *	出力ファイル名 D:\path\fname -> 出力ファイル名 D:\path\fname.lz1 ("-1"が指定されていた場合)
		 *	出力ファイル名 D:\path\fname -> 出力ファイル名 D:\path\fname.lz2 ("-1"が指定されていた場合)
		 *	出力ファイル名 D:\path\fname -> 出力ファイル名 D:\path\fname.lz2 ("-1"も"-2"も指定されていなかった場合)
		 */
		_splitpath(dstname, drive, dir, fname, ext);
		if(!ext[0]) {
			if(unit == '1') {
				_makepath(dstname, drive, dir, fname, ".lz1");
			} else { /* '2' or 指定無し */
				_makepath(dstname, drive, dir, fname, ".lz2");
			}
		}
		/* "-1"も"-2"も指定されていなかった場合、
		 * - 出力ファイル名の拡張子が".lz1"ならば、暗黙に"-1"が指定されたものとします。
		 * - 出力ファイル名の拡張子が".lz2"ならば、暗黙に"-2"が指定されたものとします。
		 * - 出力ファイル名の拡張子が".lz1"でも".lz2"でもなければ、エラーとします。
		 */
		if(!unit) {
			_splitpath(dstname, NULL, NULL, NULL, ext);
			if(!stricmp(ext, ".lz1")) {
				unit = '1';
			} else if(!stricmp(ext, ".lz2")) {
				unit = '2';
			} else {
				fprintf(stderr, "Error: Unit mode could not be supposed.\n");
				exit(1);
			}
		}
	}
	/* 入力ファイルと出力ファイルが同じであってはいけません。 */
	if(!stricmp(srcname, dstname)) {
		fprintf(stderr, "Error: Source file should not be same as destination file.\n");
		exit(1);
	}

	/* 入力ファイルを読み込みます。 */
	fp = fopen(srcname, "rb");
	if(!fp) {
		fprintf(stderr, "Error: Failed to open file %s.\n", srcname);
		exit(1);
	}
	fseek(fp, 0, SEEK_END);
	srclen = ftell(fp);
	if(!srclen) {
		fprintf(stderr, "Error: Source file should not be empty.");
		exit(1);
	}
	rewind(fp);
	srcbuf = malloc(srclen);
	if(!srcbuf) {
		fprintf(stderr, "Error: Failed to allocate source buffer.\n");
		exit(1);
	}
	fread(srcbuf, 1, srclen, fp);
	fclose(fp);

	if(unit == '1') {
		if(mode == 'e') {
			/* 圧縮。 */
			dstbuf = malloc(srclen); /* 圧縮率が100%以下となることを期待して、圧縮元と同じサイズで圧縮先バッファを確保し、圧縮してみる */
			if(!dstbuf) {
				fprintf(stderr, "Error: Failed to allocate destination buffer.\n");
				exit(1);
			}
			dstlen = LZSS_compress1(dstbuf, srclen, srcbuf, srclen);
			if(dstlen > srclen) { /* 圧縮率が100%超過だった場合は、実際の圧縮サイズで圧縮先バッファを再確保して、再圧縮する */
				dstbuf = realloc(dstbuf, dstlen);
				if(!dstbuf) {
					fprintf(stderr, "Error: Failed to allocate destination buffer.\n");
					exit(1);
				}
				LZSS_compress1(dstbuf, dstlen, srcbuf, srclen);
			}
			/* ベリファイ。 */
			verbuf = malloc(srclen);
			if(!verbuf) {
				fprintf(stderr, "Error: Failed to allocate verify buffer.\n");
				exit(1);
			}
			verlen = LZSS_uncompress1(verbuf, srclen, dstbuf, dstlen);
			if((verlen != srclen) || memcmp(verbuf, srcbuf, srclen)) {
				fprintf(stderr, "Internal error: Verify failed.n");
				exit(1);
			}
		}
		if(mode == 'd') {
			/* 展開。 */
			dstlen = LZSS_uncompress1(NULL, 0, srcbuf, srclen);
			dstbuf = malloc(dstlen);
			if(!dstbuf) {
				fprintf(stderr, "Error: Failed to allocate destination buffer.\n");
				exit(1);
			}
			LZSS_uncompress1(dstbuf, dstlen, srcbuf, srclen);
		}
	}
	if(unit == '2') {
		/* 入力データサイズを、ハーフワード単位に変換する。
		 * - 入力データサイズが奇数だった場合、末尾1バイトは、ハーフワード単位の圧縮・展開処理に含めない。
		 *   圧縮・展開処理が完了した後に、入力データの末尾1バイトをそのまま、出力データの末尾に追加する。
		 *   圧縮前のデータサイズが奇数だったならば、圧縮後のデータサイズも奇数となり、また、その逆も真である。
		 */
		int srclen2 = srclen / 2;
		int dstlen2 = 0;
		int verlen2 = 0;
		if(mode == 'e') {
			/* 圧縮。 */
			dstbuf = malloc(srclen2 * 2 + 1/*末尾1バイト追加用*/); /* 圧縮率が100%以下となることを期待して、圧縮元と同じサイズで圧縮先バッファを確保し、圧縮してみる */
			if(!dstbuf) {
				fprintf(stderr, "Error: Failed to allocate destination buffer.\n");
				exit(1);
			}
			dstlen2 = LZSS_compress2(dstbuf, srclen2, srcbuf, srclen2);
			if(dstlen2 > srclen2) { /* 圧縮率が100%超過だった場合は、実際の圧縮サイズで圧縮先バッファを再確保して、再圧縮する */
				dstbuf = realloc(dstbuf, dstlen2 * 2 + 1/*末尾1バイト追加用*/);
				if(!dstbuf) {
					fprintf(stderr, "Error: Failed to allocate destination buffer.\n");
					exit(1);
				}
				LZSS_compress2(dstbuf, dstlen2, srcbuf, srclen2);
			}
			/* ベリファイ。 */
			verbuf = malloc(srclen2 * 2);
			if(!verbuf) {
				fprintf(stderr, "Error: Failed to allocate verify buffer.\n");
				exit(1);
			}
			verlen2 = LZSS_uncompress2(verbuf, srclen2, dstbuf, dstlen2);
			if((verlen2 != srclen2) || memcmp(verbuf, srcbuf, srclen2 * 2)) {
				fprintf(stderr, "Internal error: Verify failed.n");
				exit(1);
			}
		}
		if(mode == 'd') {
			/* 展開。 */
			dstlen2 = LZSS_uncompress2(NULL, 0, srcbuf, srclen2);
			dstbuf = malloc(dstlen2 * 2 + 1/*末尾1バイト追加用*/);
			if(!dstbuf) {
				fprintf(stderr, "Error: Failed to allocate destination buffer.\n");
				exit(1);
			}
			LZSS_uncompress2(dstbuf, dstlen2, srcbuf, srclen2);
		}
		/* 出力データサイズを、バイト単位に変換する。 */
		dstlen = dstlen2 * 2;
		/* 入力データサイズが奇数だった場合は、入力データの末尾1バイトをそのまま、出力データの末尾に追加する。
		 * 出力データバッファの確保時に、末尾1バイト追加用の領域を予約済みである。
		 */
		if(srclen & 1) {
			((unsigned char*)dstbuf)[dstlen++] = ((unsigned char*)srcbuf)[srclen - 1];
		}
	}

	/* 出力ファイルを書き込みます。 */
	fp = fopen(dstname, "wb");
	if(!fp) {
		fprintf(stderr, "Error: Failed to create file %s.\n", dstname);
		exit(1);
	}
	fwrite(dstbuf, 1, dstlen, fp);
	fclose(fp);

	/* レポート表示。 */
	printf("%s (%d) => %s (%d) : %d%%\n",
		srcname, srclen,
		dstname, dstlen,
		(int)ceil((double)dstlen * 100.0 / (double)srclen));

	return 0;
}
