#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <unistd.h>
#include <limits.h>
#include <sys/stat.h>
#include <time.h>
#include <sys/time.h>

#include "u816.h"
#include "sanity.h"
#include "self.h"
#include "crunch.h"
#include "dupname.h"

int opt_append = 0;
int opt_overwrite = 0;
int opt_zerotime = 0;
int opt_alftime = 0;
int opt_gmtime = 0;
int opt_txtconv = 0;
int opt_verbose = 0;
int opt_quiet = 0;

int backup_msg = 0;
int file_count = 0;
unsigned int total_in = 0;
unsigned int total_out = 0;
struct timeval start_time;

struct stat in_file_stat;
long hdr_compsize_pos;

FILE *out_file, *in_file;
const char *out_filename, *atari_filename;

/* pcc needs this */
#ifndef PATH_MAX
#define PATH_MAX 4096
#endif

char in_filename[PATH_MAX + 1];

char hdr_filename[13];

void store_quad(int pos, unsigned long data) {
	int i;

	for(i = 0; i < 4; i++) {
		output_buf[pos++] = data & 0xff;
		data >>= 8;
	}
}

void store_cksum(void) {
	int i;
	u16 cksum = 0;

	for(i = 0; i < input_len; i++)
		cksum += input_buf[i];

	output_buf[23] = cksum & 0xff;
	output_buf[24] = cksum >> 8;
}

/* examples:
	foo => FOO.
	toolongfile => TOOLONGF. (really should be TOOLONGF.ILE)
	regular.txt => REGULAR.TXT
	too.many.dots => TOO.MAN
 */
void atarify_filename(char *result) {
	int i;
	char name[9] = { 0 }, ext[4] = { 0 }, *p;

	p = strrchr(atari_filename, '/');
	if(p)
		p++;
	else
		p = (char *)atari_filename;

	strncpy(name, p, 8);
	for(i = 0; i < 8; i++) {
		if(!name[i]) break;
		if(name[i] == '.') {
			name[i] = '\0';
			break;
		}
	}

	strcpy(result, name);
	strcat(result, ".");

	p = strchr(p, '.');
	if(p) {
		p++;
		strncpy(ext, p, 3);
		for(p = ext; *p; p++)
			if(*p == '.') *p = 0;
		strcat(result, ext);
	}

	for(p = result; *p; p++)
		*p = toupper(*p);
}

/* see Arcinfo for the gory details. */
unsigned long get_msdos_date_time(void) {
	time_t t = in_file_stat.st_mtime;
	struct tm *tm;
	int msdos_year;
	u16 ms_date, ms_time;

	if(opt_gmtime)
		tm = gmtime(&t);
	else
		tm = localtime(&t);

	tm->tm_isdst = 0;

	/* use a 0 timestamp if the year's out of range, unalf will
	   display it as <none>. */
	if(tm->tm_year < 80 || tm->tm_year > 207)
		return 0;

	msdos_year = tm->tm_year + 1900 - 1980;

	/* tm_mon + 1 because MS uses 1-12 for months, and struct tm uses 0-11 */
	ms_date = (tm->tm_mday) | (((tm->tm_mon + 1) << 5)) | (msdos_year << 9);
	ms_time = (tm->tm_sec >> 1) | (tm->tm_min << 5) | (tm->tm_hour << 11);
	return ms_date | (ms_time << 16);
}

void create_header(void) {
	unsigned long time;

	atarify_filename(hdr_filename);
	sanity_check_filename(hdr_filename);

	if(opt_alftime)
		time = 0x03130588;
	else if(opt_zerotime)
		time = 0;
	else
		time = get_msdos_date_time();

	output_buf[0] = 0x1a;
	output_buf[1] = 0x0f;
	output_buf[2] = 0x00;
	memset(&output_buf[3], 0x20, 13); /* LZ.COM space-fills this field */
	strncat((char *)&output_buf[2], hdr_filename, 13);
	store_quad(15, 0); /* compressed size, fill in later */
	store_quad(19, time);
	store_cksum();
	store_quad(25, input_len);
	output_len = 29;
}

void update_header(void) {
	store_quad(15, output_len - 29);
}

void open_input(void) {
	if(!(in_file = fopen(in_filename, "rb"))) {
		perror(in_filename);
		exit(1);
	}
}

void make_backup(void) {
	char bak[PATH_MAX + 2];
	strncpy(bak, out_filename, PATH_MAX);
	strcat(bak, "~");
	if(rename(out_filename, bak) >= 0) {
		backup_msg = 1;
	}
}

int is_printable(u8 c) {
	return (c == '\n' || c == '\t' || (c >= 0x20 && c <= 0x7c));
}

int is_text_file(void) {
	if(input_len < 2) return 0;
	return (is_printable(input_buf[0]) && is_printable(input_buf[1]));
}

void convert_eols(void) {
	int i;

	if(opt_verbose) {
		fprintf(stderr, "%s: converting EOLs/tabs in %s\n", self, in_filename);
	}

	for(i = 0; i < input_len; i++) {
		if(input_buf[i] == '\n')
			input_buf[i] = 0x9b;
		else if(input_buf[i] == '\t')
			input_buf[i] = 0x7f;
	}
}

int percent(unsigned int out, unsigned int in) {
	if(!in) return 0;
	return 100 - (int)((float)out / (float)in * 100.0);
}

