/*
 * libdivecomputer
 *
 * Copyright (C) 2015 Jef Driesen
 *
 * 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 <stdlib.h>
#include <string.h>
#include <stdio.h>

#ifdef _WIN32
#include <io.h>
#include <fcntl.h>
#endif

#include <libdivecomputer/serial.h>
#include <libdivecomputer/bluetooth.h>
#include <libdivecomputer/irda.h>
#include <libdivecomputer/usbhid.h>

#include "common.h"
#include "utils.h"

#ifdef _WIN32
#define DC_TICKS_FORMAT "%I64d"
#else
#define DC_TICKS_FORMAT "%lld"
#endif

#define C_ARRAY_SIZE(array) (sizeof (array) / sizeof *(array))

typedef struct backend_table_t {
	const char *name;
	dc_family_t type;
	unsigned int model;
} backend_table_t;

typedef struct transport_table_t {
	const char *name;
	dc_transport_t type;
} transport_table_t;

static const backend_table_t g_backends[] = {
	{"solution",    DC_FAMILY_SUUNTO_SOLUTION,     0},
	{"eon",	        DC_FAMILY_SUUNTO_EON,          0},
	{"vyper",       DC_FAMILY_SUUNTO_VYPER,        0x0A},
	{"vyper2",      DC_FAMILY_SUUNTO_VYPER2,       0x10},
	{"d9",          DC_FAMILY_SUUNTO_D9,           0x0E},
	{"eonsteel",    DC_FAMILY_SUUNTO_EONSTEEL,     0},
	{"aladin",      DC_FAMILY_UWATEC_ALADIN,       0x3F},
	{"memomouse",   DC_FAMILY_UWATEC_MEMOMOUSE,    0},
	{"smart",       DC_FAMILY_UWATEC_SMART,        0x10},
	{"sensus",      DC_FAMILY_REEFNET_SENSUS,      1},
	{"sensuspro",   DC_FAMILY_REEFNET_SENSUSPRO,   2},
	{"sensusultra", DC_FAMILY_REEFNET_SENSUSULTRA, 3},
	{"vtpro",       DC_FAMILY_OCEANIC_VTPRO,       0x4245},
	{"veo250",      DC_FAMILY_OCEANIC_VEO250,      0x424C},
	{"atom2",       DC_FAMILY_OCEANIC_ATOM2,       0x4342},
	{"nemo",        DC_FAMILY_MARES_NEMO,          0},
	{"puck",        DC_FAMILY_MARES_PUCK,          7},
	{"darwin",      DC_FAMILY_MARES_DARWIN,        0},
	{"iconhd",      DC_FAMILY_MARES_ICONHD,        0x14},
	{"ostc",        DC_FAMILY_HW_OSTC,             0},
	{"frog",        DC_FAMILY_HW_FROG,             0},
	{"ostc3",       DC_FAMILY_HW_OSTC3,            0x0A},
	{"edy",         DC_FAMILY_CRESSI_EDY,          0x08},
	{"leonardo",	DC_FAMILY_CRESSI_LEONARDO,     1},
	{"goa",         DC_FAMILY_CRESSI_GOA,          2},
	{"n2ition3",    DC_FAMILY_ZEAGLE_N2ITION3,     0},
	{"cobalt",      DC_FAMILY_ATOMICS_COBALT,      0},
	{"predator",	DC_FAMILY_SHEARWATER_PREDATOR, 2},
	{"petrel",      DC_FAMILY_SHEARWATER_PETREL,   3},
	{"nitekq",      DC_FAMILY_DIVERITE_NITEKQ,     0},
	{"aqualand",    DC_FAMILY_CITIZEN_AQUALAND,    0},
	{"idive",       DC_FAMILY_DIVESYSTEM_IDIVE,    0x03},
	{"cochran",     DC_FAMILY_COCHRAN_COMMANDER,   0},
	{"divecomputereu", DC_FAMILY_TECDIVING_DIVECOMPUTEREU, 0},
	{"descentmk1",  DC_FAMILY_GARMIN, 0},
	{"cosmiq",      DC_FAMILY_DEEPBLU,             0},
};

static const transport_table_t g_transports[] = {
	{"serial",    DC_TRANSPORT_SERIAL},
	{"usb",       DC_TRANSPORT_USB},
	{"usbhid",    DC_TRANSPORT_USBHID},
	{"irda",      DC_TRANSPORT_IRDA},
	{"bluetooth", DC_TRANSPORT_BLUETOOTH},
	{"ble",       DC_TRANSPORT_BLE},
	{"usbstorage",DC_TRANSPORT_USBSTORAGE},
};

const char *
dctool_errmsg (dc_status_t status)
{
	switch (status) {
	case DC_STATUS_SUCCESS:
		return "Success";
	case DC_STATUS_UNSUPPORTED:
		return "Unsupported operation";
	case DC_STATUS_INVALIDARGS:
		return "Invalid arguments";
	case DC_STATUS_NOMEMORY:
		return "Out of memory";
	case DC_STATUS_NODEVICE:
		return "No device found";
	case DC_STATUS_NOACCESS:
		return "Access denied";
	case DC_STATUS_IO:
		return "Input/output error";
	case DC_STATUS_TIMEOUT:
		return "Timeout";
	case DC_STATUS_PROTOCOL:
		return "Protocol error";
	case DC_STATUS_DATAFORMAT:
		return "Data format error";
	case DC_STATUS_CANCELLED:
		return "Cancelled";
	default:
		return "Unknown error";
	}
}

dc_family_t
dctool_family_type (const char *name)
{
	for (unsigned int i = 0; i < C_ARRAY_SIZE (g_backends); ++i) {
		if (strcmp (name, g_backends[i].name) == 0)
			return g_backends[i].type;
	}

	return DC_FAMILY_NULL;
}

const char *
dctool_family_name (dc_family_t type)
{
	for (unsigned int i = 0; i < C_ARRAY_SIZE (g_backends); ++i) {
		if (g_backends[i].type == type)
			return g_backends[i].name;
	}

	return NULL;
}

unsigned int
dctool_family_model (dc_family_t type)
{
	for (unsigned int i = 0; i < C_ARRAY_SIZE (g_backends); ++i) {
		if (g_backends[i].type == type)
			return g_backends[i].model;
	}

	return 0;
}

dc_transport_t
dctool_transport_type (const char *name)
{
	for (size_t i = 0; i < C_ARRAY_SIZE (g_transports); ++i) {
		if (strcmp (name, g_transports[i].name) == 0)
			return g_transports[i].type;
	}

	return DC_TRANSPORT_NONE;
}

const char *
dctool_transport_name (dc_transport_t type)
{
	for (size_t i = 0; i < C_ARRAY_SIZE (g_transports); ++i) {
		if (g_transports[i].type == type)
			return g_transports[i].name;
	}

	return NULL;
}

dc_transport_t
dctool_transport_default (dc_descriptor_t *descriptor)
{
	unsigned int transports = dc_descriptor_get_transports (descriptor);

	for (size_t i = 0; i < C_ARRAY_SIZE (g_transports); ++i) {
		if (transports & g_transports[i].type)
			return g_transports[i].type;
	}

	return DC_TRANSPORT_NONE;
}

void
dctool_event_cb (dc_device_t *device, dc_event_type_t event, const void *data, void *userdata)
{
	const dc_event_progress_t *progress = (const dc_event_progress_t *) data;
	const dc_event_devinfo_t *devinfo = (const dc_event_devinfo_t *) data;
	const dc_event_clock_t *clock = (const dc_event_clock_t *) data;
	const dc_event_vendor_t *vendor = (const dc_event_vendor_t *) data;

	switch (event) {
	case DC_EVENT_WAITING:
		message ("Event: waiting for user action\n");
		break;
	case DC_EVENT_PROGRESS:
		message ("Event: progress %3.2f%% (%u/%u)\n",
			100.0 * (double) progress->current / (double) progress->maximum,
			progress->current, progress->maximum);
		break;
	case DC_EVENT_DEVINFO:
		message ("Event: model=%u (0x%08x), firmware=%u (0x%08x), serial=%u (0x%08x)\n",
			devinfo->model, devinfo->model,
			devinfo->firmware, devinfo->firmware,
			devinfo->serial, devinfo->serial);
		break;
	case DC_EVENT_CLOCK:
		message ("Event: systime=" DC_TICKS_FORMAT ", devtime=%u\n",
			clock->systime, clock->devtime);
		break;
	case DC_EVENT_VENDOR:
		message ("Event: vendor=");
		for (unsigned int i = 0; i < vendor->size; ++i)
			message ("%02X", vendor->data[i]);
		message ("\n");
		break;
	default:
		break;
	}
}

dc_status_t
dctool_descriptor_search (dc_descriptor_t **out, const char *name, dc_family_t family, unsigned int model)
{
	dc_status_t rc = DC_STATUS_SUCCESS;

	dc_iterator_t *iterator = NULL;
	rc = dc_descriptor_iterator (&iterator);
	if (rc != DC_STATUS_SUCCESS) {
		ERROR ("Error creating the device descriptor iterator.");
		return rc;
	}

	dc_descriptor_t *descriptor = NULL, *current = NULL;
	while ((rc = dc_iterator_next (iterator, &descriptor)) == DC_STATUS_SUCCESS) {
		if (name) {
			const char *vendor = dc_descriptor_get_vendor (descriptor);
			const char *product = dc_descriptor_get_product (descriptor);

			size_t n = strlen (vendor);
			if (strncasecmp (name, vendor, n) == 0 && name[n] == ' ' &&
				strcasecmp (name + n + 1, product) == 0)
			{
				current = descriptor;
				break;
			} else if (strcasecmp (name, product) == 0) {
				current = descriptor;
				break;
			}
		} else {
			if (family == dc_descriptor_get_type (descriptor)) {
				if (model == dc_descriptor_get_model (descriptor)) {
					// Exact match found. Return immediately.
					dc_descriptor_free (current);
					current = descriptor;
					break;
				} else {
					// Possible match found. Keep searching for an exact match.
					// If no exact match is found, the first match is returned.
					if (current == NULL) {
						current = descriptor;
						descriptor = NULL;
					}
				}
			}
		}

		dc_descriptor_free (descriptor);
	}

	if (rc != DC_STATUS_SUCCESS && rc != DC_STATUS_DONE) {
		dc_descriptor_free (current);
		dc_iterator_free (iterator);
		ERROR ("Error iterating the device descriptors.");
		return rc;
	}

	dc_iterator_free (iterator);

	*out = current;

	return DC_STATUS_SUCCESS;
}

static unsigned char
hex2dec (unsigned char value)
{
	if (value >= '0' && value <= '9')
		return value - '0';
	else if (value >= 'A' && value <= 'F')
		return value - 'A' + 10;
	else if (value >= 'a' && value <= 'f')
		return value - 'a' + 10;
	else
		return 0;
}

dc_buffer_t *
dctool_convert_hex2bin (const char *str)
{
	// Get the length of the fingerprint data.
	size_t nbytes = (str ? strlen (str) / 2 : 0);
	if (nbytes == 0)
		return NULL;

	// Allocate a memory buffer.
	dc_buffer_t *buffer = dc_buffer_new (nbytes);

	// Convert the hexadecimal string.
	for (unsigned int i = 0; i < nbytes; ++i) {
		unsigned char msn = hex2dec (str[i * 2 + 0]);
		unsigned char lsn = hex2dec (str[i * 2 + 1]);
		unsigned char byte = (msn << 4) + lsn;

		dc_buffer_append (buffer, &byte, 1);
	}

	return buffer;
}

void
dctool_file_write (const char *filename, dc_buffer_t *buffer)
{
	FILE *fp = NULL;

	// Open the file.
	if (filename) {
		fp = fopen (filename, "wb");
	} else {
		fp = stdout;
#ifdef _WIN32
		// Change from text mode to binary mode.
		_setmode (_fileno (fp), _O_BINARY);
#endif
	}
	if (fp == NULL)
		return;

	// Write the entire buffer to the file.
	fwrite (dc_buffer_get_data (buffer), 1, dc_buffer_get_size (buffer), fp);

	// Close the file.
	fclose (fp);
}

dc_buffer_t *
dctool_file_read (const char *filename)
{
	FILE *fp = NULL;

	// Open the file.
	if (filename) {
		fp = fopen (filename, "rb");
	} else {
		fp = stdin;
#ifdef _WIN32
		// Change from text mode to binary mode.
		_setmode (_fileno (fp), _O_BINARY);
#endif
	}
	if (fp == NULL)
		return NULL;

	// Allocate a memory buffer.
	dc_buffer_t *buffer = dc_buffer_new (0);

	// Read the entire file into the buffer.
	size_t n = 0;
	unsigned char block[1024] = {0};
	while ((n = fread (block, 1, sizeof (block), fp)) > 0) {
		dc_buffer_append (buffer, block, n);
	}

	// Close the file.
	fclose (fp);

	return buffer;
}

static dc_status_t
dctool_usbhid_open (dc_iostream_t **out, dc_context_t *context, dc_descriptor_t *descriptor)
{
	dc_status_t status = DC_STATUS_SUCCESS;
	dc_iostream_t *iostream = NULL;

	// Discover the usbhid device.
	dc_iterator_t *iterator = NULL;
	dc_usbhid_device_t *device = NULL;
	dc_usbhid_iterator_new (&iterator, context, descriptor);
	while (dc_iterator_next (iterator, &device) == DC_STATUS_SUCCESS) {
		break;
	}
	dc_iterator_free (iterator);

	if (device == NULL) {
		ERROR ("No dive computer found.");
		status = DC_STATUS_NODEVICE;
		goto cleanup;
	}

	// Open the usbhid device.
	status = dc_usbhid_open (&iostream, context, device);
	if (status != DC_STATUS_SUCCESS) {
		ERROR ("Failed to open the usbhid device.");
		goto cleanup;
	}

	*out = iostream;

cleanup:
	dc_usbhid_device_free (device);
	return status;
}

static dc_status_t
dctool_irda_open (dc_iostream_t **out, dc_context_t *context, dc_descriptor_t *descriptor, const char *devname)
{
	dc_status_t status = DC_STATUS_SUCCESS;
	dc_iostream_t *iostream = NULL;
	unsigned int address = 0;

	if (devname) {
		// Use the address.
		address = strtoul(devname, NULL, 0);
	} else {
		// Discover the device address.
		dc_iterator_t *iterator = NULL;
		dc_irda_device_t *device = NULL;
		dc_irda_iterator_new (&iterator, context, descriptor);
		while (dc_iterator_next (iterator, &device) == DC_STATUS_SUCCESS) {
			address = dc_irda_device_get_address (device);
			dc_irda_device_free (device);
			break;
		}
		dc_iterator_free (iterator);
	}

	if (address == 0) {
		if (devname) {
			ERROR ("No valid device address specified.");
		} else {
			ERROR ("No dive computer found.");
		}
		status = DC_STATUS_NODEVICE;
		goto cleanup;
	}

	// Open the irda socket.
	status = dc_irda_open (&iostream, context, address, 1);
	if (status != DC_STATUS_SUCCESS) {
		ERROR ("Failed to open the irda socket.");
		goto cleanup;
	}

	*out = iostream;

cleanup:
	return status;
}

static dc_status_t
dctool_bluetooth_open (dc_iostream_t **out, dc_context_t *context, dc_descriptor_t *descriptor, const char *devname)
{
	dc_status_t status = DC_STATUS_SUCCESS;
	dc_iostream_t *iostream = NULL;
	dc_bluetooth_address_t address = 0;

	if (devname) {
		// Use the address.
		address = dc_bluetooth_str2addr(devname);
	} else {
		// Discover the device address.
		dc_iterator_t *iterator = NULL;
		dc_bluetooth_device_t *device = NULL;
		dc_bluetooth_iterator_new (&iterator, context, descriptor);
		while (dc_iterator_next (iterator, &device) == DC_STATUS_SUCCESS) {
			address = dc_bluetooth_device_get_address (device);
			dc_bluetooth_device_free (device);
			break;
		}
		dc_iterator_free (iterator);
	}

	if (address == 0) {
		if (devname) {
			ERROR ("No valid device address specified.");
		} else {
			ERROR ("No dive computer found.");
		}
		status = DC_STATUS_NODEVICE;
		goto cleanup;
	}

	// Open the bluetooth socket.
	status = dc_bluetooth_open (&iostream, context, address, 0);
	if (status != DC_STATUS_SUCCESS) {
		ERROR ("Failed to open the bluetooth socket.");
		goto cleanup;
	}

	*out = iostream;

cleanup:
	return status;
}

dc_status_t
dctool_iostream_open (dc_iostream_t **iostream, dc_context_t *context, dc_descriptor_t *descriptor, dc_transport_t transport, const char *devname)
{
	switch (transport) {
	case DC_TRANSPORT_SERIAL:
		return dc_serial_open (iostream, context, devname);
	case DC_TRANSPORT_USB:
		return DC_STATUS_SUCCESS;
	case DC_TRANSPORT_USBHID:
		return dctool_usbhid_open(iostream, context, descriptor);
	case DC_TRANSPORT_IRDA:
		return dctool_irda_open (iostream, context, descriptor, devname);
	case DC_TRANSPORT_BLUETOOTH:
		return dctool_bluetooth_open (iostream, context, descriptor, devname);
	case DC_TRANSPORT_USBSTORAGE:
		return dc_usb_storage_open (iostream, context, devname);
	default:
		return DC_STATUS_UNSUPPORTED;
	}
}
