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

struct hdhomerun_channel_entry_t {
	struct hdhomerun_channel_entry_t *next;
	struct hdhomerun_channel_entry_t *prev;
	uint32_t frequency;
	uint16_t channel_number;
	char name[16];
};

struct hdhomerun_channel_list_t {
	struct hdhomerun_channel_entry_t *head;
	struct hdhomerun_channel_entry_t *tail;
};

struct hdhomerun_channelmap_range_t {
	uint16_t channel_range_start;
	uint16_t channel_range_end;
	uint32_t frequency;
	uint32_t spacing;
};

struct hdhomerun_channelmap_record_t {
	const char *channelmap;
	const struct hdhomerun_channelmap_range_t *range_list;
	const char *channelmap_scan_group;
	const char *countrycodes;
};

/* AU antenna channels. Channels {6, 7, 8, 9, 9A} are numbered {5, 6, 7, 8, 9} by the HDHomeRun. */
static const struct hdhomerun_channelmap_range_t hdhomerun_channelmap_range_au_bcast[] = {
	{  5,  12, 177500000, 7000000},
	{ 21,  69, 480500000, 7000000},
	{  0,   0,         0,       0}
};

/* EU antenna channels. */
static const struct hdhomerun_channelmap_range_t hdhomerun_channelmap_range_eu_bcast[] = {
	{  5,  12, 177500000, 7000000},
	{ 21,  69, 474000000, 8000000},
	{  0,   0,         0,       0}
};

/* EU cable channels. No common standard - use frequency in MHz for channel number. */
static const struct hdhomerun_channelmap_range_t hdhomerun_channelmap_range_eu_cable[] = {
	{108, 862, 108000000, 1000000},
	{  0,   0,         0,       0}
};

/* KR cable channels. */
static const struct hdhomerun_channelmap_range_t hdhomerun_channelmap_range_kr_cable[] = {
	{  2,   4,  57000000, 6000000},
	{  5,   6,  79000000, 6000000},
	{  7,  13, 177000000, 6000000},
	{ 14,  22, 123000000, 6000000},
	{ 23, 153, 219000000, 6000000},
	{  0,   0,         0,       0}
};

/* JP antenna channels. */
static const struct hdhomerun_channelmap_range_t hdhomerun_channelmap_range_jp_bcast[] = {
	{ 13, 62, 473000000, 6000000},
	{  0,  0,         0,       0}
};

/* US antenna channels. */
static const struct hdhomerun_channelmap_range_t hdhomerun_channelmap_range_us_bcast[] = {
	{  2,   4,  57000000, 6000000},
	{  5,   6,  79000000, 6000000},
	{  7,  13, 177000000, 6000000},
	{ 14,  69, 473000000, 6000000},
	{  0,   0,         0,       0}
};

/* US cable channels. */
static const struct hdhomerun_channelmap_range_t hdhomerun_channelmap_range_us_cable[] = {
	{  2,   4,  57000000, 6000000},
	{  5,   6,  79000000, 6000000},
	{  7,  13, 177000000, 6000000},
	{ 14,  22, 123000000, 6000000},
	{ 23,  94, 219000000, 6000000},
	{ 95,  99,  93000000, 6000000},
	{100, 158, 651000000, 6000000},
	{  0,   0,         0,       0}
};

/* US cable channels (HRC). */
static const struct hdhomerun_channelmap_range_t hdhomerun_channelmap_range_us_hrc[] = {
	{  2,   4,  55752700, 6000300},
	{  5,   6,  79753900, 6000300},
	{  7,  13, 175758700, 6000300},
	{ 14,  22, 121756000, 6000300},
	{ 23,  94, 217760800, 6000300},
	{ 95,  99,  91754500, 6000300},
	{100, 158, 649782400, 6000300},
	{  0,   0,         0,       0}
};

/* US cable channels (IRC). */
static const struct hdhomerun_channelmap_range_t hdhomerun_channelmap_range_us_irc[] = {
	{  2,   4,  57012500, 6000000},
	{  5,   6,  81012500, 6000000},
	{  7,  13, 177012500, 6000000},
	{ 14,  22, 123012500, 6000000},
	{ 23,  41, 219012500, 6000000},
	{ 42,  42, 333025000, 6000000},
	{ 43,  94, 339012500, 6000000},
	{ 95,  97,  93012500, 6000000},
	{ 98,  99, 111025000, 6000000},
	{100, 158, 651012500, 6000000},
	{  0,   0,         0,       0}
};

