/*
 * hdhomerun_config.c
 *
 * Copyright © 2006-2008 Silicondust USA Inc. <www.silicondust.com>.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 */

#include "hdhomerun.h"

/*
 * The console output format should be set to UTF-8, however in XP and Vista this breaks batch file processing.
 * Attempting to restore on exit fails to restore if the program is terminated by the user.
 * Solution - set the output format each printf.
 */
#if defined(__WINDOWS__)
#define printf console_printf
#define vprintf console_vprintf
#endif

static const char *appname;

struct hdhomerun_device_t *hd;

static int help(void)
{
	printf("Usage:\n");
	printf("\t%s discover\n", appname);
	printf("\t%s <id> get help\n", appname);
	printf("\t%s <id> get <item>\n", appname);
	printf("\t%s <id> set <item> <value>\n", appname);
	printf("\t%s <id> scan <tuner> [<filename>]\n", appname);
	printf("\t%s <id> save <tuner> <filename>\n", appname);
	printf("\t%s <id> upgrade <filename>\n", appname);
	return -1;
}

static void extract_appname(const char *argv0)
{
	const char *ptr = strrchr(argv0, '/');
	if (ptr) {
		argv0 = ptr + 1;
	}
	ptr = strrchr(argv0, '\\');
	if (ptr) {
		argv0 = ptr + 1;
	}
	appname = argv0;
}

static bool_t contains(const char *arg, const char *cmpstr)
{
	if (strcmp(arg, cmpstr) == 0) {
		return TRUE;
	}

	if (*arg++ != '-') {
		return FALSE;
	}
	if (*arg++ != '-') {
		return FALSE;
	}
	if (strcmp(arg, cmpstr) == 0) {
		return TRUE;
	}

	return FALSE;
}

static uint32_t parse_ip_addr(const char *str)
{
	unsigned int a[4];
	if (sscanf(str, "%u.%u.%u.%u", &a[0], &a[1], &a[2], &a[3]) != 4) {
		return 0;
	}

	return (uint32_t)((a[0] << 24) | (a[1] << 16) | (a[2] << 8) | (a[3] << 0));
}

static int discover_print(char *target_ip_str)
{
	uint32_t target_ip = 0;
	if (target_ip_str) {
		target_ip = parse_ip_addr(target_ip_str);
		if (target_ip == 0) {
			fprintf(stderr, "invalid ip address: %s\n", target_ip_str);
			return -1;
		}
	}

	struct hdhomerun_discover_device_t result_list[64];
	int count = hdhomerun_discover_find_devices_custom(target_ip, HDHOMERUN_DEVICE_TYPE_TUNER, HDHOMERUN_DEVICE_ID_WILDCARD, result_list, 64);
	if (count < 0) {
		fprintf(stderr, "error sending discover request\n");
		return -1;
	}
	if (count == 0) {
		printf("no devices found\n");
		return 0;
	}

	int index;
	for (index = 0; index < count; index++) {
		struct hdhomerun_discover_device_t *result = &result_list[index];
		printf("hdhomerun device %08X found at %u.%u.%u.%u\n",
			(unsigned int)result->device_id,
			(unsigned int)(result->ip_addr >> 24) & 0x0FF, (unsigned int)(result->ip_addr >> 16) & 0x0FF,
			(unsigned int)(result->ip_addr >> 8) & 0x0FF, (unsigned int)(result->ip_addr >> 0) & 0x0FF
		);
	}

	return count;
}

static int cmd_get(const char *item)
{
	char *ret_value;
	char *ret_error;
	if (hdhomerun_device_get_var(hd, item, &ret_value, &ret_error) < 0) {
		fprintf(stderr, "communication error sending request to hdhomerun device\n");
		return -1;
	}

	if (ret_error) {
		printf("%s\n", ret_error);
		return 0;
	}

	printf("%s\n", ret_value);
	return 1;
}

static int cmd_set_internal(const char *item, const char *value)
{
	char *ret_error;
	if (hdhomerun_device_set_var(hd, item, value, NULL, &ret_error) < 0) {
		fprintf(stderr, "communication error sending request to hdhomerun device\n");
		return -1;
	}

	if (ret_error) {
		printf("%s\n", ret_error);
		return 0;
	}

	return 1;
}

