/*
 * hdhomerun_channelscan.c
 *
 * Copyright © 2007-2015 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"

struct hdhomerun_channelscan_t {
	struct hdhomerun_device_t *hd;
	uint32_t scanned_channels;
	struct hdhomerun_channel_list_t *channel_list;	
	struct hdhomerun_channel_entry_t *next_channel;
};

struct hdhomerun_channelscan_t *channelscan_create(struct hdhomerun_device_t *hd, const char *channelmap)
{
	struct hdhomerun_channelscan_t *scan = (struct hdhomerun_channelscan_t *)calloc(1, sizeof(struct hdhomerun_channelscan_t));
	if (!scan) {
		return NULL;
	}

	scan->hd = hd;

	scan->channel_list = hdhomerun_channel_list_create(channelmap);
	if (!scan->channel_list) {
		free(scan);
		return NULL;
	}

	scan->next_channel = hdhomerun_channel_list_last(scan->channel_list);
	return scan;
}

void channelscan_destroy(struct hdhomerun_channelscan_t *scan)
{
	hdhomerun_channel_list_destroy(scan->channel_list);
	free(scan);
}

static int channelscan_find_lock(struct hdhomerun_channelscan_t *scan, uint32_t frequency, struct hdhomerun_channelscan_result_t *result)
{
	/* Set channel. */
	char channel_str[64];
	hdhomerun_sprintf(channel_str, channel_str + sizeof(channel_str), "auto:%u", (unsigned int)frequency);

	int ret = hdhomerun_device_set_tuner_channel(scan->hd, channel_str);
	if (ret <= 0) {
		return ret;
	}

	/* Wait for lock. */
	ret = hdhomerun_device_wait_for_lock(scan->hd, &result->status);
	if (ret <= 0) {
		return ret;
	}
	if (!result->status.lock_supported) {
		return 1;
	}

	/* Wait for symbol quality = 100%. */
	uint64_t timeout = getcurrenttime() + 5000;
	while (1) {
		ret = hdhomerun_device_get_tuner_status(scan->hd, NULL, &result->status);
		if (ret <= 0) {
			return ret;
		}

		if (result->status.symbol_error_quality == 100) {
			return 1;
		}

		if (getcurrenttime() >= timeout) {
			return 1;
		}

		msleep_approx(250);
	}
}

static void channelscan_extract_name(struct hdhomerun_channelscan_program_t *program, const char *line)
{
	/* Find start of name. */
	const char *start = strchr(line, ' ');
	if (!start) {
		return;
	}
	start++;

	start = strchr(start, ' ');
	if (!start) {
		return;
	}
	start++;

	/* Find end of name. */
	const char *end = strstr(start, " (");
	if (!end) {
		end = strchr(line, 0);
	}

	if (end <= start) {
		return;
	}

	/* Extract name. */
	size_t length = (size_t)(end - start);
	if (length > sizeof(program->name) - 1) {
		length = sizeof(program->name) - 1;
	}

	strncpy(program->name, start, length);
	program->name[length] = 0;
}

static int channelscan_detect_programs(struct hdhomerun_channelscan_t *scan, struct hdhomerun_channelscan_result_t *result, bool *pchanged, bool *pincomplete)
{
	*pchanged = false;
	*pincomplete = false;

	char *streaminfo;
	int ret = hdhomerun_device_get_tuner_streaminfo(scan->hd, &streaminfo);
	if (ret <= 0) {
		return ret;
	}

	char *next_line = streaminfo;
	int program_count = 0;

	while (1) {
		char *line = next_line;

		next_line = strchr(line, '\n');
		if (!next_line) {
			break;
		}
		*next_line++ = 0;

		unsigned int transport_stream_id;
		if (sscanf(line, "tsid=0x%x", &transport_stream_id) == 1) {
			result->transport_stream_id = (uint16_t)transport_stream_id;
			result->transport_stream_id_detected = true;
			continue;
		}

		unsigned int original_network_id;
		if (sscanf(line, "onid=0x%x", &original_network_id) == 1) {
			result->original_network_id = (uint16_t)original_network_id;
			result->original_network_id_detected = true;
			continue;
		}

		if (program_count >= HDHOMERUN_CHANNELSCAN_MAX_PROGRAM_COUNT) {
			continue;
		}

		struct hdhomerun_channelscan_program_t program;
		memset(&program, 0, sizeof(program));

		hdhomerun_sprintf(program.program_str, program.program_str + sizeof(program.program_str), "%s", line);

		unsigned int program_number;
		unsigned int virtual_major, virtual_minor;
		if (sscanf(line, "%u: %u.%u", &program_number, &virtual_major, &virtual_minor) != 3) {
			if (sscanf(line, "%u: %u", &program_number, &virtual_major) != 2) {
				continue;
			}
			virtual_minor = 0;
		}

		program.program_number = (uint16_t)program_number;
		program.virtual_major = (uint16_t)virtual_major;
		program.virtual_minor = (uint16_t)virtual_minor;

		channelscan_extract_name(&program, line);

		if (strstr(line, "(control)")) {
			program.type = HDHOMERUN_CHANNELSCAN_PROGRAM_CONTROL;
		} else if (strstr(line, "(encrypted)")) {
			program.type = HDHOMERUN_CHANNELSCAN_PROGRAM_ENCRYPTED;
		} else if (strstr(line, "(no data)")) {
			program.type = HDHOMERUN_CHANNELSCAN_PROGRAM_NODATA;
			*pincomplete = true;
		} else {
			program.type = HDHOMERUN_CHANNELSCAN_PROGRAM_NORMAL;
			if ((program.virtual_major == 0) || (program.name[0] == 0)) {
				*pincomplete = true;
			}
		}

		if (memcmp(&result->programs[program_count], &program, sizeof(program)) != 0) {
			memcpy(&result->programs[program_count], &program, sizeof(program));
			*pchanged = true;
		}

		program_count++;
	}

	if (program_count == 0) {
		*pincomplete = true;
	}
	if (result->program_count != program_count) {
		result->program_count = program_count;
		*pchanged = true;
	}

	return 1;
}

