/* mars.c
 *
 * Copyright (C) 2004 Theodore Kilgore <kilgota@auburn.edu>
 *
 * white_balance() Copyright (C) 2008 Theodore Kilgore and Amauri Magagna.
 *
 * 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 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
 */

#define _DEFAULT_SOURCE

#include <config.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <fcntl.h>
#include <string.h>
#include <math.h>
#include <unistd.h>

#include <libgphoto2/gamma.h>

#include <gphoto2/gphoto2.h>
#include <gphoto2/gphoto2-port.h>
#include "libgphoto2/gphoto2-endian.h"

#include "mars.h"

#define GP_MODULE "mars"

#define INIT 		0xb5
#define GET_DATA 	0x0f

static int
m_read (GPPort *port, char *data, int size)
{
	int ret;
	ret = gp_port_write(port, "\x21", 1);
	if (ret < GP_OK) return ret;

	return gp_port_read(port, data, 16);
}

static int
m_command (GPPort *port, char *command, int size, char *response)
{
	gp_port_write(port, command, size);
	return m_read(port, response, 16);
}

static int mars_routine (Info *info, GPPort *port, char param, int n);

int
mars_init (Camera *camera, GPPort *port, Info *info)
{
	char c[16];
	unsigned char status = 0;
	memset(info,0, sizeof(*info));
	memset(c,0,sizeof(c));
	GP_DEBUG("Running mars_init\n");

	/* Init routine done twice, usually. First time is a dry run. But if
	 * camera reports 0x02 it is "jammed" and we must clear it.
	 */

	m_read(port, c, 16);
	if (c[0] == 0x02) {
		gp_port_write(port, "\x19", 1);
		gp_port_read(port, c, 16);
	}
	else {
		status = mars_routine (info, port, INIT, 0);
		GP_DEBUG("status = 0x%x\n", status);
	}

	/* Not a typo. This _will_ download the config data ;) */
	mars_read_picture_data (camera, info, port, (char *)info, 0x2000, 0);

	/* Removing extraneous line(s) of data. See "protocol.txt" */

	if ((info[0] == 0xff)&& (info[1] == 0)&&(info[2]==0xff))
		memmove(info, info + 16, 0x1ff0); /* Saving config */
	else
		memmove(info, info + 144, 0x1f70); /* Saving config */

	GP_DEBUG("Leaving mars_init\n");
	return GP_OK;
}

int
mars_get_num_pics      (Info *info)
{
	unsigned int i = 0;

	for (i = 0; i < 0x3fe; i++)
		if ( !(0xff - info[8*i]) )  {
			GP_DEBUG ( "i is %i\n", i);
			memcpy(info+0x1ff0, "i", 1);
			return i;
		}
	memcpy(info+0x1ff0, "0", 1);
	return 0;
}


int
mars_get_pic_data_size (Info *info, int n)
{
	return (info[8*n+6]*0x100 + info[8*n+5])*0x100 + info[8*n+4];
}

static int
set_usb_in_endpoint (Camera *camera, int inep)
{
	GPPortSettings settings;
	gp_port_get_settings ( camera ->port, &settings);
	settings.usb.inep = inep;
	GP_DEBUG("inep reset to %02X\n", inep);
	return gp_port_set_settings ( camera ->port, settings);
}


static int
mars_read_data (GPPort *port, char *data, int size)
{
	int MAX_BULK = 0x2000;
	int len = 0;
	while(size > 0) {
		len = (size>MAX_BULK)?MAX_BULK:size;
		gp_port_read  (port, data, len);
		data += len;
		size -= len;
	}

	return 1;
}

int
mars_read_picture_data (Camera *camera, Info *info, GPPort *port,
					char *data, int size, int n)

{
	unsigned char c[16];

	memset(c,0,sizeof(c));
	/*Initialization routine for download. */
	mars_routine (info, port, GET_DATA, n);
	/*Data transfer begins*/
	set_usb_in_endpoint (camera, 0x82);
	mars_read_data (port, data, size);
	set_usb_in_endpoint (camera, 0x83);
	return GP_OK;
}