static int cmd_set(const char *item, const char *value)
{
	if (strcmp(value, "-") == 0) {
		char *buffer = NULL;
		size_t pos = 0;

		while (1) {
			buffer = (char *)realloc(buffer, pos + 1024);
			if (!buffer) {
				fprintf(stderr, "out of memory\n");
				return -1;
			}

			size_t size = fread(buffer + pos, 1, 1024, stdin);
			pos += size;

			if (size < 1024) {
				break;
			}
		}

		buffer[pos] = 0;

		int ret = cmd_set_internal(item, buffer);

		free(buffer);
		return ret;
	}

	return cmd_set_internal(item, value);
}

static volatile sig_atomic_t sigabort_flag = FALSE;
static volatile sig_atomic_t siginfo_flag = FALSE;
 
static void sigabort_handler(int arg)
{
	sigabort_flag = TRUE;
}

static void siginfo_handler(int arg)
{
	siginfo_flag = TRUE;
}

static void register_signal_handlers(sig_t sigpipe_handler, sig_t sigint_handler, sig_t siginfo_handler)
{
#if defined(SIGPIPE)
	signal(SIGPIPE, sigpipe_handler);
#endif
#if defined(SIGINT)
	signal(SIGINT, sigint_handler);
#endif
#if defined(SIGINFO)
	signal(SIGINFO, siginfo_handler);
#endif
}

static void cmd_scan_printf(FILE *fp, const char *fmt, ...)
{
	va_list ap;
	va_start(ap, fmt);

	if (fp) {
		va_list apc;
		va_copy(apc, ap);

		vfprintf(fp, fmt, apc);
		fflush(fp);

		va_end(apc);
	}

	vprintf(fmt, ap);
	fflush(stdout);

	va_end(ap);
}

static int cmd_scan(const char *tuner_str, const char *filename)
{
	if (hdhomerun_device_set_tuner_from_str(hd, tuner_str) <= 0) {
		fprintf(stderr, "invalid tuner number\n");
		return -1;
	}

	char *ret_error;
	if (hdhomerun_device_tuner_lockkey_request(hd, &ret_error) <= 0) {
		fprintf(stderr, "failed to lock tuner\n");
		if (ret_error) {
			fprintf(stderr, "%s\n", ret_error);
		}
		return -1;
	}

	hdhomerun_device_set_tuner_target(hd, "none");

	char *channelmap;
	if (hdhomerun_device_get_tuner_channelmap(hd, &channelmap) <= 0) {
		fprintf(stderr, "failed to query channelmap from device\n");
		return -1;
	}

	const char *channelmap_scan_group = hdhomerun_channelmap_get_channelmap_scan_group(channelmap);
	if (!channelmap_scan_group) {
		fprintf(stderr, "unknown channelmap '%s'\n", channelmap);
		return -1;
	}

	if (hdhomerun_device_channelscan_init(hd, channelmap_scan_group) <= 0) {
		fprintf(stderr, "failed to initialize channel scan\n");
		return -1;
	}

	FILE *fp = NULL;
	if (filename) {
		fp = fopen(filename, "w");
		if (!fp) {
			fprintf(stderr, "unable to create file: %s\n", filename);
			return -1;
		}
	}

	register_signal_handlers(sigabort_handler, sigabort_handler, siginfo_handler);

	int ret = 0;
	while (!sigabort_flag) {
		struct hdhomerun_channelscan_result_t result;
		ret = hdhomerun_device_channelscan_advance(hd, &result);
		if (ret <= 0) {
			break;
		}

		cmd_scan_printf(fp, "SCANNING: %u (%s)\n",
			(unsigned int)result.frequency, result.channel_str
		);

		ret = hdhomerun_device_channelscan_detect(hd, &result);
		if (ret < 0) {
			break;
		}
		if (ret == 0) {
			continue;
		}

		cmd_scan_printf(fp, "LOCK: %s (ss=%u snq=%u seq=%u)\n",
			result.status.lock_str, result.status.signal_strength,
			result.status.signal_to_noise_quality, result.status.symbol_error_quality
		);

		if (result.transport_stream_id_detected) {
			cmd_scan_printf(fp, "TSID: 0x%04X\n", result.transport_stream_id);
		}
		if (result.original_network_id_detected) {
			cmd_scan_printf(fp, "ONID: 0x%04X\n", result.original_network_id);
		}

		int i;
		for (i = 0; i < result.program_count; i++) {
			struct hdhomerun_channelscan_program_t *program = &result.programs[i];
			cmd_scan_printf(fp, "PROGRAM %s\n", program->program_str);
		}
	}

	hdhomerun_device_tuner_lockkey_release(hd);

	if (fp) {
		fclose(fp);
	}
	if (ret < 0) {
		fprintf(stderr, "communication error sending request to hdhomerun device\n");
	}
	return ret;
}

