/******************************************************************************
 *
 *  Copyright (C) 2012 Marvell International Ltd.
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at:
 *
 *  http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 *
 ******************************************************************************/

#define LOG_TAG "hardware_mrvl"

#include <utils/Log.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <assert.h>

#include "bt_vendor_lib.h"
#include "bt_hci_bdroid.h"

#define HCI_CMD_MARVELL_WRITE_PCM_SETTINGS      0xFC07
#define HCI_CMD_MARVELL_WRITE_PCM_SYNC_SETTINGS 0xFC28
#define HCI_CMD_MARVELL_WRITE_PCM_LINK_SETTINGS 0xFC29
#define HCI_CMD_MARVELL_SET_SCO_DATA_PATH       0xFC1D
#define HCI_CMD_MARVELL_WRITE_BD_ADDRESS        0xFC22

#define WRITE_PCM_SETTINGS_SIZE            1
#define WRITE_PCM_SYNC_SETTINGS_SIZE       3
#define WRITE_PCM_LINK_SETTINGS_SIZE       2
#define SET_SCO_DATA_PATH_SIZE             1
#define WRITE_BD_ADDRESS_SIZE              8


#define HCI_CMD_PREAMBLE_SIZE 3

#define HCI_EVT_CMD_CMPL_OPCODE 3

#define STREAM_TO_UINT16(u16, p) \
do { \
	u16 = ((uint16_t)(*(p)) + (((uint16_t)(*((p) + 1))) << 8)); \
	(p) += 2; \
} while (0)

#define UINT16_TO_STREAM(p, u16) \
do { \
	*(p)++ = (uint8_t)(u16); \
	*(p)++ = (uint8_t)((u16) >> 8); \
} while (0)

struct bt_evt_param_t {
	uint16_t cmd;
	uint8_t cmd_ret_param;
};

/***********************************************************
 *  Externs
 ***********************************************************
 */
extern unsigned char bdaddr[6];
extern bt_vendor_callbacks_t *vnd_cb;

/***********************************************************
 *  Local variables
 ***********************************************************
 */
static uint8_t write_pcm_settings[WRITE_PCM_SETTINGS_SIZE] = {
	0x02
};

static uint8_t write_pcm_sync_settings[WRITE_PCM_SYNC_SETTINGS_SIZE] = {
	0x03,
	0x00,
	0x03
};

static uint8_t write_pcm_link_settings[WRITE_PCM_LINK_SETTINGS_SIZE] = {
	0x03,
	0x00
};

static uint8_t set_sco_data_path[SET_SCO_DATA_PATH_SIZE] = {
	0x01
};

static uint8_t write_bd_address[WRITE_BD_ADDRESS_SIZE] = {
	0xFE, /* Parameter ID */
	0x06, /* bd_addr length */
	0x00, /* 6th byte of bd_addr */
	0x00, /* 5th */
	0x00, /* 4th */
	0x00, /* 3rd */
	0x00, /* 2nd */
	0x00  /* 1st */
};

/***********************************************************
 *  Local functions
 ***********************************************************
 */
static char *cmd_to_str(uint16_t cmd)
{
	switch (cmd) {
	case HCI_CMD_MARVELL_WRITE_PCM_SETTINGS:
		return "write_pcm_settings";
	case HCI_CMD_MARVELL_WRITE_PCM_SYNC_SETTINGS:
		return "write_pcm_sync_settings";
	case HCI_CMD_MARVELL_WRITE_PCM_LINK_SETTINGS:
		return "write_pcm_link_settings";
	case HCI_CMD_MARVELL_SET_SCO_DATA_PATH:
		return "set_sco_data_path";
	case HCI_CMD_MARVELL_WRITE_BD_ADDRESS:
		return "write_bd_address";
	default:
		break;
	}

	return "unknown command";
}