static const struct hdhomerun_channelmap_record_t hdhomerun_channelmap_table[] = {
	{"au-bcast", hdhomerun_channelmap_range_au_bcast, "au-bcast",               "AU"},
	{"au-cable", hdhomerun_channelmap_range_eu_cable, "au-cable",               "AU"},
	{"eu-bcast", hdhomerun_channelmap_range_eu_bcast, "eu-bcast",               NULL},
	{"eu-cable", hdhomerun_channelmap_range_eu_cable, "eu-cable",               NULL},
	{"tw-bcast", hdhomerun_channelmap_range_us_bcast, "tw-bcast",               "TW"},
	{"tw-cable", hdhomerun_channelmap_range_us_cable, "tw-cable",               "TW"},

	{"kr-bcast", hdhomerun_channelmap_range_us_bcast, "kr-bcast",               "KR"},
	{"kr-cable", hdhomerun_channelmap_range_kr_cable, "kr-cable",               "KR"},
	{"us-bcast", hdhomerun_channelmap_range_us_bcast, "us-bcast",               NULL},
	{"us-cable", hdhomerun_channelmap_range_us_cable, "us-cable us-hrc us-irc", NULL},
	{"us-hrc",   hdhomerun_channelmap_range_us_hrc  , "us-cable us-hrc us-irc", NULL},
	{"us-irc",   hdhomerun_channelmap_range_us_irc,   "us-cable us-hrc us-irc", NULL},

	{"jp-bcast", hdhomerun_channelmap_range_jp_bcast, "jp-bcast",               "JP" },
	{ NULL, NULL, NULL, NULL }
};

const char *hdhomerun_channelmap_get_channelmap_from_country_source(const char *countrycode, const char *source, const char *supported)
{
	const char *default_result = NULL;

	const struct hdhomerun_channelmap_record_t *record = hdhomerun_channelmap_table;
	while (record->channelmap) {
		/* Ignore records that do not match the requested source. */
		if (!strstr(record->channelmap, source)) {
			record++;
			continue;
		}

		/* Ignore records that are not supported by the hardware. */
		if (!strstr(supported, record->channelmap)) {
			record++;
			continue;
		}

		/* If this record is the default result then remember it and keep searching. */
		if (!record->countrycodes) {
			default_result = record->channelmap;
			record++;
			continue;
		}

		/* Ignore records that have a countrycode filter and do not match. */
		if (!strstr(record->countrycodes, countrycode)) {
			record++;
			continue;
		}

		/* Record found with exact match for source and countrycode. */
		return record->channelmap;
	}

	return default_result;
}

const char *hdhomerun_channelmap_get_channelmap_scan_group(const char *channelmap)
{
	const struct hdhomerun_channelmap_record_t *record = hdhomerun_channelmap_table;
	while (record->channelmap) {
		if (strstr(channelmap, record->channelmap)) {
			return record->channelmap_scan_group;
		}
		record++;
	}

	return NULL;
}

uint16_t hdhomerun_channel_entry_channel_number(struct hdhomerun_channel_entry_t *entry)
{
	return entry->channel_number;
}

uint32_t hdhomerun_channel_entry_frequency(struct hdhomerun_channel_entry_t *entry)
{
	return entry->frequency;
}

const char *hdhomerun_channel_entry_name(struct hdhomerun_channel_entry_t *entry)
{
	return entry->name;
}

struct hdhomerun_channel_entry_t *hdhomerun_channel_list_first(struct hdhomerun_channel_list_t *channel_list)
{
	return channel_list->head;
}

struct hdhomerun_channel_entry_t *hdhomerun_channel_list_last(struct hdhomerun_channel_list_t *channel_list)
{
	return channel_list->tail;
}

struct hdhomerun_channel_entry_t *hdhomerun_channel_list_next(struct hdhomerun_channel_list_t *channel_list, struct hdhomerun_channel_entry_t *entry)
{
	return entry->next;
}

struct hdhomerun_channel_entry_t *hdhomerun_channel_list_prev(struct hdhomerun_channel_list_t *channel_list, struct hdhomerun_channel_entry_t *entry)
{
	return entry->prev;
}

uint32_t hdhomerun_channel_list_total_count(struct hdhomerun_channel_list_t *channel_list)
{
	uint32_t count = 0;

	struct hdhomerun_channel_entry_t *entry = hdhomerun_channel_list_first(channel_list);
	while (entry) {
		count++;
		entry = hdhomerun_channel_list_next(channel_list, entry);
	}

	return count;
}

uint32_t hdhomerun_channel_list_frequency_count(struct hdhomerun_channel_list_t *channel_list)
{
	uint32_t count = 0;
	uint32_t last_frequency = 0;

	struct hdhomerun_channel_entry_t *entry = hdhomerun_channel_list_first(channel_list);
	while (entry) {
		if (entry->frequency != last_frequency) {
			last_frequency = entry->frequency;
			count++;
		}

		entry = hdhomerun_channel_list_next(channel_list, entry);
	}

	return count;
}