void crunch_file(const char *filename_arg) {
	strncpy(in_filename, filename_arg, PATH_MAX);

	/* 1st strtok() returns in_filename itself, whether or not there's an "=",
	   *and* changes the '=' to '\0' if there is one. */
	strtok(in_filename, "=");
	/* 2nd returns everything after the "=", if there is one (or null if not, *or*
	   if it's the last char of the string) */
	atari_filename = strtok(NULL, "=");
	if(!atari_filename) atari_filename = in_filename;

	open_input();

	/* read in entire input, couldn't do it this way on the Atari */
	input_len = fread(input_buf, 1, MAX_INPUT_SIZE - 1, in_file);

	if(input_len == MAX_INPUT_SIZE - 1) {
		if(fgetc(in_file) != EOF)
			fprintf(stderr, "%s: %s: this file is too large; only compressing the first 16MB.\n", self, in_filename);
	}

	if(opt_txtconv > 1 || (opt_txtconv && is_text_file()))
		convert_eols();

	output_len = 0;
	fstat(fileno(in_file), &in_file_stat); /* for timestamp */
	fclose(in_file);

	create_header();
	if(!opt_quiet) {
		fputs("Crunching ", stdout);
		if(opt_verbose) {
			safe_print_filename(in_filename, stdout);
			fputs(" as ", stdout);
		}
		safe_print_filename(hdr_filename, stdout);

		if(!opt_verbose) putchar('\n');
		fflush(stdout);
	}

	/* crunches the entire input to memory! */
	crunch();
	update_header();

	/* don't open the output file until crunch() has succeeded once.
	   this avoids leaving 0-byte turds */
	if(!out_file) {
		if(!opt_overwrite) make_backup();
		out_file = fopen(out_filename, opt_append ? "ab" : "wb");
		if(!out_file) {
			fprintf(stderr, "%s: fatal: ", self);
			perror(out_filename);
			exit(1);
		}
		/* so -vv will correctly say "Created" if trying to append to
		   an already-existing file. */
		if(ftell(out_file) == 0) opt_append = 0;
	}

	if(fwrite(output_buf, 1, output_len, out_file) < 0) {
		fprintf(stderr, "%s: fatal: ", self);
		perror(out_filename);
		exit(1);
	}

	if(opt_verbose) {
		printf("  %u/%u (%d%%)\n",
				input_len, output_len, percent(output_len, input_len));
	}

	total_in += input_len;
	total_out += output_len;
}

void usage(void) {
	extern char *usage_msg[];
	char **line;

	puts("alf (ALF compressor) v" VERSION " by B. Watson, WTFPL.");
	printf("Usage: %s -[options] archive.alf file[=NAME] [file[=NAME] ...]\n", self);
	puts("Use file=NAME to set the filenames in the ALF archive.");
	puts("Options:");

	for(line = usage_msg; *line; line++)
		puts(*line);

	exit(0);
}

float tv_to_float(struct timeval *tv) {
	return ((float)tv->tv_sec) + (((float)tv->tv_usec) / 1000000.0);
}

void print_elapsed_time(void) {
	struct timeval end_time;
	float s, e, l, mbs;

	gettimeofday(&end_time, 0);
	end_time.tv_sec -= start_time.tv_sec;
	start_time.tv_sec = 0;

	s = tv_to_float(&start_time);
	e = tv_to_float(&end_time);
	l = e - s;

	mbs = ((float)total_in) / (1048576.0) / l;

	printf("Elapsed time: %.3fs (%.2fMB/s).\n", l, mbs);
}

int main(int argc, char **argv) {
	int opt;

	gettimeofday(&start_time, 0);
	set_self(argv[0]);

	if(argc < 2 || !strcmp(argv[1], "--help") || !strcmp(argv[1], "-h")) {
		usage();
	}

	if(!strcmp(argv[1], "--version")) {
		puts(VERSION);
		exit(0);
	}

	/* don't let getopt() print error message for us. */
	opterr = 0;

	while((opt = getopt(argc, argv, "aAt:oqvV")) != -1) {
		switch(opt) {
			case 'a': opt_txtconv++; break;
			case 'A': opt_append = 1; opt_overwrite = 1; break;
			case 'o': opt_overwrite = 1; opt_append = 0; break;
			case 'q': opt_quiet = 1; break;
			case 't': opt_zerotime = opt_alftime = opt_gmtime = 0;
						 switch(*optarg) {
							 case 'z': opt_zerotime = 1; break;
							 case 'd': opt_alftime = 1; break;
							 case 'u': opt_gmtime = 1; break;
							 default:
										  fprintf(stderr, "%s: fatal: invalid -t suboption '-%c' (try -h or --help)\n", self, *optarg);
										  exit(1);
						 }
						 break;
			case 'v': opt_verbose++; break;
			case 'V': puts(VERSION); exit(0); break;
			default:
				fprintf(stderr, "%s: fatal: invalid option '-%c' (try -h or --help)\n", self, optopt);
				exit(1);
		}
	}

	if(opt_quiet) opt_verbose = 0;

	if(optind >= argc) {
		fprintf(stderr, "%s: fatal: missing alf file argument (try -h or --help)\n", self);
		exit(1);
	}

	out_filename = argv[optind];

	optind++;

	if(optind >= argc) {
		fprintf(stderr, "%s: fatal: no filenames (nothing to compress) (try -h or --help)\n", self);
		exit(1);
	}

	while(optind < argc) {
		crunch_file(argv[optind++]);
		file_count++;
	}

	if(out_file) fclose(out_file);

	if(opt_verbose) {
		if(file_count > 1) {
			printf("Compressed %d file%s: ", file_count, file_count == 1 ? "" : "s");
			printf("%u/%u (%d%%)\n",
					total_in, total_out,
					percent(total_out, total_in));
		}
		print_elapsed_time();
		if(backup_msg) {
			printf("Backed up old '%s' to '%s~'.\n", out_filename, out_filename);
		} else if(opt_append) {
			printf("Appended to '%s'.\n", out_filename);
		} else {
			printf("Created '%s'.\n", out_filename);
		}
	}

	if(!opt_quiet)
		puts("Processing complete.");

	exit(0);
}