static void cmd_save_print_stats(void)
{
	struct hdhomerun_video_stats_t stats;
	hdhomerun_device_get_video_stats(hd, &stats);

	fprintf(stderr, "%u packets received, %u overflow errors, %u network errors, %u transport errors, %u sequence errors\n",
		(unsigned int)stats.packet_count, 
		(unsigned int)stats.overflow_error_count,
		(unsigned int)stats.network_error_count, 
		(unsigned int)stats.transport_error_count, 
		(unsigned int)stats.sequence_error_count
	);
}

static int cmd_save(const char *tuner_str, const char *filename)
{
	if (hdhomerun_device_set_tuner_from_str(hd, tuner_str) <= 0) {
		fprintf(stderr, "invalid tuner number\n");
		return -1;
	}

	FILE *fp;
	if (strcmp(filename, "null") == 0) {
		fp = NULL;
	} else if (strcmp(filename, "-") == 0) {
		fp = stdout;
	} else {
		fp = fopen(filename, "wb");
		if (!fp) {
			fprintf(stderr, "unable to create file %s\n", filename);
			return -1;
		}
	}

	int ret = hdhomerun_device_stream_start(hd);
	if (ret <= 0) {
		fprintf(stderr, "unable to start stream\n");
		if (fp && fp != stdout) {
			fclose(fp);
		}
		return ret;
	}

	register_signal_handlers(sigabort_handler, sigabort_handler, siginfo_handler);

	struct hdhomerun_video_stats_t stats_old, stats_cur;
	hdhomerun_device_get_video_stats(hd, &stats_old);

	uint64_t next_progress = getcurrenttime() + 1000;

	while (!sigabort_flag) {
		uint64_t loop_start_time = getcurrenttime();

		if (siginfo_flag) {
			fprintf(stderr, "\n");
			cmd_save_print_stats();
			siginfo_flag = FALSE;
		}

		size_t actual_size;
		uint8_t *ptr = hdhomerun_device_stream_recv(hd, VIDEO_DATA_BUFFER_SIZE_1S, &actual_size);
		if (!ptr) {
			msleep_approx(64);
			continue;
		}

		if (fp) {
			if (fwrite(ptr, 1, actual_size, fp) != actual_size) {
				fprintf(stderr, "error writing output\n");
				return -1;
			}
		}

		if (loop_start_time >= next_progress) {
			next_progress += 1000;
			if (loop_start_time >= next_progress) {
				next_progress = loop_start_time + 1000;
			}

			/* Windows - indicate activity to suppress auto sleep mode. */
			#if defined(__WINDOWS__)
			SetThreadExecutionState(ES_SYSTEM_REQUIRED);
			#endif

			/* Video stats. */
			hdhomerun_device_get_video_stats(hd, &stats_cur);

			if (stats_cur.overflow_error_count > stats_old.overflow_error_count) {
				fprintf(stderr, "o");
			} else if (stats_cur.network_error_count > stats_old.network_error_count) {
				fprintf(stderr, "n");
			} else if (stats_cur.transport_error_count > stats_old.transport_error_count) {
				fprintf(stderr, "t");
			} else if (stats_cur.sequence_error_count > stats_old.sequence_error_count) {
				fprintf(stderr, "s");
			} else {
				fprintf(stderr, ".");
			}

			stats_old = stats_cur;
			fflush(stderr);
		}

		int32_t delay = 64 - (int32_t)(getcurrenttime() - loop_start_time);
		if (delay <= 0) {
			continue;
		}

		msleep_approx(delay);
	}

	if (fp) {
		fclose(fp);
	}

	hdhomerun_device_stream_stop(hd);

	fprintf(stderr, "\n");
	fprintf(stderr, "-- Video statistics --\n");
	cmd_save_print_stats();

	return 0;
}

static int cmd_upgrade(const char *filename)
{
	FILE *fp = fopen(filename, "rb");
	if (!fp) {
		fprintf(stderr, "unable to open file %s\n", filename);
		return -1;
	}

	printf("uploading firmware...\n");
	if (hdhomerun_device_upgrade(hd, fp) <= 0) {
		fprintf(stderr, "error sending upgrade file to hdhomerun device\n");
		fclose(fp);
		return -1;
	}

	fclose(fp);
	msleep_minimum(2000);

	printf("upgrading firmware...\n");
	msleep_minimum(8000);

	printf("rebooting...\n");
	int count = 0;
	char *version_str;
	while (1) {
		if (hdhomerun_device_get_version(hd, &version_str, NULL) >= 0) {
			break;
		}

		count++;
		if (count > 30) {
			fprintf(stderr, "error finding device after firmware upgrade\n");
			return -1;
		}

		msleep_minimum(1000);
	}

	printf("upgrade complete - now running firmware %s\n", version_str);
	return 0;
}

