/*
 * libdivecomputer
 *
 * Copyright (C) 2014 John Van Ostrand
 *
 * 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 <math.h>

#include <libdivecomputer/units.h>

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

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

#define COCHRAN_MODEL_COMMANDER_TM 0
#define COCHRAN_MODEL_COMMANDER_PRE21000 1
#define COCHRAN_MODEL_COMMANDER_AIR_NITROX 2
#define COCHRAN_MODEL_EMC_14 3
#define COCHRAN_MODEL_EMC_16 4
#define COCHRAN_MODEL_EMC_20 5

// Cochran time stamps start at Jan 1, 1992
#define COCHRAN_EPOCH 694242000

#define UNSUPPORTED 0xFFFFFFFF

typedef enum cochran_sample_format_t {
	SAMPLE_TM,
	SAMPLE_CMDR,
	SAMPLE_EMC,
} cochran_sample_format_t;


typedef enum cochran_date_encoding_t {
	DATE_ENCODING_MSDHYM,
	DATE_ENCODING_SMHDMY,
	DATE_ENCODING_TICKS,
} cochran_date_encoding_t;

typedef struct cochran_parser_layout_t {
	cochran_sample_format_t format;
	unsigned int headersize;
	unsigned int samplesize;
	unsigned int pt_sample_interval;
	cochran_date_encoding_t date_encoding;
	unsigned int datetime;
	unsigned int pt_profile_begin;
	unsigned int water_conductivity;
	unsigned int pt_profile_pre;
	unsigned int start_temp;
	unsigned int start_depth;
	unsigned int dive_number;
	unsigned int altitude;
	unsigned int pt_profile_end;
	unsigned int end_temp;
	unsigned int divetime;
	unsigned int max_depth;
	unsigned int avg_depth;
	unsigned int oxygen;
	unsigned int helium;
	unsigned int min_temp;
	unsigned int max_temp;
} cochran_parser_layout_t;

typedef struct cochran_events_t {
	unsigned char code;
	unsigned int data_bytes;
	parser_sample_event_t type;
	parser_sample_flags_t flag;
} cochran_events_t;

typedef struct event_size_t {
	unsigned int code;
	unsigned int size;
} event_size_t;

typedef struct cochran_commander_parser_t {
	dc_parser_t base;
	unsigned int model;
	const cochran_parser_layout_t *layout;
	const event_size_t *events;
	unsigned int nevents;
} cochran_commander_parser_t ;

static dc_status_t cochran_commander_parser_set_data (dc_parser_t *parser, const unsigned char *data, unsigned int size);
static dc_status_t cochran_commander_parser_get_datetime (dc_parser_t *parser, dc_datetime_t *datetime);
static dc_status_t cochran_commander_parser_get_field (dc_parser_t *parser, dc_field_type_t type, unsigned int flags, void *value);
static dc_status_t cochran_commander_parser_samples_foreach (dc_parser_t *parser, dc_sample_callback_t callback, void *userdata);

static const dc_parser_vtable_t cochran_commander_parser_vtable = {
	sizeof(cochran_commander_parser_t),
	DC_FAMILY_COCHRAN_COMMANDER,
	cochran_commander_parser_set_data, /* set_data */
	cochran_commander_parser_get_datetime, /* datetime */
	cochran_commander_parser_get_field, /* fields */
	cochran_commander_parser_samples_foreach, /* samples_foreach */
	NULL /* destroy */
};

static const cochran_parser_layout_t cochran_cmdr_tm_parser_layout = {
	SAMPLE_TM,   // format
	90,          // headersize
	1,           // samplesize
	72,          // pt_sample_interval
	DATE_ENCODING_TICKS, // date_encoding
	15,          // datetime, 4 bytes
	0,           // pt_profile_begin, 4 bytes
	UNSUPPORTED, // water_conductivity, 1 byte, 0=low(fresh), 2=high(sea)
	0,           // pt_profile_pre, 4 bytes
	83,          // start_temp, 1 byte, F
	UNSUPPORTED, // start_depth, 2 bytes, /4=ft
	20,          // dive_number, 2 bytes
	UNSUPPORTED, // altitude, 1 byte, /4=kilofeet
	UNSUPPORTED, // pt_profile_end, 4 bytes
	UNSUPPORTED, // end_temp, 1 byte F
	57,          // divetime, 2 bytes, minutes
	49,          // max_depth, 2 bytes, /4=ft
	51,          // avg_depth, 2 bytes, /4=ft
	74,          // oxygen, 4 bytes (2 of) 2 bytes, /256=%
	UNSUPPORTED, // helium, 4 bytes (2 of) 2 bytes, /256=%
	82,          // min_temp, 1 byte, /2+20=F
	UNSUPPORTED, // max_temp, 1 byte, /2+20=F
};

