/* $Id$ */
/* Copyright (c) 2012-2020 Pierre Pronchery <khorben@defora.org> */
/* This file is part of DeforaOS Desktop Phone */
/* Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are
 * met:
 *
 * 1. Redistributions of source code must retain the above copyright notice,
 *    this list of conditions and the following disclaimer.
 *
 * 2. Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY ITS AUTHORS AND CONTRIBUTORS "AS IS" AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED.  IN NO EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */



#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <termios.h>
#include <errno.h>
#include <osmocom/core/select.h>
#include <osmocom/core/serial.h>
#include <osmocom/core/timer.h>
#include <glib.h>
#include <System.h>
#include <Phone/modem.h>
#include "../../config.h"


/* Osmocom */
/* private */
/* types */
struct tool_server
{
	struct osmo_fd bfd;
	uint8_t dlci;
	struct llist_head connections;
};

enum dnload_state
{
	WAITING_PROMPT1,
	WAITING_PROMPT2,
	DOWNLOADING
};

enum romload_state
{
	WAITING_IDENTIFICATION,
	WAITING_PARAM_ACK,
	SENDING_BLOCKS,
	SENDING_LAST_BLOCK,
	LAST_BLOCK_SENT,
	WAITING_BLOCK_ACK,
	WAITING_CHECKSUM_ACK,
	WAITING_BRANCH_ACK,
	FINISHED
};

enum mtk_state
{
	MTK_INIT_1,
	MTK_INIT_2,
	MTK_INIT_3,
	MTK_INIT_4,
	MTK_WAIT_WRITE_ACK,
	MTK_WAIT_ADDR_ACK,
	MTK_WAIT_SIZE_ACK,
	MTK_SENDING_BLOCKS,
	MTK_WAIT_BRANCH_CMD_ACK,
	MTK_WAIT_BRANCH_ADDR_ACK,
	MTK_FINISHED
};

enum dnload_mode
{
	MODE_C123,
	MODE_C123xor,
	MODE_C140,
	MODE_C140xor,
	MODE_C155,
	MODE_ROMLOAD,
	MODE_MTK,
	MODE_INVALID
};

typedef struct _OsmocomDnload
{
	enum dnload_state state;
	enum romload_state romload_state;
	enum mtk_state mtk_state;
	enum dnload_mode mode, previous_mode;
	struct osmo_fd serial_fd;
	char *filename, *previous_filename;
	char *chainload_filename;

	int expect_hdlc;

	int dump_rx;
	int dump_tx;
	int beacon_interval;

	/* data to be downloaded */
	uint8_t *data;
	int data_len;

	uint8_t *write_ptr;

	/* romload: block to be downloaded */
	uint8_t *block;
	int block_len;
	uint8_t block_number;
	uint16_t block_payload_size;
	int romload_dl_checksum;
	uint8_t *block_ptr;
	uint8_t load_address[4];

	uint8_t mtk_send_size[4];
	int block_count;
	int echo_bytecount;

	struct tool_server layer2_server;
	struct tool_server loader_server;
} OsmocomDnload;

typedef struct _ModemPlugin
{
	ModemPluginHelper * helper;

	guint reset;

	/* modem */
	struct osmo_fd fd;
	guint source;
	OsmocomDnload dnload;
	struct osmo_timer_list tick_timer;
} Osmocom;


/* constants */
#define ROMLOAD_INIT_BAUDRATE	B19200
#define ROMLOAD_DL_BAUDRATE	B115200
#define ROMLOAD_BLOCK_HDR_LEN	10
#define ROMLOAD_ADDRESS		0x820000

#define MTK_INIT_BAUDRATE	B19200
#define MTK_ADDRESS		0x40001400
#define MTK_BLOCK_SIZE		1024

static const uint8_t romload_ident_cmd[] = { 0x3c, 0x69 }; /* <i */