static int cmd_execute(void)
{
	char *ret_value;
	char *ret_error;
	if (hdhomerun_device_get_var(hd, "/sys/boot", &ret_value, &ret_error) < 0) {
		fprintf(stderr, "communication error sending request to hdhomerun device\n");
		return -1;
	}

	if (ret_error) {
		printf("%s\n", ret_error);
		return 0;
	}

	char *end = ret_value + strlen(ret_value);
	char *pos = ret_value;

	while (1) {
		if (pos >= end) {
			break;
		}

		char *eol_r = strchr(pos, '\r');
		if (!eol_r) {
			eol_r = end;
		}

		char *eol_n = strchr(pos, '\n');
		if (!eol_n) {
			eol_n = end;
		}

		char *eol = eol_r;
		if (eol_n < eol) {
			eol = eol_n;
		}

		char *sep = strchr(pos, ' ');
		if (!sep || sep > eol) {
			pos = eol + 1;
			continue;
		}

		*sep = 0;
		*eol = 0;

		char *item = pos;
		char *value = sep + 1;

		printf("set %s \"%s\"\n", item, value);

		cmd_set_internal(item, value);

		pos = eol + 1;
	}

	return 1;
}

static int main_cmd(int argc, char *argv[])
{
	if (argc < 1) {
		return help();
	}

	char *cmd = *argv++; argc--;

	if (contains(cmd, "key")) {
		if (argc < 2) {
			return help();
		}
		uint32_t lockkey = strtoul(argv[0], NULL, 0);
		hdhomerun_device_tuner_lockkey_use_value(hd, lockkey);

		cmd = argv[1];
		argv+=2; argc-=2;
	}

	if (contains(cmd, "get")) {
		if (argc < 1) {
			return help();
		}
		return cmd_get(argv[0]);
	}

	if (contains(cmd, "set")) {
		if (argc < 2) {
			return help();
		}
		return cmd_set(argv[0], argv[1]);
	}

	if (contains(cmd, "scan")) {
		if (argc < 1) {
			return help();
		}
		if (argc < 2) {
			return cmd_scan(argv[0], NULL);
		} else {
			return cmd_scan(argv[0], argv[1]);
		}
	}

	if (contains(cmd, "save")) {
		if (argc < 2) {
			return help();
		}
		return cmd_save(argv[0], argv[1]);
	}

	if (contains(cmd, "upgrade")) {
		if (argc < 1) {
			return help();
		}
		return cmd_upgrade(argv[0]);
	}

	if (contains(cmd, "execute")) {
		return cmd_execute();
	}

	return help();
}

static int main_internal(int argc, char *argv[])
{
#if defined(__WINDOWS__)
	/* Initialize network socket support. */
	WORD wVersionRequested = MAKEWORD(2, 0);
	WSADATA wsaData;
	WSAStartup(wVersionRequested, &wsaData);
#endif

	extract_appname(argv[0]);
	argv++;
	argc--;

	if (argc == 0) {
		return help();
	}

	char *id_str = *argv++; argc--;
	if (contains(id_str, "help")) {
		return help();
	}
	if (contains(id_str, "discover")) {
		if (argc < 1) {
			return discover_print(NULL);
		} else {
			return discover_print(argv[0]);
		}
	}

	/* Device object. */
	hd = hdhomerun_device_create_from_str(id_str, NULL);
	if (!hd) {
		fprintf(stderr, "invalid device id: %s\n", id_str);
		return -1;
	}

	/* Device ID check. */
	uint32_t device_id_requested = hdhomerun_device_get_device_id_requested(hd);
	if (!hdhomerun_discover_validate_device_id(device_id_requested)) {
		fprintf(stderr, "invalid device id: %08X\n", (unsigned int)device_id_requested);
	}

	/* Connect to device and check model. */
	const char *model = hdhomerun_device_get_model_str(hd);
	if (!model) {
		fprintf(stderr, "unable to connect to device\n");
		hdhomerun_device_destroy(hd);
		return -1;
	}

	/* Command. */
	int ret = main_cmd(argc, argv);

	/* Cleanup. */
	hdhomerun_device_destroy(hd);

	/* Complete. */
	return ret;
}

int main(int argc, char *argv[])
{
	int ret = main_internal(argc, argv);
	if (ret <= 0) {
		return 1;
	}
	return 0;
}