static void populate_bd_addr_params(uint8_t *params, uint8_t *addr)
{
	assert(params && addr);

	*params++ = addr[5];
	*params++ = addr[4];
	*params++ = addr[3];
	*params++ = addr[2];
	*params++ = addr[1];
	*params   = addr[0];
}

static HC_BT_HDR *build_cmd_buf(uint16_t cmd, uint8_t pl_len, uint8_t *payload)
{
	HC_BT_HDR *p_buf = NULL;
	uint16_t cmd_len = HCI_CMD_PREAMBLE_SIZE + pl_len;
	uint8_t *p;

	assert(vnd_cb && payload);

	p_buf = (HC_BT_HDR *) vnd_cb->alloc(BT_HC_HDR_SIZE + cmd_len);

	if (!p_buf)
		return NULL;

	p_buf->event = MSG_STACK_TO_HC_HCI_CMD;
	p_buf->offset = 0;
	p_buf->layer_specific = 0;
	p_buf->len = cmd_len;

	p = (uint8_t *) (p_buf + 1);

	/* opcode */
	UINT16_TO_STREAM(p, cmd);

	/* length of payload */
	*p = pl_len;
	++p;

	/* payload */
	memcpy(p, payload, pl_len);

	return p_buf;
}

static void parse_evt_buf(HC_BT_HDR *p_evt_buf,
		struct bt_evt_param_t *evt_params)
{
	uint8_t *p = (uint8_t *) (p_evt_buf + 1) + HCI_EVT_CMD_CMPL_OPCODE;

	assert(p_evt_buf && evt_params);

	/* opcode */
	STREAM_TO_UINT16(evt_params->cmd, p);

	/* command return parameter */
	evt_params->cmd_ret_param = *p;
}

static void hw_mrvl_config_start_cb(void *p_mem)
{
	HC_BT_HDR *p_evt_buf = (HC_BT_HDR *) p_mem;
	struct bt_evt_param_t evt_params = {0, 0};

	assert(vnd_cb && p_mem);

	parse_evt_buf(p_evt_buf, &evt_params);

	/* free the buffer */
	vnd_cb->dealloc(p_evt_buf);

	switch (evt_params.cmd) {
	case HCI_CMD_MARVELL_WRITE_BD_ADDRESS:
		/* fw config succeeds */
		ALOGI("FW config succeeds!");
		vnd_cb->fwcfg_cb(BT_VND_OP_RESULT_SUCCESS);
		return;

	default:
		ALOGE("Received event for unexpected cmd (0x%04hX). Fail.",
			evt_params.cmd);
		break;
	} /* end of switch (evt_params.cmd) */

	ALOGE("Vendor lib fwcfg aborted");
	vnd_cb->fwcfg_cb(BT_VND_OP_RESULT_FAIL);
}

