/*
 * Deeplu Cosmiq+ parsing
 *
 * Copyright (C) 2019 Linus Torvalds
 *
 * 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 <stdio.h>
#include <stdarg.h>
#include <string.h>

#include "deepblu.h"
#include "context-private.h"
#include "parser-private.h"
#include "array.h"

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

#define MAXFIELDS 128

struct msg_desc;

#define MAXTYPE 16
#define MAXGASES 16
#define MAXSTRINGS 32

typedef struct deepblu_parser_t {
	dc_parser_t base;

	dc_sample_callback_t callback;
	void *userdata;

	// 20 sec for scuba, 1 sec for freedives
	int sample_interval;

	// Field cache
	struct {
		unsigned int initialized;

		// dc_get_field() data
		unsigned int DIVETIME;
		double MAXDEPTH;
		double AVGDEPTH;
		double ATMOSPHERIC;
		dc_divemode_t DIVEMODE;
		unsigned int GASMIX_COUNT;
		dc_salinity_t SALINITY;
		dc_gasmix_t gasmix[MAXGASES];

		dc_field_string_t strings[MAXSTRINGS];
	} cache;
} deepblu_parser_t;

// I *really* need to make this generic
static void add_string(deepblu_parser_t *deepblu, const char *desc, const char *data);
static void add_string_fmt(deepblu_parser_t *deepblu, const char *desc, const char *fmt, ...);

/*
 * Macro to make it easy to set DC_FIELD_xyz values
 */
#define ASSIGN_FIELD(name, value) do { \
	deepblu->cache.initialized |= 1u << DC_FIELD_##name; \
	deepblu->cache.name = (value); \
} while (0)


static dc_status_t deepblu_parser_set_data (dc_parser_t *abstract, const unsigned char *data, unsigned int size);
static dc_status_t deepblu_parser_get_datetime (dc_parser_t *abstract, dc_datetime_t *datetime);
static dc_status_t deepblu_parser_get_field (dc_parser_t *abstract, dc_field_type_t type, unsigned int flags, void *value);
static dc_status_t deepblu_parser_samples_foreach (dc_parser_t *abstract, dc_sample_callback_t callback, void *userdata);

static const dc_parser_vtable_t deepblu_parser_vtable = {
	sizeof(deepblu_parser_t),
	DC_FAMILY_DEEPBLU,
	deepblu_parser_set_data, /* set_data */
	deepblu_parser_get_datetime, /* datetime */
	deepblu_parser_get_field, /* fields */
	deepblu_parser_samples_foreach, /* samples_foreach */
	NULL /* destroy */
};

dc_status_t
deepblu_parser_create (dc_parser_t **out, dc_context_t *context)
{
	deepblu_parser_t *parser = NULL;

	if (out == NULL)
		return DC_STATUS_INVALIDARGS;

	// Allocate memory.
	parser = (deepblu_parser_t *) dc_parser_allocate (context, &deepblu_parser_vtable);
	if (parser == NULL) {
		ERROR (context, "Failed to allocate memory.");
		return DC_STATUS_NOMEMORY;
	}

	*out = (dc_parser_t *) parser;

	return DC_STATUS_SUCCESS;
}

/*
 * FIXME! This should all be generic.
 *
 * Now it's just copied between all the different
 * dive computers that support the strings..
 */
static void add_string(deepblu_parser_t *deepblu, const char *desc, const char *value)
{
	int i;

	deepblu->cache.initialized |= 1 << DC_FIELD_STRING;
	for (i = 0; i < MAXSTRINGS; i++) {
		dc_field_string_t *str = deepblu->cache.strings+i;
		if (str->desc)
			continue;
		str->desc = desc;
		str->value = strdup(value);
		break;
	}
}