/* MTK romloader specific */
static const uint8_t mtk_init_cmd[] = { 0xa0, 0x0a, 0x50, 0x05 };
static const uint8_t mtk_init_resp[] = { 0x5f, 0xf5, 0xaf, 0xfa };
static const uint8_t mtk_command[] = { 0xa1, 0xa2, 0xa4, 0xa8 };


/* variables */
/* FIXME ugly hack to circumvent API limitations in Osmocom */
Osmocom * osmocom;

static ModemConfig _osmocom_config[] =
{
	{ "device",	"Device",		MCT_FILENAME	},
	{ "baudrate",	"Baudrate",		MCT_UINT32	},
	{ "hwflow",	"Hardware flow control",MCT_BOOLEAN	},
	{ NULL,		NULL,			MCT_NONE	}
};


/* prototypes */
static ModemPlugin * _osmocom_init(ModemPluginHelper * helper);
static void _osmocom_destroy(ModemPlugin * modem);
static int _osmocom_start(ModemPlugin * modem, unsigned int retry);
static int _osmocom_stop(ModemPlugin * modem);
static int _osmocom_request(ModemPlugin * modem, ModemRequest * request);

/* callbacks */
static gboolean _osmocom_on_idle(gpointer data);
static gboolean _osmocom_on_reset(gpointer data);

/* XXX re-write both to be non-blocking instead */
static void _osmocom_on_beacon_timer(void * data);
static void _osmocom_on_mtk_timer(void * data);

static int _osmocom_on_serial_read(struct osmo_fd * fd, unsigned int flags);


/* public */
/* variables */
ModemPluginDefinition plugin =
{
	"Osmocom",
	NULL,
	_osmocom_config,
	_osmocom_init,
	_osmocom_destroy,
	_osmocom_start,
	_osmocom_stop,
	_osmocom_request,
	NULL
};


/* private */
/* functions */
/* osmocom_init */
static ModemPlugin * _osmocom_init(ModemPluginHelper * helper)
{
	if((osmocom = object_new(sizeof(*osmocom))) == NULL)
		return NULL;
	memset(osmocom, 0, sizeof(*osmocom));
	osmocom->helper = helper;
	return osmocom;
}


/* osmocom_destroy */
static void _osmocom_destroy(ModemPlugin * modem)
{
	_osmocom_stop(modem);
	object_delete(osmocom);
	osmocom = NULL;
}


/* osmocom_reset */
static int _reset_open(ModemPlugin * modem);
static unsigned int _reset_baudrate(ModemPlugin * modem, unsigned int baudrate);

static int _osmocom_reset(ModemPlugin * modem, unsigned int retry)
{
	Osmocom * osmocom = modem;

	_osmocom_stop(modem);
	if(_reset_open(modem) != 0)
	{
		if(retry > 0)
			osmocom->reset = g_timeout_add(retry,
					_osmocom_on_reset, modem);
		return -1;
	}
#if 0
	osmocom->channel = g_io_channel_unix_new(fd);
	if(g_io_channel_set_encoding(osmocom->channel, NULL, &error)
			                        != G_IO_STATUS_NORMAL)
	{
		modem->helper->error(modem->helper->modem, error->message, 1);
		g_error_free(error);
	}
	g_io_channel_set_buffered(osmocom->channel, FALSE);
	osmocom->
#else
	osmocom->source = g_idle_add(_osmocom_on_idle, modem);
#endif
	return 0;
}