static const cochran_parser_layout_t cochran_cmdr_1_parser_layout = {
	SAMPLE_CMDR, // format
	256,         // headersize
	2,           // samplesize
	UNSUPPORTED, // pt_sample_interval
	DATE_ENCODING_TICKS, // date_encoding
	8,           // datetime, 4 bytes
	0,           // pt_profile_begin, 4 bytes
	24,          // water_conductivity, 1 byte, 0=low(fresh), 2=high(sea)
	28,          // pt_profile_pre, 4 bytes
	43,          // start_temp, 1 byte, F
	54,          // start_depth, 2 bytes, /4=ft
	68,          // dive_number, 2 bytes
	73,          // altitude, 1 byte, /4=kilofeet
	128,         // pt_profile_end, 4 bytes
	153,         // end_temp, 1 byte F
	166,         // divetime, 2 bytes, minutes
	168,         // max_depth, 2 bytes, /4=ft
	170,         // avg_depth, 2 bytes, /4=ft
	210,         // oxygen, 4 bytes (2 of) 2 bytes, /256=%
	UNSUPPORTED, // helium, 4 bytes (2 of) 2 bytes, /256=%
	232,         // min_temp, 1 byte, /2+20=F
	233,         // max_temp, 1 byte, /2+20=F
};

static const cochran_parser_layout_t cochran_cmdr_parser_layout = {
	SAMPLE_CMDR, // format
	256,         // headersize
	2,           // samplesize
	UNSUPPORTED, // pt_sample_interval
	DATE_ENCODING_MSDHYM, // date_encoding
	0,           // datetime, 6 bytes
	6,           // pt_profile_begin, 4 bytes
	24,          // water_conductivity, 1 byte, 0=low(fresh), 2=high(sea)
	30,          // pt_profile_pre, 4 bytes
	45,          // start_temp, 1 byte, F
	56,          // start_depth, 2 bytes, /4=ft
	70,          // dive_number, 2 bytes
	73,          // altitude, 1 byte, /4=kilofeet
	128,         // pt_profile_end, 4 bytes
	153,         // end_temp, 1 byte F
	166,         // divetime, 2 bytes, minutes
	168,         // max_depth, 2 bytes, /4=ft
	170,         // avg_depth, 2 bytes, /4=ft
	210,         // oxygen, 4 bytes (2 of) 2 bytes, /256=%
	UNSUPPORTED, // helium, 4 bytes (2 of) 2 bytes, /256=%
	232,         // min_temp, 1 byte, /2+20=F
	233,         // max_temp, 1 byte, /2+20=F
};

static const cochran_parser_layout_t cochran_emc_parser_layout = {
	SAMPLE_EMC,  // format
	512,         // headersize
	3,           // samplesize
	UNSUPPORTED, // pt_sample_interval
	DATE_ENCODING_SMHDMY, // date_encoding
	0,           // datetime, 6 bytes
	6,           // pt_profile_begin, 4 bytes
	24,          // water_conductivity, 1 byte 0=low(fresh), 2=high(sea)
	30,          // pt_profile_pre, 4 bytes
	55,          // start_temp, 1 byte, F
	42,          // start_depth, 2 bytes, /256=ft
	86,          // dive_number, 2 bytes,
	89,          // altitude, 1 byte /4=kilofeet
	256,         // pt_profile_end, 4 bytes
	293,         // end_temp, 1 byte, F
	304,         // divetime, 2 bytes, minutes
	306,         // max_depth, 2 bytes, /4=ft
	310,         // avg_depth, 2 bytes, /4=ft
	144,         // oxygen, 6 bytes (3 of) 2 bytes, /256=%
	164,         // helium, 6 bytes (3 of) 2 bytes, /256=%
	403,         // min_temp, 1 byte, /2+20=F
	407,         // max_temp, 1 byte, /2+20=F
};