static void add_string_fmt(deepblu_parser_t *deepblu, const char *desc, const char *fmt, ...)
{
	char buffer[256];
	va_list ap;

	va_start(ap, fmt);
	buffer[sizeof(buffer)-1] = 0;
	(void) vsnprintf(buffer, sizeof(buffer)-1, fmt, ap);
	va_end(ap);

	add_string(deepblu, desc, buffer);
}


static double
pressure_to_depth(unsigned int mbar)
{
	// Specific weight of seawater (millibar to cm)
	const double specific_weight = 1.024 * 0.980665;

	// Absolute pressure, subtract surface pressure
	if (mbar < 1013)
		return 0.0;
	mbar -= 1013;
	return mbar / specific_weight / 100.0;
}

static dc_status_t
deepblu_parser_set_data (dc_parser_t *abstract, const unsigned char *data, unsigned int size)
{
	deepblu_parser_t *deepblu = (deepblu_parser_t *) abstract;
	const unsigned char *hdr = data;
	const unsigned char *profile = data + 256;
	unsigned int divetime, maxpressure;

	if (size < 256)
		return DC_STATUS_IO;

	deepblu->callback = NULL;
	deepblu->userdata = NULL;
	memset(&deepblu->cache, 0, sizeof(deepblu->cache));

	// LE16 at 0 is 'dive number'

	// LE16 at 12 is the dive time
	// It's in seconds for freedives, minutes for scuba/gauge
	divetime = hdr[12] + 256*hdr[13];

	// Byte at 2 is 'activity type' (2 = scuba, 3 = gauge, 4 = freedive)
	// Byte at 3 is O2 percentage
	switch (data[2]) {
	case 2:
		// SCUBA - divetime in minutes
		divetime *= 60;
		deepblu->cache.gasmix[0].oxygen = data[3] / 100.0;
		deepblu->cache.initialized |= 1u << DC_FIELD_GASMIX;
		ASSIGN_FIELD(GASMIX_COUNT, 1);
		ASSIGN_FIELD(DIVEMODE, DC_DIVEMODE_OC);
		break;
	case 3:
		// GAUGE - divetime in minutes
		divetime *= 60;
		ASSIGN_FIELD(DIVEMODE, DC_DIVEMODE_GAUGE);
		break;
	case 4:
		// FREEDIVE - divetime in seconds
		ASSIGN_FIELD(DIVEMODE, DC_DIVEMODE_FREEDIVE);
		deepblu->sample_interval = 1;
		break;
	default:
		ERROR (abstract->context, "Deepblu: unknown activity type '%02x'", data[2]);
		break;
	}

	// Seems to be fixed at 20s for scuba, 1s for freedive
	deepblu->sample_interval = hdr[26];

	maxpressure = hdr[22] + 256*hdr[23];	// Maxpressure in millibar

	ASSIGN_FIELD(DIVETIME, divetime);
	ASSIGN_FIELD(MAXDEPTH, pressure_to_depth(maxpressure));

	return DC_STATUS_SUCCESS;
}

// The layout of the header in the 'data' is
//  0: LE16 dive number
//  2: dive type byte?
//  3: O2 percentage byte
//  4: unknown
//  5: unknown
//  6: LE16 year
//  8: day of month
//  9: month
// 10: minute
// 11: hour
// 12: LE16 dive time
// 14: LE16 ??
// 16: LE16 surface pressure?
// 18: LE16 ??
// 20: LE16 ??
// 22: LE16 max depth pressure
// 24: LE16 water temp
// 26: LE16 ??
// 28: LE16 ??
// 30: LE16 ??
// 32: LE16 ??
// 34: LE16 ??
static dc_status_t
deepblu_parser_get_datetime (dc_parser_t *abstract, dc_datetime_t *datetime)
{
	deepblu_parser_t *deepblu = (deepblu_parser_t *) abstract;
	const unsigned char *data = deepblu->base.data;
	int len = deepblu->base.size;

	if (len < 256)
		return DC_STATUS_IO;
	datetime->year = data[6] + (data[7] << 8);
	datetime->day = data[8];
	datetime->month = data[9];
	datetime->minute = data[10];
	datetime->hour = data[11];
	datetime->second = 0;
	datetime->timezone = DC_TIMEZONE_NONE;

	return DC_STATUS_SUCCESS;
}