static int _reset_open(ModemPlugin * modem)
{
	Osmocom * osmocom = modem;
	ModemPluginHelper * helper = osmocom->helper;
	char const * device;
	unsigned int baudrate;
	int flags;
	uint32_t tmpaddr = ROMLOAD_ADDRESS;
	char const * p;

	if((device = helper->config_get(helper->modem, "device")) == NULL)
		device = "/dev/modem";
	if((p = helper->config_get(helper->modem, "baudrate")) == NULL
			|| (baudrate = strtoul(p, NULL, 10)) == 0)
		baudrate = 115200;
	baudrate = _reset_baudrate(modem, baudrate);
	if((osmocom->fd.fd = osmo_serial_init(device, baudrate)) < 0)
	{
		/* XXX report error */
#ifdef DEBUG
		fprintf(stderr, "DEBUG: %s: %s\n", device, strerror(errno));
#endif
		return -1;
	}
	if(osmo_fd_register(&osmocom->fd) != 0)
	{
#ifdef DEBUG
		fprintf(stderr, "DEBUG: %s: %s\n", device, strerror(errno));
#endif
		/* XXX report error */
		return -1;
	}
	/* Set serial socket to non-blocking mode of operation */
	if((flags = fcntl(osmocom->fd.fd, F_GETFL)) != -1)
	{
		flags |= O_NONBLOCK;
		fcntl(osmocom->fd.fd, F_SETFL, flags);
	}
	osmocom->dnload.serial_fd.when = BSC_FD_READ;
	osmocom->dnload.serial_fd.cb = _osmocom_on_serial_read;
	if(osmocom->dnload.mode == MODE_ROMLOAD)
	{
		tmpaddr = ROMLOAD_ADDRESS;
		osmo_serial_set_baudrate(osmocom->fd.fd, ROMLOAD_INIT_BAUDRATE);
		osmocom->tick_timer.cb = &_osmocom_on_beacon_timer;
		osmocom->tick_timer.data = modem;
		osmo_timer_schedule(&osmocom->tick_timer, 0,
				osmocom->dnload.beacon_interval);
	}
	else
	{
		tmpaddr = MTK_ADDRESS;
		osmo_serial_set_baudrate(osmocom->fd.fd, MTK_INIT_BAUDRATE);
		osmocom->tick_timer.cb = &_osmocom_on_mtk_timer;
		osmocom->tick_timer.data = modem;
		osmo_timer_schedule(&osmocom->tick_timer, 0,
				osmocom->dnload.beacon_interval);
	}
	/* FIXME not endian proof */
	osmocom->dnload.load_address[0] = (tmpaddr >> 24) & 0xff;
	osmocom->dnload.load_address[1] = (tmpaddr >> 16) & 0xff;
	osmocom->dnload.load_address[2] = (tmpaddr >> 8) & 0xff;
	osmocom->dnload.load_address[3] = tmpaddr & 0xff;
	return 0;
}

static unsigned int _reset_baudrate(ModemPlugin * modem, unsigned int baudrate)
{
	switch(baudrate)
	{
		case 1200:
			return B1200;
		case 2400:
			return B2400;
		case 4800:
			return B4800;
		case 9600:
			return B9600;
		case 19200:
			return B19200;
		case 38400:
			return B38400;
#ifdef B76800
		case 76800:
			return B76800;
#endif
#ifdef B14400
		case 14400:
			return B14400;
#endif
#ifdef B28800
		case 28800:
			return B28800;
#endif
		case 57600:
			return B57600;
		case 115200:
			return B115200;
		case 230400:
			return B230400;
		case 460800:
			return B460800;
		case 921600:
			return B921600;
		default:
			error_set("%u%s", baudrate,
					"Unsupported baudrate (using 115200)");
			modem->helper->error(NULL, error_get(NULL), 1);
			return B115200;
	}
}


/* osmocom_start */
static int _osmocom_start(ModemPlugin * modem, unsigned int retry)
{
#ifdef DEBUG
	fprintf(stderr, "DEBUG: %s()\n", __func__);
#endif
	_osmocom_reset(modem, retry);
	return 0;
}


/* osmocom_stop */
static int _osmocom_stop(ModemPlugin * modem)
{
	Osmocom * osmocom = modem;

#ifdef DEBUG
	fprintf(stderr, "DEBUG: %s()\n", __func__);
#endif
	if(osmocom->source != 0)
	{
		g_source_remove(osmocom->source);
		osmocom->source = 0;
	}
	return 0;
}