static const cochran_events_t cochran_events[] = {
	{0xA8, 1, SAMPLE_EVENT_SURFACE,  SAMPLE_FLAGS_BEGIN}, // Entered PDI mode
	{0xA9, 1, SAMPLE_EVENT_SURFACE,  SAMPLE_FLAGS_END},   // Exited PDI mode
	{0xAB, 5, SAMPLE_EVENT_NONE,     SAMPLE_FLAGS_NONE},  // Ceiling decrease
	{0xAD, 5, SAMPLE_EVENT_NONE,     SAMPLE_FLAGS_NONE},  // Ceiling increase
	{0xB5, 1, SAMPLE_EVENT_AIRTIME,  SAMPLE_FLAGS_BEGIN}, // Air < 5 mins deco
	{0xBD, 1, SAMPLE_EVENT_NONE,     SAMPLE_FLAGS_NONE},  // Switched to nomal PO2 setting
	{0xBE, 1, SAMPLE_EVENT_NONE,     SAMPLE_FLAGS_NONE},  // Ceiling > 60 ft
	{0xC0, 1, SAMPLE_EVENT_NONE,     SAMPLE_FLAGS_NONE},  // Switched to FO2 21% mode
	{0xC1, 1, SAMPLE_EVENT_ASCENT,   SAMPLE_FLAGS_BEGIN}, // Ascent rate greater than limit
	{0xC2, 1, SAMPLE_EVENT_NONE,     SAMPLE_FLAGS_NONE},  // Low battery warning
	{0xC3, 1, SAMPLE_EVENT_OLF,      SAMPLE_FLAGS_NONE},  // CNS Oxygen toxicity warning
	{0xC4, 1, SAMPLE_EVENT_MAXDEPTH, SAMPLE_FLAGS_NONE},  // Depth exceeds user set point
	{0xC5, 1, SAMPLE_EVENT_NONE,     SAMPLE_FLAGS_BEGIN}, // Entered decompression mode
	{0xC7, 1, SAMPLE_EVENT_VIOLATION,SAMPLE_FLAGS_BEGIN}, // Entered Gauge mode (e.g. locked out)
	{0xC8, 1, SAMPLE_EVENT_PO2,      SAMPLE_FLAGS_BEGIN}, // PO2 too high
	{0xCC, 1, SAMPLE_EVENT_NONE,     SAMPLE_FLAGS_BEGIN}, // Low Cylinder 1 pressure
	{0xCE, 1, SAMPLE_EVENT_NONE,     SAMPLE_FLAGS_BEGIN}, // Non-decompression warning
	{0xCF, 1, SAMPLE_EVENT_OLF,      SAMPLE_FLAGS_BEGIN}, // O2 Toxicity
	{0xCD, 1, SAMPLE_EVENT_NONE,     SAMPLE_FLAGS_NONE},  // Switched to deco blend
	{0xD0, 1, SAMPLE_EVENT_WORKLOAD, SAMPLE_FLAGS_BEGIN}, // Breathing rate alarm
	{0xD3, 1, SAMPLE_EVENT_NONE,     SAMPLE_FLAGS_NONE},  // Low gas 1 flow rate
	{0xD6, 1, SAMPLE_EVENT_CEILING,  SAMPLE_FLAGS_BEGIN}, // Depth is less than ceiling
	{0xD8, 1, SAMPLE_EVENT_NONE,     SAMPLE_FLAGS_END},   // End decompression mode
	{0xE1, 1, SAMPLE_EVENT_ASCENT,   SAMPLE_FLAGS_END},   // End ascent rate warning
	{0xE2, 1, SAMPLE_EVENT_NONE,     SAMPLE_FLAGS_NONE},  // Low SBAT battery warning
	{0xE3, 1, SAMPLE_EVENT_NONE,     SAMPLE_FLAGS_NONE},  // Switched to FO2 mode
	{0xE5, 1, SAMPLE_EVENT_NONE,     SAMPLE_FLAGS_NONE},  // Switched to PO2 mode
	{0xEE, 1, SAMPLE_EVENT_NONE,     SAMPLE_FLAGS_END},   // End non-decompresison warning
	{0xEF, 1, SAMPLE_EVENT_NONE,     SAMPLE_FLAGS_NONE},  // Switch to blend 2
	{0xF0, 1, SAMPLE_EVENT_WORKLOAD, SAMPLE_FLAGS_END},   // Breathing rate alarm
	{0xF3, 1, SAMPLE_EVENT_NONE,     SAMPLE_FLAGS_NONE},  // Switch to blend 1
	{0xF6, 1, SAMPLE_EVENT_CEILING,  SAMPLE_FLAGS_END},   // End Depth is less than ceiling
};

static const event_size_t cochran_cmdr_event_bytes[] = {
	{0x00, 17}, {0x01, 21}, {0x02, 18},
	{0x03, 17}, {0x06, 19}, {0x07, 19},
	{0x08, 19}, {0x09, 19}, {0x0a, 19},
	{0x0b, 21}, {0x0c, 19}, {0x0d, 19},
	{0x0e, 19}, {0x10, 21},
};

static const event_size_t cochran_emc_event_bytes[] = {
	{0x00, 19}, {0x01, 23}, {0x02, 20},
	{0x03, 19}, {0x06, 21}, {0x07, 21},
	{0x0a, 21}, {0x0b, 21}, {0x0f, 19},
	{0x10, 21},
};