int
mars_reset (GPPort *port)
{
	gp_port_write(port, "\x19\x54", 2);
	return GP_OK;
}

static void precalc_table(code_table_t *table)
{
	int i;
	int is_abs, val, len;

	for (i = 0; i < 256; i++) {
		is_abs = 0;
		val = 0;
		len = 0;
		if ((i & 0x80) == 0) {
			/* code 0 */
			val = 0;
			len = 1;
		} else if ((i & 0xE0) == 0xC0) {
			/* code 110 */
			val = -3;
			len = 3;
		} else if ((i & 0xE0) == 0xA0) {
			/* code 101 */
			val = +3;
			len = 3;
		} else if ((i & 0xF0) == 0x80) {
			/* code 1000 */
			val = +8;
			len = 4;
		} else if ((i & 0xF0) == 0x90) {
			/* code 1001 */
			val = -8;
			len = 4;
		} else if ((i & 0xF0) == 0xF0) {
			/* code 1111 */
			val = -20;
			len = 4;
		} else if ((i & 0xF8) == 0xE0) {
			/* code 11100 */
			val = +20;
			len = 5;
		} else if ((i & 0xF8) == 0xE8) {
			/* code 11101xxxxx */
			is_abs = 1;
			val = 0;	/* value is calculated later */
			len = 5;
		}
		table[i].is_abs = is_abs;
		table[i].val = val;
		table[i].len = len;
	}
}

#define CLAMP(x)	((x)<0?0:((x)>255)?255:(x))
#define GET_CODE 	addr = inp + (bitpos >> 3); \
	    code = (addr[0] << (bitpos & 7)) | (addr[1] >> (8 - (bitpos & 7)))


int mars_decompress (unsigned char *inp, unsigned char *outp, int width,
   int height)
{
	int row, col;
	unsigned char code;
	int val;
	code_table_t table[256];
	unsigned char *addr;
	int bitpos;
	unsigned char lp=0,tp=0,tlp=0,trp=0;
	/* First calculate the Huffman table */
	precalc_table(table);

	bitpos = 0;

	/* main decoding loop */
	for (row = 0; row < height; row++) {
		col = 0;

	/* first two pixels in first two rows are stored as raw 8-bit */
	if (row < 2) {
		GET_CODE;
		bitpos += 8;
		*outp++ = code;

		GET_CODE;
		bitpos += 8;
		*outp++ = code;

		col += 2;
	}

	while (col < width) {
		/* get bitcode */
		GET_CODE;
		/* update bit position */
		bitpos += table[code].len;

		/* calculate pixel value */
		if (table[code].is_abs) {
			/* get 5 more bits and use them as absolute value */
			GET_CODE;
			val = (code & 0xF8);
			bitpos += 5;

		} else {
			/* value is relative to top or left pixel */
			val = table[code].val;
			lp =  outp[-2];
			if (row > 1) {
				if (col > 1)
					tlp = outp[-2*width-2];
				tp  = outp[-2*width];
				if (col < width-2)
					trp = outp[-2*width+2];
			}
			if (row < 2) {
				/* top row: relative to left pixel */
				val += lp;
			} else if (col < 2) {
				/* left column: relative to top pixel */
				/* initial estimate */
				val += (tp + trp)/2;
			} else if (col > width - 3) {
				/* left column: relative to top pixel */
				val += (tp + lp + tlp +1)/3;
				/* main area: average of left and top pixel */
			} else {
				/* initial estimate for predictor */
				tlp>>=1;
				trp>>=1;
				val += (lp + tp + tlp + trp +1)/3;
			}
		}
		/* store pixel */
		*outp++ = CLAMP(val);
		col++;
	}
	}
	return GP_OK;
}