int channelscan_advance(struct hdhomerun_channelscan_t *scan, struct hdhomerun_channelscan_result_t *result)
{
	memset(result, 0, sizeof(struct hdhomerun_channelscan_result_t));

	struct hdhomerun_channel_entry_t *entry = scan->next_channel;
	if (!entry) {
		return 0;
	}

	/* Combine channels with same frequency. */
	result->frequency = hdhomerun_channel_entry_frequency(entry);

	char *ptr = result->channel_str;
	char *end = result->channel_str + sizeof(result->channel_str);
	hdhomerun_sprintf(ptr, end, hdhomerun_channel_entry_name(entry));

	while (1) {
		entry = hdhomerun_channel_list_prev(scan->channel_list, entry);
		if (!entry) {
			scan->next_channel = NULL;
			break;
		}

		if (hdhomerun_channel_entry_frequency(entry) != result->frequency) {
			scan->next_channel = entry;
			break;
		}

		ptr = strchr(ptr, 0);
		hdhomerun_sprintf(ptr, end, ", %s", hdhomerun_channel_entry_name(entry));
	}

	return 1;
}

int channelscan_detect(struct hdhomerun_channelscan_t *scan, struct hdhomerun_channelscan_result_t *result)
{
	scan->scanned_channels++;

	/* Find lock. */
	int ret = channelscan_find_lock(scan, result->frequency, result);
	if (ret <= 0) {
		return ret;
	}
	if (!result->status.lock_supported) {
		return 1;
	}

	/* Detect programs. */
	result->program_count = 0;

	uint64_t timeout;
	if (strstr(hdhomerun_device_get_model_str(scan->hd), "atsc")) {
		timeout = getcurrenttime() + 4000;
	} else {
		timeout = getcurrenttime() + 10000;
	}

	uint64_t complete_time = getcurrenttime() + 1000;

	while (1) {
		bool changed, incomplete;
		ret = channelscan_detect_programs(scan, result, &changed, &incomplete);
		if (ret <= 0) {
			return ret;
		}

		if (changed) {
			complete_time = getcurrenttime() + 1000;
		}

		if (!incomplete && (getcurrenttime() >= complete_time)) {
			break;
		}

		if (getcurrenttime() >= timeout) {
			break;
		}

		msleep_approx(250);
	}

	/* Lock => skip overlapping channels. */
	uint32_t max_next_frequency = result->frequency - 5500000;
	while (1) {
		if (!scan->next_channel) {
			break;
		}

		if (hdhomerun_channel_entry_frequency(scan->next_channel) <= max_next_frequency) {
			break;
		}

		scan->next_channel = hdhomerun_channel_list_prev(scan->channel_list, scan->next_channel);
	}

	/* Success. */
	return 1;
}

uint8_t channelscan_get_progress(struct hdhomerun_channelscan_t *scan)
{
	struct hdhomerun_channel_entry_t *entry = scan->next_channel;
	if (!entry) {
		return 100;
	}

	uint32_t channels_remaining = 1;
	uint32_t frequency = hdhomerun_channel_entry_frequency(entry);

	while (1) {
		entry = hdhomerun_channel_list_prev(scan->channel_list, entry);
		if (!entry) {
			break;
		}

		if (hdhomerun_channel_entry_frequency(entry) != frequency) {
			channels_remaining++;
			frequency = hdhomerun_channel_entry_frequency(entry);
		}
	}

	return (uint8_t) (scan->scanned_channels * 100 / (scan->scanned_channels + channels_remaining));
}