static unsigned int
cochran_commander_handle_event (cochran_commander_parser_t *parser, unsigned char code, dc_sample_callback_t callback, void *userdata)
{
	dc_parser_t *abstract = (dc_parser_t *) parser;

	const cochran_events_t *event = NULL;
	for (unsigned int i = 0; i < C_ARRAY_SIZE(cochran_events); ++i) {
		if (cochran_events[i].code == code) {
			event = cochran_events + i;
			break;
		}
	}

	if (event == NULL) {
		// Unknown event, send warning so we know we missed something
		WARNING(abstract->context, "Unknown event 0x%02x", code);
		return 1;
	}

	switch (code) {
	case 0xAB: // Ceiling decrease
		// Indicated to lower ceiling by 10 ft (deeper)
		// Bytes 1-2: first stop duration (min)
		// Bytes 3-4: total stop duration (min)
		// Handled in calling function
		break;
	case 0xAD: // Ceiling increase
		// Indicates to raise ceiling by 10 ft (shallower)
		// Handled in calling function
		break;
	case 0xC0: // Switched to FO2 21% mode (surface)
		// Event seen upon surfacing
		// handled in calling function
		break;
	case 0xCD: // Switched to deco blend
	case 0xEF: // Switched to gas blend 2
	case 0xF3: // Switched to gas blend 1
		// handled in calling function
		break;
	default:
		// Don't send known events of type NONE
		if (event->type != SAMPLE_EVENT_NONE) {
			dc_sample_value_t sample = {0};
			sample.event.type = event->type;
			sample.event.time = 0;
			sample.event.value = 0;
			sample.event.flags = event->flag;
			if (callback) callback (DC_SAMPLE_EVENT, sample, userdata);
		}
	}

	return event->data_bytes;
}


/*
 * Used to find the end of a dive that has an incomplete dive-end
 * block. It parses backwards past inter-dive events.
 */
static int
cochran_commander_backparse(cochran_commander_parser_t *parser, const unsigned char *samples, int size)
{
	int result = size, best_result = size;

	for (unsigned int i = 0; i < parser->nevents; i++) {
		int ptr = size - parser->events[i].size;
		if (ptr > 0 && samples[ptr] == parser->events[i].code) {
			// Recurse to find the largest match. Because we are parsing backwards
			// and the events vary in size we can't be sure the byte that matches
			// the event code is an event code or data from inside a longer or shorter
			// event.
			result = cochran_commander_backparse(parser, samples, ptr);
		}

		if (result < best_result) {
			best_result = result;
		}
	}

	return best_result;
}


dc_status_t
cochran_commander_parser_create (dc_parser_t **out, dc_context_t *context, unsigned int model)
{
	cochran_commander_parser_t *parser = NULL;
	dc_status_t status = DC_STATUS_SUCCESS;

	if (out == NULL)
		return DC_STATUS_INVALIDARGS;

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

	parser->model = model;

	switch (model) {
	case COCHRAN_MODEL_COMMANDER_TM:
		parser->layout = &cochran_cmdr_tm_parser_layout;
		parser->events = NULL;	// No inter-dive events on this model
		parser->nevents = 0;
		break;
	case COCHRAN_MODEL_COMMANDER_PRE21000:
		parser->layout = &cochran_cmdr_1_parser_layout;
		parser->events = cochran_cmdr_event_bytes;
		parser->nevents = C_ARRAY_SIZE(cochran_cmdr_event_bytes);
		break;
	case COCHRAN_MODEL_COMMANDER_AIR_NITROX:
		parser->layout = &cochran_cmdr_parser_layout;
		parser->events = cochran_cmdr_event_bytes;
		parser->nevents = C_ARRAY_SIZE(cochran_cmdr_event_bytes);
		break;
	case COCHRAN_MODEL_EMC_14:
	case COCHRAN_MODEL_EMC_16:
	case COCHRAN_MODEL_EMC_20:
		parser->layout = &cochran_emc_parser_layout;
		parser->events = cochran_emc_event_bytes;
		parser->nevents = C_ARRAY_SIZE(cochran_emc_event_bytes);
		break;
	default:
		status = DC_STATUS_UNSUPPORTED;
		goto error_free;
	}

	*out = (dc_parser_t *) parser;

	return DC_STATUS_SUCCESS;

error_free:
	dc_parser_deallocate ((dc_parser_t *) parser);
	return status;
}


static dc_status_t
cochran_commander_parser_set_data (dc_parser_t *abstract, const unsigned char *data, unsigned int size)
{
	return DC_STATUS_SUCCESS;
}