static void hw_mrvl_sco_config_cb(void *p_mem)
{
	HC_BT_HDR *p_evt_buf = (HC_BT_HDR *) p_mem;
	struct bt_evt_param_t evt_params = {0, 0};
	uint16_t cmd;
	HC_BT_HDR *p_buf;

	assert(vnd_cb && p_mem);

	parse_evt_buf(p_evt_buf, &evt_params);

	/* free the buffer */
	vnd_cb->dealloc(p_evt_buf);

	switch (evt_params.cmd) {
	case HCI_CMD_MARVELL_WRITE_PCM_SETTINGS:
		/* Send HCI_CMD_MARVELL_WRITE_PCM_SYNC_SETTINGS */
		cmd = HCI_CMD_MARVELL_WRITE_PCM_SYNC_SETTINGS;
		p_buf = build_cmd_buf(cmd,
				WRITE_PCM_SYNC_SETTINGS_SIZE,
				write_pcm_sync_settings);
		break;

	case HCI_CMD_MARVELL_WRITE_PCM_SYNC_SETTINGS:
		/* Send HCI_CMD_MARVELL_WRITE_PCM_LINK_SETTINGS */
		cmd = HCI_CMD_MARVELL_WRITE_PCM_LINK_SETTINGS;
		p_buf = build_cmd_buf(cmd,
				WRITE_PCM_LINK_SETTINGS_SIZE,
				write_pcm_link_settings);
		break;

	case HCI_CMD_MARVELL_WRITE_PCM_LINK_SETTINGS:
		/* Send HCI_CMD_MARVELL_SET_SCO_DATA_PATH */
		cmd = HCI_CMD_MARVELL_SET_SCO_DATA_PATH;
		p_buf = build_cmd_buf(cmd,
				SET_SCO_DATA_PATH_SIZE,
				set_sco_data_path);
		break;

	case HCI_CMD_MARVELL_SET_SCO_DATA_PATH:
		/* sco config succeeds */
		ALOGI("SCO PCM config succeeds!");
		vnd_cb->scocfg_cb(BT_VND_OP_RESULT_SUCCESS);
		return;

	default:
		ALOGE("Received event for unexpected cmd (0x%04hX). Fail.",
			evt_params.cmd);
		p_buf = NULL;
		break;
	} /* switch (evt_params.cmd) */

	if (p_buf) {
		ALOGI("Sending hci command 0x%04hX (%s)", cmd, cmd_to_str(cmd));
		if (vnd_cb->xmit_cb(cmd, p_buf, hw_mrvl_sco_config_cb))
			return;
		else
			vnd_cb->dealloc(p_buf);
	}

	ALOGE("Vendor lib scocfg aborted");
	vnd_cb->scocfg_cb(BT_VND_OP_RESULT_FAIL);
}

/***********************************************************
 *  Global functions
 ***********************************************************
 */
void hw_mrvl_config_start(void)
{
	HC_BT_HDR *p_buf;
	uint16_t cmd;

	assert(vnd_cb);

	ALOGI("Start HW config ...");
	/* Start with HCI_CMD_MARVELL_WRITE_BD_ADDRESS */
	ALOGI("Setting bd addr to %02hhX:%02hhX:%02hhX:%02hhX:%02hhX:%02hhX",
		bdaddr[0], bdaddr[1], bdaddr[2],
		bdaddr[3], bdaddr[4], bdaddr[5]);
	populate_bd_addr_params(write_bd_address + 2, bdaddr);

	cmd   = HCI_CMD_MARVELL_WRITE_BD_ADDRESS;
	p_buf = build_cmd_buf(cmd,
			WRITE_BD_ADDRESS_SIZE,
			write_bd_address);

	if (p_buf) {
		ALOGI("Sending hci command 0x%04hX (%s)", cmd, cmd_to_str(cmd));
		if (vnd_cb->xmit_cb(cmd, p_buf, hw_mrvl_config_start_cb))
			return;
		else
			vnd_cb->dealloc(p_buf);
	}

	ALOGE("Vendor lib fwcfg aborted");
	vnd_cb->fwcfg_cb(BT_VND_OP_RESULT_FAIL);
}


void hw_mrvl_sco_config(void)
{
	HC_BT_HDR *p_buf;
	uint16_t cmd;

	assert(vnd_cb);

	ALOGI("Start SCO config ...");
	/* Start with HCI_CMD_MARVELL_WRITE_PCM_SETTINGS */
	cmd   = HCI_CMD_MARVELL_WRITE_PCM_SETTINGS;
	p_buf = build_cmd_buf(cmd,
			WRITE_PCM_SETTINGS_SIZE,
			write_pcm_settings);

	if (p_buf) {
		ALOGI("Sending hci command 0x%04hX (%s)", cmd, cmd_to_str(cmd));
		if (vnd_cb->xmit_cb(cmd, p_buf, hw_mrvl_sco_config_cb))
			return;
		else
			vnd_cb->dealloc(p_buf);
	}

	ALOGE("Vendor lib scocfg aborted");
	vnd_cb->scocfg_cb(BT_VND_OP_RESULT_FAIL);
}