static int
mars_routine (Info *info, GPPort *port, char param, int n)
{
	char c[16];
	char start[2] = {0x19, 0x51};
	char do_something[2];
	char address1[2];
	char address2[2];
	char address3[2];
	char address4[2];
	char address5[2];
	char address6[2];

	do_something[0]= 0x19;
	do_something[1]=param;

	/* See protocol.txt for my theories about what these mean. */
	address1[0] = 0x19;
	address1[1] = info[8*n+1];
	address2[0] = 0x19;
	address2[1] = info[8*n+2];
	address3[0] = 0x19;
	address3[1] = info[8*n+3];
	address4[0] = 0x19;
	address4[1] = info[8*n+4];
	address5[0] = 0x19;
	address5[1] = info[8*n+5];
	address6[0] = 0x19;
	address6[1] = info[8*n+6];


	memset(c,0,sizeof(c));

	/*Routine used in initialization, photo download, and reset. */
	m_read(port, c, 16);
	m_command(port, start, 2, c);
	m_command(port, do_something, 2, c);
	m_command(port, address1, 2, c);

	c[0] = 0;
	gp_port_write(port, address2, 2);
	/* Moving the memory cursor to the given address? */
	while ((c[0] != 0xa)) {
		if (m_read(port, c, 16) < 16)
			break;
	}

	m_command(port, address3, 2, c);
	m_command(port, address4, 2, c);
	m_command(port, address5, 2, c);
	m_command(port, address6, 2, c);
	gp_port_write(port, "\x19", 1);
	gp_port_read(port, c , 16);
	/* Next thing is to switch the inep. Some cameras need a pause here */
	usleep (MARS_SLEEP);

	return(c[0]);
}


/*
 *	========= White Balance / Color Enhance / Gamma adjust ===============
 *
 *	Get histogram for each color plane
 *	Expand to reach 0.5% of white dots in image
 *
 *	Get new histogram for each color plane
 *	Expand to reach 0.5% of black dots in image
 *
 *	Get new histogram
 *	Calculate and apply gamma correction
 *
 *	if not a dark image:
 *	For each dot, increases color separation
 *
 *	======================================================================
 */

int
histogram (unsigned char *data, unsigned int size, int *htable_r, int *htable_g, int *htable_b)
{
	unsigned int x;
	/* Initializations */
	for (x = 0; x < 0x100; x++) {
		htable_r[x] = 0;
		htable_g[x] = 0;
		htable_b[x] = 0;
	}
	/* Building the histograms */
	for (x = 0; x < (size * 3); x += 3)
	{
		htable_r[data[x+0]]++;	/* red histogram */
		htable_g[data[x+1]]++;	/* green histogram */
		htable_b[data[x+2]]++;	/* blue histogram */
	}
	return 0;
}