static dc_status_t
cochran_commander_parser_get_datetime (dc_parser_t *abstract, dc_datetime_t *datetime)
{
	cochran_commander_parser_t *parser = (cochran_commander_parser_t *) abstract;
	const cochran_parser_layout_t *layout = parser->layout;
	const unsigned char *data = abstract->data;

	if (abstract->size < layout->headersize)
		return DC_STATUS_DATAFORMAT;

	dc_ticks_t ts = 0;

	if (datetime) {
		switch (layout->date_encoding)
		{
		case DATE_ENCODING_MSDHYM:
			datetime->second = data[layout->datetime + 1];
			datetime->minute = data[layout->datetime + 0];
			datetime->hour = data[layout->datetime + 3];
			datetime->day = data[layout->datetime + 2];
			datetime->month = data[layout->datetime + 5];
			datetime->year = data[layout->datetime + 4] + (data[layout->datetime + 4] > 91 ? 1900 : 2000);
			datetime->timezone = DC_TIMEZONE_NONE;
			break;
		case DATE_ENCODING_SMHDMY:
			datetime->second = data[layout->datetime + 0];
			datetime->minute = data[layout->datetime + 1];
			datetime->hour = data[layout->datetime + 2];
			datetime->day = data[layout->datetime + 3];
			datetime->month = data[layout->datetime + 4];
			datetime->year = data[layout->datetime + 5] + (data[layout->datetime + 5] > 91 ? 1900 : 2000);
			datetime->timezone = DC_TIMEZONE_NONE;
			break;
		case DATE_ENCODING_TICKS:
			ts = array_uint32_le(data + layout->datetime) + COCHRAN_EPOCH;
			dc_datetime_localtime(datetime, ts);
			break;
		}
	}

	return DC_STATUS_SUCCESS;
}


static dc_status_t
cochran_commander_parser_get_field (dc_parser_t *abstract, dc_field_type_t type, unsigned int flags, void *value)
{
	const cochran_commander_parser_t *parser = (cochran_commander_parser_t *) abstract;
	const cochran_parser_layout_t *layout = parser->layout;
	const unsigned char *data = abstract->data;
	unsigned int minutes = 0, qfeet = 0;

	dc_gasmix_t *gasmix = (dc_gasmix_t *) value;
	dc_salinity_t *water = (dc_salinity_t *) value;

	if (abstract->size < layout->headersize)
		return DC_STATUS_DATAFORMAT;

	if (value) {
		switch (type) {
		case DC_FIELD_TEMPERATURE_SURFACE:
			*((double *) value) = (data[layout->start_temp] - 32.0) / 1.8;
			break;
		case DC_FIELD_TEMPERATURE_MINIMUM:
			if (data[layout->min_temp] == 0xFF)
				return DC_STATUS_UNSUPPORTED;
			*((double *) value) = (data[layout->min_temp] / 2.0 + 20 - 32) / 1.8;
			break;
		case DC_FIELD_TEMPERATURE_MAXIMUM:
			if (layout->max_temp == UNSUPPORTED)
				return DC_STATUS_UNSUPPORTED;
			if (data[layout->max_temp] == 0xFF)
				return DC_STATUS_UNSUPPORTED;
			*((double *) value) = (data[layout->max_temp] / 2.0 + 20 - 32) / 1.8;
			break;
		case DC_FIELD_DIVETIME:
			minutes = array_uint16_le(data + layout->divetime);
			if (minutes == 0xFFFF)
				return DC_STATUS_UNSUPPORTED;
			*((unsigned int *) value) = minutes * 60;
			break;
		case DC_FIELD_MAXDEPTH:
			qfeet = array_uint16_le(data + layout->max_depth);
			if (qfeet == 0xFFFF)
				return DC_STATUS_UNSUPPORTED;
			*((double *) value) = qfeet / 4.0 * FEET;
			break;
		case DC_FIELD_AVGDEPTH:
			qfeet = array_uint16_le(data + layout->avg_depth);
			if (qfeet == 0xFFFF)
				return DC_STATUS_UNSUPPORTED;
			*((double *) value) = qfeet / 4.0 * FEET;
			break;
		case DC_FIELD_GASMIX_COUNT:
			*((unsigned int *) value) = 2;
			break;
		case DC_FIELD_GASMIX:
			// Gas percentages are decimal and encoded as
			// highbyte = integer portion
			// lowbyte = decimal portion, divide by 256 to get decimal value
			gasmix->oxygen = array_uint16_le (data + layout->oxygen + 2 * flags) / 256.0 / 100;
			if (layout->helium == UNSUPPORTED) {
				gasmix->helium = 0;
			} else {
				gasmix->helium = array_uint16_le (data + layout->helium + 2 * flags) / 256.0 / 100;
			}
			gasmix->nitrogen = 1.0 - gasmix->oxygen - gasmix->helium;
			break;
		case DC_FIELD_SALINITY:
			// 0x00 = low conductivity, 0x10 = high, maybe there's a 0x01 and 0x11?
			// Assume Cochran's conductivity ranges from 0 to 3
			// 0 is fresh water, anything else is sea water
			// for density assume
			//  0 = 1000kg/m³, 2 = 1025kg/m³
			// and other values are linear
			if (layout->water_conductivity == UNSUPPORTED)
				return DC_STATUS_UNSUPPORTED;
			if ((data[layout->water_conductivity] & 0x3) == 0)
				water->type = DC_WATER_FRESH;
			else
				water->type = DC_WATER_SALT;
			water->density = 1000.0 + 12.5 * (data[layout->water_conductivity] & 0x3);
			break;
		case DC_FIELD_ATMOSPHERIC:
			// Cochran measures air pressure and stores it as altitude.
			// Convert altitude (measured in 1/4 kilofeet) back to pressure.
			if (layout->altitude == UNSUPPORTED)
				return DC_STATUS_UNSUPPORTED;
			*(double *) value = ATM / BAR * pow(1 - 0.0000225577 * data[layout->altitude] * 250.0 * FEET, 5.25588);
			break;
		default:
			return DC_STATUS_UNSUPPORTED;
		}
	}

	return DC_STATUS_SUCCESS;
}