uint32_t hdhomerun_channel_frequency_round(uint32_t frequency, uint32_t resolution)
{
	frequency += resolution / 2;
	return (frequency / resolution) * resolution;
}

uint32_t hdhomerun_channel_frequency_round_normal(uint32_t frequency)
{
	return hdhomerun_channel_frequency_round(frequency, 125000);
}

uint32_t hdhomerun_channel_number_to_frequency(struct hdhomerun_channel_list_t *channel_list, uint16_t channel_number)
{
	struct hdhomerun_channel_entry_t *entry = hdhomerun_channel_list_first(channel_list);
	while (entry) {
		if (entry->channel_number == channel_number) {
			return entry->frequency;
		}

		entry = hdhomerun_channel_list_next(channel_list, entry);
	}

	return 0;
}

uint16_t hdhomerun_channel_frequency_to_number(struct hdhomerun_channel_list_t *channel_list, uint32_t frequency)
{
	frequency = hdhomerun_channel_frequency_round_normal(frequency);

	struct hdhomerun_channel_entry_t *entry = hdhomerun_channel_list_first(channel_list);
	while (entry) {
		if (entry->frequency == frequency) {
			return entry->channel_number;
		}
		if (entry->frequency > frequency) {
			return 0;
		}

		entry = hdhomerun_channel_list_next(channel_list, entry);
	}

	return 0;
}

static void hdhomerun_channel_list_build_insert(struct hdhomerun_channel_list_t *channel_list, struct hdhomerun_channel_entry_t *entry)
{
	struct hdhomerun_channel_entry_t *prev = NULL;
	struct hdhomerun_channel_entry_t *next = channel_list->head;

	while (next) {
		if (next->frequency > entry->frequency) {
			break;
		}

		prev = next;
		next = next->next;
	}

	entry->prev = prev;
	entry->next = next;

	if (prev) {
		prev->next = entry;
	} else {
		channel_list->head = entry;
	}

	if (next) {
		next->prev = entry;
	} else {
		channel_list->tail = entry;
	}
}

static void hdhomerun_channel_list_build_range(struct hdhomerun_channel_list_t *channel_list, const char *channelmap, const struct hdhomerun_channelmap_range_t *range)
{
	uint16_t channel_number;
	for (channel_number = range->channel_range_start; channel_number <= range->channel_range_end; channel_number++) {
		struct hdhomerun_channel_entry_t *entry = (struct hdhomerun_channel_entry_t *)calloc(1, sizeof(struct hdhomerun_channel_entry_t));
		if (!entry) {
			return;
		}

		entry->channel_number = channel_number;
		entry->frequency = range->frequency + ((uint32_t)(channel_number - range->channel_range_start) * range->spacing);
		entry->frequency = hdhomerun_channel_frequency_round_normal(entry->frequency);
		hdhomerun_sprintf(entry->name, entry->name + sizeof(entry->name), "%s:%u", channelmap, entry->channel_number);

		hdhomerun_channel_list_build_insert(channel_list, entry);
	}
}

static void hdhomerun_channel_list_build_ranges(struct hdhomerun_channel_list_t *channel_list, const struct hdhomerun_channelmap_record_t *record)
{
	const struct hdhomerun_channelmap_range_t *range = record->range_list;
	while (range->frequency) {
		hdhomerun_channel_list_build_range(channel_list, record->channelmap, range);
		range++;
	}
}

void hdhomerun_channel_list_destroy(struct hdhomerun_channel_list_t *channel_list)
{
	while (channel_list->head) {
		struct hdhomerun_channel_entry_t *entry = channel_list->head;
		channel_list->head = entry->next;
		free(entry);
	}

	free(channel_list);
}

struct hdhomerun_channel_list_t *hdhomerun_channel_list_create(const char *channelmap)
{
	struct hdhomerun_channel_list_t *channel_list = (struct hdhomerun_channel_list_t *)calloc(1, sizeof(struct hdhomerun_channel_list_t));
	if (!channel_list) {
		return NULL;
	}

	const struct hdhomerun_channelmap_record_t *record = hdhomerun_channelmap_table;
	while (record->channelmap) {
		if (!strstr(channelmap, record->channelmap)) {
			record++;
			continue;
		}

		hdhomerun_channel_list_build_ranges(channel_list, record);
		record++;
	}

	if (!channel_list->head) {
		free(channel_list);
		return NULL;
	}

	return channel_list;
}