/* osmocom_request */
static int _request_call(ModemPlugin * modem, ModemRequest * request);
static int _request_message_send(ModemPlugin * modem, ModemRequest * request);

static int _osmocom_request(ModemPlugin * modem, ModemRequest * request)
{
	switch(request->type)
	{
		case MODEM_REQUEST_CALL:
			return _request_call(modem, request);
		case MODEM_REQUEST_MESSAGE_SEND:
			return _request_message_send(modem, request);
#ifdef DEBUG
		default:
			break;
#endif
	}
	return 0;
}

static int _request_call(ModemPlugin * modem, ModemRequest * request)
{
	Osmocom * osmocom = modem;

	/* FIXME implement */
	return -1;
}

static int _request_message_send(ModemPlugin * modem, ModemRequest * request)
{
	Osmocom * osmocom = modem;

	/* FIXME implement */
	return -1;
}


/* callbacks */
/* osmocom_on_idle */
static gboolean _osmocom_on_idle(gpointer data)
{
	ModemPlugin * modem = data;
	Osmocom * osmocom = modem;

	if(osmo_select_main(0) < 0)
	{
		osmocom->source = 0;
		return FALSE;
	}
	return TRUE;
}


/* osmocom_on_reset */
static gboolean _osmocom_on_reset(gpointer data)
{
	ModemPlugin * modem = data;
	Osmocom * osmocom = modem;

	if(_osmocom_reset(modem, 0) == 0)
	{
		osmocom->reset = 0;
		return FALSE;
	}
	return TRUE;
}


/* osmocom_on_beacon_timer */
static void _osmocom_on_beacon_timer(void * data)
{
	ModemPlugin * modem = data;
	Osmocom * osmocom = modem;
	int rc;

	if(osmocom->dnload.romload_state == WAITING_IDENTIFICATION)
	{
		printf("Sending Calypso romloader beacon...\n");
		rc = write(osmocom->dnload.serial_fd.fd, romload_ident_cmd,
				sizeof(romload_ident_cmd));
		if(rc != sizeof(romload_ident_cmd))
			printf("Error sending identification beacon\n");
		osmo_timer_schedule(&osmocom->tick_timer, 0,
				osmocom->dnload.beacon_interval);
	}
}


/* osmocom_on_mtk_timer */
static void _osmocom_on_mtk_timer(void * data)
{
	ModemPlugin * modem = data;
	Osmocom * osmocom = modem;
	int rc;

	if(osmocom->dnload.mtk_state == MTK_INIT_1)
	{
		printf("Sending MTK romloader beacon...\n");
		rc = write(osmocom->dnload.serial_fd.fd, &mtk_init_cmd[0], 1);
		if(rc != 1)
			printf("Error sending identification beacon\n");
		osmo_timer_schedule(&osmocom->tick_timer, 0,
				osmocom->dnload.beacon_interval);
	}
}


/* osmocom_on_serial_read */
static int _read_handle_romload();
static int _read_handle();
static int _read_handle_write();

static int _osmocom_on_serial_read(struct osmo_fd * fd, unsigned int flags)
{
	int rc;

	/* FIXME really implement */
	if(flags & BSC_FD_READ)
	{
#if 0
		switch(osmocom->mode)
		{
			case MODE_ROMLOAD:
				rc = _read_handle_romload();
				break;
			default:
				rc = _read_handle();
				break;
		}
		if(rc == 0)
			/* XXX wtf? */
			exit(2);
#endif
	}
	if(flags & BSC_FD_WRITE)
	{
		rc = _read_handle_write();
		if(rc != 0)
			osmocom->dnload.state = WAITING_PROMPT1;
	}
	return 0;
}

static int _read_handle_romload()
{
	return -1;
}

static int _read_handle()
{
	return -1;
}

static int _read_handle_write()
{
	return -1;
}