/*
 * Parse early Commander computers
 */
static dc_status_t
cochran_commander_parser_samples_foreach_tm (dc_parser_t *abstract, dc_sample_callback_t callback, void *userdata)
{
	cochran_commander_parser_t *parser = (cochran_commander_parser_t *) abstract;
	const cochran_parser_layout_t *layout = parser->layout;
	const unsigned char *data = abstract->data;
	const unsigned char *samples = data + layout->headersize;

	if (abstract->size < layout->headersize)
		return DC_STATUS_DATAFORMAT;

	unsigned int size = abstract->size - layout->headersize;
	unsigned int sample_interval = data[layout->pt_sample_interval];

	dc_sample_value_t sample = {0};
	unsigned int time = 0, last_sample_time = 0;
	unsigned int offset = 2;
	unsigned int deco_ceiling = 0;

	unsigned int temp = samples[0];	// Half degrees F
	unsigned int depth = samples[1];	// Half feet

	last_sample_time = sample.time = time;
	if (callback) callback (DC_SAMPLE_TIME, sample, userdata);

	sample.depth = (depth / 2.0) * FEET;
	if (callback) callback (DC_SAMPLE_DEPTH, sample, userdata);

	sample.temperature = (temp / 2.0 - 32.0) / 1.8;
	if (callback) callback (DC_SAMPLE_TEMPERATURE, sample, userdata);

	sample.gasmix = 0;
	if (callback) callback(DC_SAMPLE_GASMIX, sample, userdata);

	while (offset < size) {
		const unsigned char *s = samples + offset;

		sample.time = time;
		if (last_sample_time != sample.time) {
			// We haven't issued this time yet.
			last_sample_time = sample.time;
			if (callback) callback (DC_SAMPLE_TIME, sample, userdata);
		}

		if (*s & 0x80) {
			// Event or temperate change byte
			if (*s & 0x60) {
				// Event byte
				switch (*s) {
				case 0xC5:  // Deco obligation begins
					break;
				case 0xD8:  // Deco obligation ends
					break;
				case 0xAB:  // Decrement ceiling (deeper)
					deco_ceiling += 10; // feet

					sample.deco.type = DC_DECO_DECOSTOP;
					sample.deco.time = 60; // We don't know the duration
					sample.deco.depth = deco_ceiling * FEET;
					if (callback) callback(DC_SAMPLE_DECO, sample, userdata);
					break;
				case 0xAD:  // Increment ceiling (shallower)
					deco_ceiling -= 10; // feet

					sample.deco.type = DC_DECO_DECOSTOP;
					sample.deco.depth = deco_ceiling * FEET;
					sample.deco.time = 60; // We don't know the duration
					if (callback) callback(DC_SAMPLE_DECO, sample, userdata);
					break;
				default:
					cochran_commander_handle_event(parser, s[0], callback, userdata);
					break;
				}
			} else {
				// Temp change
				if (*s & 0x10)
					temp -= (*s & 0x0f);
				else
					temp += (*s & 0x0f);
				sample.temperature = (temp / 2.0 - 32.0) / 1.8;
				if (callback) callback (DC_SAMPLE_TEMPERATURE, sample, userdata);
			}

			offset++;
			continue;
		}

		// Depth sample
		if (s[0] & 0x40)
			depth -= s[0] & 0x3f;
		else
			depth += s[0] & 0x3f;

		sample.depth = (depth / 2.0) * FEET;
		if (callback) callback (DC_SAMPLE_DEPTH, sample, userdata);

		offset++;
		time += sample_interval;
	}
	return DC_STATUS_SUCCESS;
}