static dc_status_t get_string_field(dc_field_string_t *strings, unsigned idx, dc_field_string_t *value)
{
	if (idx < MAXSTRINGS) {
		dc_field_string_t *res = strings+idx;
		if (res->desc && res->value) {
			*value = *res;
			return DC_STATUS_SUCCESS;
		}
	}
	return DC_STATUS_UNSUPPORTED;
}

// Ugly define thing makes the code much easier to read
// I'd love to use __typeof__, but that's a gcc'ism
#define field_value(p, NAME) \
	(memcpy((p), &deepblu->cache.NAME, sizeof(deepblu->cache.NAME)), DC_STATUS_SUCCESS)
// Hacky hack hack
#define GASMIX gasmix[flags]

static dc_status_t
deepblu_parser_get_field (dc_parser_t *abstract, dc_field_type_t type, unsigned int flags, void *value)
{
	deepblu_parser_t *deepblu = (deepblu_parser_t *) abstract;

	if (!value)
		return DC_STATUS_INVALIDARGS;

	/* This whole sequence should be standardized */
	if (!(deepblu->cache.initialized & (1 << type)))
		return DC_STATUS_UNSUPPORTED;

	switch (type) {
	case DC_FIELD_DIVETIME:
		return field_value(value, DIVETIME);
	case DC_FIELD_MAXDEPTH:
		return field_value(value, MAXDEPTH);
	case DC_FIELD_AVGDEPTH:
		return field_value(value, AVGDEPTH);
	case DC_FIELD_GASMIX_COUNT:
	case DC_FIELD_TANK_COUNT:
		return field_value(value, GASMIX_COUNT);
	case DC_FIELD_GASMIX:
		if (flags >= MAXGASES)
			return DC_STATUS_UNSUPPORTED;
		return field_value(value, GASMIX);
	case DC_FIELD_SALINITY:
		return field_value(value, SALINITY);
	case DC_FIELD_ATMOSPHERIC:
		return field_value(value, ATMOSPHERIC);
	case DC_FIELD_DIVEMODE:
		return field_value(value, DIVEMODE);
	case DC_FIELD_TANK:
		return DC_STATUS_UNSUPPORTED;
	case DC_FIELD_STRING:
		return get_string_field(deepblu->cache.strings, flags, (dc_field_string_t *)value);
	default:
		return DC_STATUS_UNSUPPORTED;
	}
	return DC_STATUS_SUCCESS;
}

static dc_status_t
deepblu_parser_samples_foreach (dc_parser_t *abstract, dc_sample_callback_t callback, void *userdata)
{
	deepblu_parser_t *deepblu = (deepblu_parser_t *) abstract;
	const unsigned char *data = deepblu->base.data;
	int len = deepblu->base.size, i;

	deepblu->callback = callback;
	deepblu->userdata = userdata;

	// Skip the header information
	if (len < 256)
		return DC_STATUS_IO;
	data += 256;
	len -= 256;

	// The rest should be samples every 20s with temperature and depth
	for (i = 0; i < len/4; i++) {
		dc_sample_value_t sample = {0};
		unsigned int temp = data[0]+256*data[1];
		unsigned int pressure = data[2]+256*data[3];

		data += 4;
		sample.time = (i+1)*deepblu->sample_interval;
		if (callback) callback (DC_SAMPLE_TIME, sample, userdata);

		sample.depth = pressure_to_depth(pressure);
		if (callback) callback (DC_SAMPLE_DEPTH, sample, userdata);

		sample.temperature = temp / 10.0;
		if (callback) callback (DC_SAMPLE_TEMPERATURE, sample, userdata);
	}

	return DC_STATUS_SUCCESS;
}