int
mars_white_balance (unsigned char *data, unsigned int size, float saturation,
						float image_gamma)
{
	unsigned int x, max;
	int r, g, b, d;
	double r_factor, g_factor, b_factor, max_factor;
	int htable_r[0x100], htable_g[0x100], htable_b[0x100];
	unsigned char gtable[0x100];
	double new_gamma, gamma=1.0;

	/* ------------------- GAMMA CORRECTION ------------------- */

	histogram(data, size, htable_r, htable_g, htable_b);
	x = 1;
	for (d = 48; d < 208; d++)
	{
		x += htable_r[d];
		x += htable_g[d];
		x += htable_b[d];
	}
	new_gamma = sqrt((double) (x * 1.5) / (double) (size * 3));
	x=0;
	GP_DEBUG("Provisional gamma correction = %1.2f\n", new_gamma);
	/* Recalculate saturation factor for later use. */
	saturation=saturation*new_gamma*new_gamma;
	GP_DEBUG("saturation = %1.2f\n", saturation);
	if(new_gamma >= 1.0)
		gamma = new_gamma;
	else
		gamma = image_gamma;

	GP_DEBUG("Gamma correction = %1.2f\n", gamma);
	gp_gamma_fill_table(gtable, gamma);

	/* ---------------- BRIGHT DOTS ------------------- */
	max = size / 200;
	histogram(data, size, htable_r, htable_g, htable_b);

	for (r=0xfe, x=0; (r > 32) && (x < max); r--)
		x += htable_r[r];
	for (g=0xfe, x=0; (g > 32) && (x < max); g--)
		x += htable_g[g];
	for (b=0xfe, x=0; (b > 32) && (x < max); b--)
		x += htable_b[b];
	r_factor = (double) 0xfd / r;
	g_factor = (double) 0xfd / g;
	b_factor = (double) 0xfd / b;

	max_factor = r_factor;
	if (g_factor > max_factor) max_factor = g_factor;
	if (b_factor > max_factor) max_factor = b_factor;

	if (max_factor >= 2.5) {
		r_factor = (r_factor / max_factor) * 2.5;
		g_factor = (g_factor / max_factor) * 2.5;
		b_factor = (b_factor / max_factor) * 2.5;
	}
	GP_DEBUG("White balance (bright): r=%1d, g=%1d, b=%1d, fr=%1.3f, fg=%1.3f, fb=%1.3f\n", r, g, b, r_factor, g_factor, b_factor);
	if (max_factor <= 2.5) {
		for (x = 0; x < (size * 3); x += 3)
		{
			d = (data[x+0]<<8) * r_factor;
			d >>=8;
			if (d > 0xff) { d = 0xff; }
			data[x+0] = d;
			d = (data[x+1]<<8) * g_factor;
			d >>=8;
			if (d > 0xff) { d = 0xff; }
			data[x+1] = d;
			d = (data[x+2]<<8) * b_factor;
			d >>=8;
			if (d > 0xff) { d = 0xff; }
			data[x+2] = d;
		}
	}
	/* ---------------- DARK DOTS ------------------- */
	max = size / 200;  /*  1/200 = 0.5%  */
	histogram(data, size, htable_r, htable_g, htable_b);

	for (r=0, x=0; (r < 96) && (x < max); r++)
		x += htable_r[r];
	for (g=0, x=0; (g < 96) && (x < max); g++)
		x += htable_g[g];
	for (b=0, x=0; (b < 96) && (x < max); b++)
		x += htable_b[b];

	r_factor = (double) 0xfe / (0xff-r);
	g_factor = (double) 0xfe / (0xff-g);
	b_factor = (double) 0xfe / (0xff-b);

	max_factor = r_factor;
	if (g_factor > max_factor) max_factor = g_factor;
	if (b_factor > max_factor) max_factor = b_factor;

	if (max_factor >= 1.15) {
		r_factor = (r_factor / max_factor) * 1.15;
		g_factor = (g_factor / max_factor) * 1.15;
		b_factor = (b_factor / max_factor) * 1.15;
	}
	GP_DEBUG(
	"White balance (dark): r=%1d, g=%1d, b=%1d, fr=%1.3f, fg=%1.3f, fb=%1.3f\n",
				r, g, b, r_factor, g_factor, b_factor);

	for (x = 0; x < (size * 3); x += 3)
	{
		d = (int) 0xff08-(((0xff-data[x+0])<<8) * r_factor);
		d >>= 8;
		if (d < 0) { d = 0; }
		data[x+0] = d;
		d = (int) 0xff08-(((0xff-data[x+1])<<8) * g_factor);
		d >>= 8;
		if (d < 0) { d = 0; }
		data[x+1] = d;
		d = (int) 0xff08-(((0xff-data[x+2])<<8) * b_factor);
		d >>= 8;
		if (d < 0) { d = 0; }
		data[x+2] = d;
	}

	/* ------------------ COLOR ENHANCE ------------------ */

	if(saturation > 0.0) {
		for (x = 0; x < (size * 3); x += 3)
		{
			r = data[x+0]; g = data[x+1]; b = data[x+2];
			d = (int) (r + g + b) /3.;
			if ( r > d )
				r = r + (int) ((r - d) * (0xff-r)/(0x100-d) * saturation);
			else
				r = r + (int) ((r - d) * (0xff-d)/(0x100-r) * saturation);
			if (g > d)
				g = g + (int) ((g - d) * (0xff-g)/(0x100-d) * saturation);
			else
				g = g + (int) ((g - d) * (0xff-d)/(0x100-g) * saturation);
			if (b > d)
				b = b + (int) ((b - d) * (0xff-b)/(0x100-d) * saturation);
			else
				b = b + (int) ((b - d) * (0xff-d)/(0x100-b) * saturation);
			data[x+0] = CLAMP(r);
			data[x+1] = CLAMP(g);
			data[x+2] = CLAMP(b);
		}
	}
	return 0;
}