/*
 * Parse Commander I (Pre-21000 s/n), II and EMC computers
 */
static dc_status_t
cochran_commander_parser_samples_foreach_emc (dc_parser_t *abstract, dc_sample_callback_t callback, void *userdata)
{
	cochran_commander_parser_t *parser = (cochran_commander_parser_t *) abstract;
	const cochran_parser_layout_t *layout = parser->layout;
	const unsigned char *data = abstract->data;
	const unsigned char *samples = data + layout->headersize;
	const unsigned char *last_sample = NULL;

	if (abstract->size < layout->headersize)
		return DC_STATUS_DATAFORMAT;

	unsigned int size = abstract->size - layout->headersize;

	dc_sample_value_t sample = {0};
	unsigned int time = 0, last_sample_time = 0;
	unsigned int offset = 0;
	double start_depth = 0;
	int depth = 0;
	unsigned int deco_obligation = 0;
	unsigned int deco_ceiling = 0;
	unsigned int corrupt_dive = 0;

	// In rare circumstances Cochran computers won't record the end-of-dive
	// log entry block. When the end-sample pointer is 0xFFFFFFFF it's corrupt.
	// That means we don't really know where the dive samples end and we don't
	// know what the dive summary values are (i.e. max depth, min temp)
	if (array_uint32_le(data + layout->pt_profile_end) == 0xFFFFFFFF) {
		corrupt_dive = 1;
		dc_datetime_t d;
		cochran_commander_parser_get_datetime(abstract, &d);

		WARNING(abstract->context, "Incomplete dive on %02d/%02d/%02d at %02d:%02d:%02d, trying to parse samples",
				d.year, d.month, d.day, d.hour, d.minute, d.second);

		// Eliminate inter-dive events
		size = cochran_commander_backparse(parser, samples, size);
	}

	// Cochran samples depth every second and varies between ascent rate
	// and temp every other second.

	// Prime values from the dive log section
	if (parser->model == COCHRAN_MODEL_COMMANDER_AIR_NITROX ||
		parser->model == COCHRAN_MODEL_COMMANDER_PRE21000) {
		// Commander stores start depth in quarter-feet
		start_depth = array_uint16_le (data + layout->start_depth) / 4.0;
	} else {
		// EMC stores start depth in 256ths of a foot.
		start_depth = array_uint16_le (data + layout->start_depth) / 256.0;
	}

	last_sample_time = sample.time = time;
	if (callback) callback (DC_SAMPLE_TIME, sample, userdata);

	sample.depth = start_depth * FEET;
	if (callback) callback (DC_SAMPLE_DEPTH, sample, userdata);

	sample.temperature = (data[layout->start_temp] - 32.0) / 1.8;
	if (callback) callback (DC_SAMPLE_TEMPERATURE, sample, userdata);

	sample.gasmix = 0;
	if (callback) callback(DC_SAMPLE_GASMIX, sample, userdata);
	unsigned int last_gasmix = sample.gasmix;

	while (offset < size) {
		const unsigned char *s = samples + offset;

		sample.time = time;
		if (last_sample_time != sample.time) {
			// We haven't issued this time yet.
			last_sample_time = sample.time;
			if (callback) callback (DC_SAMPLE_TIME, sample, userdata);
		}

		// If corrupt_dive end before offset
		if (corrupt_dive) {
			// When we aren't sure where the sample data ends we can
			// look for events that shouldn't be in the sample data.
			// 0xFF is unwritten memory
			// 0xA8 indicates start of post-dive interval
			// 0xE3 (switch to FO2 mode) and 0xF3 (switch to blend 1) occur
			// at dive start so when we see them after the first second we
			// found the beginning of the next dive.
			if (s[0] == 0xFF || s[0] == 0xA8) {
				DEBUG(abstract->context, "Used corrupt dive breakout 1 on event %02x", s[0]);
				break;
			}
			if (time > 1 && (s[0] == 0xE3 || s[0] == 0xF3)) {
				DEBUG(abstract->context, "Used corrupt dive breakout 2 on event %02x", s[0]);
				break;
			}
		}

		// Check for event
		if (s[0] & 0x80) {
			offset += cochran_commander_handle_event(parser, s[0], callback, userdata);

			// Events indicating change in deco status
			switch (s[0]) {
			case 0xC5:  // Deco obligation begins
				deco_obligation = 1;
				break;
			case 0xD8:  // Deco obligation ends
				deco_obligation = 0;
				break;
			case 0xAB:  // Decrement ceiling (deeper)
				deco_ceiling += 10; // feet

				sample.deco.type = DC_DECO_DECOSTOP;
				sample.deco.time = (array_uint16_le(s + 3) + 1) * 60;
				sample.deco.depth = deco_ceiling * FEET;
				if (callback) callback(DC_SAMPLE_DECO, sample, userdata);
				break;
			case 0xAD:  // Increment ceiling (shallower)
				deco_ceiling -= 10; // feet

				sample.deco.type = DC_DECO_DECOSTOP;
				sample.deco.depth = deco_ceiling * FEET;
				sample.deco.time = (array_uint16_le(s + 3) + 1) * 60;
				if (callback) callback(DC_SAMPLE_DECO, sample, userdata);
				break;
			case 0xC0:  // Switched to FO2 21% mode (surface)
				// Event seen upon surfacing
				break;
			case 0xCD:  // Switched to deco blend
			case 0xEF:  // Switched to gas blend 2
				if (last_gasmix != 1) {
					sample.gasmix = 1;
					if (callback) callback(DC_SAMPLE_GASMIX, sample, userdata);
					last_gasmix = sample.gasmix;
				}
				break;
			case 0xF3:  // Switched to gas blend 1
				if (last_gasmix != 0) {
					sample.gasmix = 0;
					if (callback) callback(DC_SAMPLE_GASMIX, sample, userdata);
					last_gasmix = sample.gasmix;
				}
				break;
			}

			continue;
		}

		// Make sure we have a full sample
		if (offset + layout->samplesize > size)
			break;

		// Depth is logged as change in feet, bit 0x40 means negative depth
		if (s[0] & 0x40)
			depth -= (s[0] & 0x3f);
		else
			depth += (s[0] & 0x3f);

		sample.depth = (start_depth + depth / 4.0) * FEET;
		if (callback) callback (DC_SAMPLE_DEPTH, sample, userdata);

		// Ascent rate is logged in the 0th sample, temp in the 1st, repeat.
		if (time % 2 == 0) {
			// Ascent rate
			double ascent_rate = 0.0;
			if (s[1] & 0x80)
				ascent_rate = (s[1] & 0x7f);
			else
				ascent_rate = -(s[1] & 0x7f);
			ascent_rate *= FEET / 4.0;
		} else {
			// Temperature logged in half degrees F above 20
			double temperature = s[1] / 2.0 + 20.0;
			sample.temperature = (temperature - 32.0) / 1.8;

			if (callback) callback (DC_SAMPLE_TEMPERATURE, sample, userdata);
		}

		// Cochran EMC models store NDL and deco stop time
		// in the 20th to 23rd sample
		if (layout->format == SAMPLE_EMC) {
			// Tissue load is recorded across 20 samples, we ignore them
			// NDL and deco stop time is recorded across the next 4 samples
			// The first 2 are either NDL or stop time at deepest stop (if in deco)
			// The next 2 are total deco stop time.
			unsigned int deco_time = 0;
			switch (time % 24) {
			case 21:
				deco_time = last_sample[2] + s[2] * 256 + 1;
				if (deco_obligation) {
					/* Deco time for deepest stop, unused */
				} else {
					/* Send deco NDL sample */
					sample.deco.type = DC_DECO_NDL;
					sample.deco.time = deco_time * 60;
					sample.deco.depth = 0;
					if (callback) callback (DC_SAMPLE_DECO, sample, userdata);
				}
				break;
			case 23:
				/* Deco time, total obligation */
				deco_time = last_sample[2] + s[2] * 256 + 1;
				if (deco_obligation) {
					sample.deco.type = DC_DECO_DECOSTOP;
					sample.deco.depth = deco_ceiling * FEET;
					sample.deco.time = deco_time * 60;
					if (callback) callback (DC_SAMPLE_DECO, sample, userdata);
				}
				break;
			}
			last_sample = s;
		}

		time++;
		offset += layout->samplesize;
	}

	return DC_STATUS_SUCCESS;
}


static dc_status_t
cochran_commander_parser_samples_foreach (dc_parser_t *abstract, dc_sample_callback_t callback, void *userdata)
{
	cochran_commander_parser_t *parser = (cochran_commander_parser_t *) abstract;

	if (parser->model == COCHRAN_MODEL_COMMANDER_TM)
		return cochran_commander_parser_samples_foreach_tm (abstract, callback, userdata);
	else
		return cochran_commander_parser_samples_foreach_emc (abstract, callback, userdata);
}
