// SPDX-License-Identifier: GPL-2.0-or-later
/*
 *  Copyright (c) 2009-2010 Atheros Communications Inc.
 *
 */

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

#define _GNU_SOURCE
#include <stdio.h>
#include <errno.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <time.h>
#include <limits.h>
#include <sys/stat.h>
#include <sys/time.h>
#include <sys/types.h>
#include <sys/param.h>
#include <sys/ioctl.h>

#include "bluetooth/bluetooth.h"
#include "bluetooth/hci.h"
#include "bluetooth/hci_lib.h"

#include "hciattach.h"

#define TRUE    1
#define FALSE   0

#define FW_PATH "/lib/firmware/ar3k/"

struct ps_cfg_entry {
	uint32_t id;
	uint32_t len;
	uint8_t *data;
};

struct ps_entry_type {
	unsigned char type;
	unsigned char array;
};

#define MAX_TAGS              50
#define PS_HDR_LEN            4
#define HCI_VENDOR_CMD_OGF    0x3F
#define HCI_PS_CMD_OCF        0x0B

struct ps_cfg_entry ps_list[MAX_TAGS];

static void load_hci_ps_hdr(uint8_t *cmd, uint8_t ps_op, int len, int index)
{
	hci_command_hdr *ch = (void *)cmd;

	ch->opcode = htobs(cmd_opcode_pack(HCI_VENDOR_CMD_OGF,
						HCI_PS_CMD_OCF));
	ch->plen = len + PS_HDR_LEN;
	cmd += HCI_COMMAND_HDR_SIZE;

	cmd[0] = ps_op;
	cmd[1] = index;
	cmd[2] = index >> 8;
	cmd[3] = len;
}

#define PS_EVENT_LEN 100

/*
 * Send HCI command and wait for command complete event.
 * The event buffer has to be freed by the caller.
 */
static int send_hci_cmd_sync(int dev, uint8_t *cmd, int len, uint8_t **event)
{
	int err;
	uint8_t *hci_event;
	uint8_t pkt_type = HCI_COMMAND_PKT;

	if (len == 0)
		return len;

	if (write(dev, &pkt_type, 1) != 1)
		return -EILSEQ;
	if (write(dev, (unsigned char *)cmd, len) != len)
		return -EILSEQ;

	hci_event = (uint8_t *)malloc(PS_EVENT_LEN);
	if (!hci_event)
		return -ENOMEM;

	err = read_hci_event(dev, (unsigned char *)hci_event, PS_EVENT_LEN);
	if (err > 0) {
		*event = hci_event;
	} else {
		free(hci_event);
		return -EILSEQ;
	}

	return len;
}

#define HCI_EV_SUCCESS        0x00

static int read_ps_event(uint8_t *event, uint16_t ocf)
{
	hci_event_hdr *eh;
	uint16_t opcode = htobs(cmd_opcode_pack(HCI_VENDOR_CMD_OGF, ocf));

	event++;

	eh = (void *)event;
	event += HCI_EVENT_HDR_SIZE;

	if (eh->evt == EVT_CMD_COMPLETE) {
		evt_cmd_complete *cc = (void *)event;

		event += EVT_CMD_COMPLETE_SIZE;

		if (cc->opcode == opcode && event[0] == HCI_EV_SUCCESS)
			return 0;
		else
			return -EILSEQ;
	}

	return -EILSEQ;
}

static int write_cmd(int fd, uint8_t *buffer, int len)
{
	uint8_t *event;
	int err;

	err = send_hci_cmd_sync(fd, buffer, len, &event);
	if (err < 0)
		return err;

	err = read_ps_event(event, HCI_PS_CMD_OCF);

	free(event);

	return err;
}

#define PS_WRITE           1
#define PS_RESET           2
#define WRITE_PATCH        8
#define ENABLE_PATCH       11

#define HCI_PS_CMD_HDR_LEN 7

#define PS_RESET_PARAM_LEN 6
#define HCI_MAX_CMD_SIZE   260
#define PS_RESET_CMD_LEN   (HCI_PS_CMD_HDR_LEN + PS_RESET_PARAM_LEN)

#define PS_ID_MASK         0xFF

/* Sends PS commands using vendor specficic HCI commands */
static int write_ps_cmd(int fd, uint8_t opcode, uint32_t ps_param)
{
	uint8_t cmd[HCI_MAX_CMD_SIZE];
	uint32_t i;

	switch (opcode) {
	case ENABLE_PATCH:
		load_hci_ps_hdr(cmd, opcode, 0, 0x00);

		if (write_cmd(fd, cmd, HCI_PS_CMD_HDR_LEN) < 0)
			return -EILSEQ;
		break;

	case PS_RESET:
		load_hci_ps_hdr(cmd, opcode, PS_RESET_PARAM_LEN, 0x00);

		cmd[7] = 0x00;
		cmd[PS_RESET_CMD_LEN - 2] = ps_param & PS_ID_MASK;
		cmd[PS_RESET_CMD_LEN - 1] = (ps_param >> 8) & PS_ID_MASK;

		if (write_cmd(fd, cmd, PS_RESET_CMD_LEN) < 0)
			return -EILSEQ;
		break;

	case PS_WRITE:
		for (i = 0; i < ps_param; i++) {
			load_hci_ps_hdr(cmd, opcode, ps_list[i].len,
							ps_list[i].id);

			memcpy(&cmd[HCI_PS_CMD_HDR_LEN], ps_list[i].data,
							ps_list[i].len);

			if (write_cmd(fd, cmd, ps_list[i].len +
						HCI_PS_CMD_HDR_LEN) < 0)
				return -EILSEQ;
		}
		break;
	}

	return 0;
}

#define __is_delim(ch) ((ch) == ':')
#define MAX_PREAMBLE_LEN 4

/* Parse PS entry preamble of format [X:X] for main type and subtype */
static int get_ps_type(char *ptr, int index, char *type, char *sub_type)
{
	int i;
	int delim = FALSE;

	if (index > MAX_PREAMBLE_LEN)
		return -EILSEQ;

	for (i = 1; i < index; i++) {
		if (__is_delim(ptr[i])) {
			delim = TRUE;
			continue;
		}

		if (isalpha(ptr[i])) {
			if (delim == FALSE)
				(*type) = toupper(ptr[i]);
			else
				(*sub_type) = toupper(ptr[i]);
		}
	}

	return 0;
}

#define ARRAY   'A'
#define STRING  'S'
#define DECIMAL 'D'
#define BINARY  'B'

#define PS_HEX           0
#define PS_DEC           1

static int get_input_format(char *buf, struct ps_entry_type *format)
{
	char *ptr = NULL;
	char type = '\0';
	char sub_type = '\0';

	format->type = PS_HEX;
	format->array = TRUE;

	if (strstr(buf, "[") != buf)
		return 0;

	ptr = strstr(buf, "]");
	if (!ptr)
		return -EILSEQ;

	if (get_ps_type(buf, ptr - buf, &type, &sub_type) < 0)
		return -EILSEQ;

	/* Check is data type is of array */
	if (type == ARRAY || sub_type == ARRAY)
		format->array = TRUE;

	if (type == STRING || sub_type == STRING)
		format->array = FALSE;

	if (type == DECIMAL || type == BINARY)
		format->type = PS_DEC;
	else
		format->type = PS_HEX;

	return 0;
}

#define UNDEFINED 0xFFFF

static unsigned int read_data_in_section(char *buf, struct ps_entry_type type)
{
	char *ptr = buf;

	if (!buf)
		return UNDEFINED;

	if (buf == strstr(buf, "[")) {
		ptr = strstr(buf, "]");
		if (!ptr)
			return UNDEFINED;

		ptr++;
	}

	if (type.type == PS_HEX && type.array != TRUE)
		return strtol(ptr, NULL, 16);

	return UNDEFINED;
}

struct tag_info {
	unsigned section;
	unsigned line_count;
	unsigned char_cnt;
	unsigned byte_count;
};

static inline int update_char_count(const char *buf)
{
	char *end_ptr;

	if (strstr(buf, "[") == buf) {
		end_ptr = strstr(buf, "]");
		if (!end_ptr)
			return 0;
		else
			return (end_ptr - buf) + 1;
	}

	return 0;
}

/* Read PS entries as string, convert and add to Hex array */
static void update_tag_data(struct ps_cfg_entry *tag,
				struct tag_info *info, const char *ptr)
{
	char buf[3];

	buf[2] = '\0';

	strncpy(buf, &ptr[info->char_cnt], 2);
	tag->data[info->byte_count] = strtol(buf, NULL, 16);
	info->char_cnt += 3;
	info->byte_count++;

	strncpy(buf, &ptr[info->char_cnt], 2);
	tag->data[info->byte_count] = strtol(buf, NULL, 16);
	info->char_cnt += 3;
	info->byte_count++;
}

#define PS_UNDEF   0
#define PS_ID      1
#define PS_LEN     2
#define PS_DATA    3

#define PS_MAX_LEN         500
#define LINE_SIZE_MAX      (PS_MAX_LEN * 2)
#define ENTRY_PER_LINE     16

#define __check_comment(buf) (((buf)[0] == '/') && ((buf)[1] == '/'))
#define __skip_space(str)      while (*(str) == ' ') ((str)++)

static int ath_parse_ps(FILE *stream)
{
	char buf[LINE_SIZE_MAX + 1];
	char *ptr;
	uint8_t tag_cnt = 0;
	int16_t byte_count = 0;
	struct ps_entry_type format;
	struct tag_info status = { 0, 0, 0, 0 };

	do {
		int read_count;
		struct ps_cfg_entry *tag;

		ptr = fgets(buf, LINE_SIZE_MAX, stream);
		if (!ptr)
			break;

		__skip_space(ptr);
		if (__check_comment(ptr))
			continue;

		/* Lines with a '#' will be followed by new PS entry */
		if (ptr == strstr(ptr, "#")) {
			if (status.section != PS_UNDEF) {
				return -EILSEQ;
			} else {
				status.section = PS_ID;
				continue;
			}
		}

		tag = &ps_list[tag_cnt];

		switch (status.section) {
		case PS_ID:
			if (get_input_format(ptr, &format) < 0)
				return -EILSEQ;

			tag->id = read_data_in_section(ptr, format);
			status.section = PS_LEN;
			break;

		case PS_LEN:
			if (get_input_format(ptr, &format) < 0)
				return -EILSEQ;

			byte_count = read_data_in_section(ptr, format);
			if (byte_count > PS_MAX_LEN)
				return -EILSEQ;

			tag->len = byte_count;
			tag->data = (uint8_t *)malloc(byte_count);

			status.section = PS_DATA;
			status.line_count = 0;
			break;

		case PS_DATA:
			if (status.line_count == 0)
				if (get_input_format(ptr, &format) < 0)
					return -EILSEQ;

			__skip_space(ptr);

			status.char_cnt = update_char_count(ptr);

			read_count = (byte_count > ENTRY_PER_LINE) ?
					ENTRY_PER_LINE : byte_count;

			if (format.type == PS_HEX && format.array == TRUE) {
				while (read_count > 0) {
					update_tag_data(tag, &status, ptr);
					read_count -= 2;
				}

				if (byte_count > ENTRY_PER_LINE)
					byte_count -= ENTRY_PER_LINE;
				else
					byte_count = 0;
			}

			status.line_count++;

			if (byte_count == 0)
				memset(&status, 0x00, sizeof(struct tag_info));

			if (status.section == PS_UNDEF)
				tag_cnt++;

			if (tag_cnt == MAX_TAGS)
				return -EILSEQ;
			break;
		}
	} while (ptr);

	return tag_cnt;
}

#define MAX_PATCH_CMD 244
struct patch_entry {
	int16_t len;
	uint8_t data[MAX_PATCH_CMD];
};

#define SET_PATCH_RAM_ID	0x0D
#define SET_PATCH_RAM_CMD_SIZE	11
#define ADDRESS_LEN		4
static int set_patch_ram(int dev, char *patch_loc, int len)
{
	int err;
	uint8_t cmd[20];
	int i, j;
	char loc_byte[3];
	uint8_t *event;
	uint8_t *loc_ptr = &cmd[7];

	if (!patch_loc)
		return -1;

	loc_byte[2] = '\0';

	load_hci_ps_hdr(cmd, SET_PATCH_RAM_ID, ADDRESS_LEN, 0);

	for (i = 0, j = 3; i < 4; i++, j--) {
		loc_byte[0] = patch_loc[0];
		loc_byte[1] = patch_loc[1];
		loc_ptr[j] = strtol(loc_byte, NULL, 16);
		patch_loc += 2;
	}

	err = send_hci_cmd_sync(dev, cmd, SET_PATCH_RAM_CMD_SIZE, &event);
	if (err < 0)
		return err;

	err = read_ps_event(event, HCI_PS_CMD_OCF);

	free(event);

	return err;
}

#define PATCH_LOC_KEY    "DA:"
#define PATCH_LOC_STRING_LEN    (8 + 237)
static int ps_patch_download(int fd, FILE *stream)
{
	char byte[3];
	char ptr[MAX_PATCH_CMD + 1];
	int byte_cnt;
	int patch_count = 0;
	char patch_loc[PATCH_LOC_STRING_LEN + 1];

	byte[2] = '\0';

	while (fgets(ptr, MAX_PATCH_CMD, stream)) {
		if (strlen(ptr) <= 1)
			continue;
		else if (strstr(ptr, PATCH_LOC_KEY) == ptr) {
			strncpy(patch_loc, &ptr[sizeof(PATCH_LOC_KEY) - 1],
							PATCH_LOC_STRING_LEN);
			if (set_patch_ram(fd, patch_loc, sizeof(patch_loc)) < 0)
				return -1;
		} else if (isxdigit(ptr[0]))
			break;
		else
			return -1;
	}

	byte_cnt = strtol(ptr, NULL, 16);

	while (byte_cnt > 0) {
		int i;
		uint8_t cmd[HCI_MAX_CMD_SIZE];
		struct patch_entry patch;

		if (byte_cnt > MAX_PATCH_CMD)
			patch.len = MAX_PATCH_CMD;
		else
			patch.len = byte_cnt;

		for (i = 0; i < patch.len; i++) {
			if (!fgets(byte, 3, stream))
				return -1;

			patch.data[i] = strtoul(byte, NULL, 16);
		}

		load_hci_ps_hdr(cmd, WRITE_PATCH, patch.len, patch_count);
		memcpy(&cmd[HCI_PS_CMD_HDR_LEN], patch.data, patch.len);

		if (write_cmd(fd, cmd, patch.len + HCI_PS_CMD_HDR_LEN) < 0)
			return -1;

		patch_count++;
		byte_cnt = byte_cnt - MAX_PATCH_CMD;
	}

	if (write_ps_cmd(fd, ENABLE_PATCH, 0) < 0)
		return -1;

	return patch_count;
}

#define PS_RAM_SIZE 2048

static int ps_config_download(int fd, int tag_count)
{
	if (write_ps_cmd(fd, PS_RESET, PS_RAM_SIZE) < 0)
		return -1;

	if (tag_count > 0)
		if (write_ps_cmd(fd, PS_WRITE, tag_count) < 0)
			return -1;
	return 0;
}

#define PS_ASIC_FILE_PREFIX		"PS_ASIC"
#define PS_FPGA_FILE_PREFIX		"PS_FPGA"

static void get_ps_file_name(uint32_t devtype, uint32_t rom_version, char *path,
								char *region)
{
	char *file_prefix;
	struct stat st;

	if (devtype == 0xdeadc0de)
		file_prefix = PS_ASIC_FILE_PREFIX;
	else
		file_prefix = PS_FPGA_FILE_PREFIX;

	if (!region)
		goto default_ps_file;

	snprintf(path, MAXPATHLEN, "%s%x/%s-%s.pst", FW_PATH, rom_version,
			file_prefix, region);
	if (stat(path, &st) == 0)
		return;

	perror("PS file with region code not exist, use default PS file\n");

default_ps_file:
	snprintf(path, MAXPATHLEN, "%s%x/%s.pst", FW_PATH, rom_version,
			file_prefix);
}

#define PATCH_FILE        "RamPatch.txt"
#define FPGA_ROM_VERSION  0x99999999
#define ROM_DEV_TYPE      0xdeadc0de

static void get_patch_file_name(uint32_t dev_type, uint32_t rom_version,
				uint32_t build_version, char *path)
{
	if (rom_version == FPGA_ROM_VERSION && dev_type != ROM_DEV_TYPE &&
					dev_type != 0 && build_version == 1)
		path[0] = '\0';
	else
		snprintf(path, MAXPATHLEN, "%s%x/%s",
				FW_PATH, rom_version, PATCH_FILE);
}

#define VERIFY_CRC   9
#define PS_REGION    1
#define PATCH_REGION 2

static int get_ath3k_crc(int dev)
{
	uint8_t cmd[7];
	uint8_t *event;
	int err;

	load_hci_ps_hdr(cmd, VERIFY_CRC, 0, PS_REGION | PATCH_REGION);

	err = send_hci_cmd_sync(dev, cmd, sizeof(cmd), &event);
	if (err < 0)
		return err;
	/* Send error code if CRC check patched */
	if (read_ps_event(event, HCI_PS_CMD_OCF) >= 0)
		err = -EILSEQ;

	free(event);

	return err;
}

#define DEV_REGISTER      0x4FFC
#define GET_DEV_TYPE_OCF  0x05

static int get_device_type(int dev, uint32_t *code)
{
	uint8_t cmd[8];
	uint8_t *event;
	uint32_t reg;
	int err;
	uint8_t *ptr = cmd;
	hci_command_hdr *ch = (void *)cmd;

	ch->opcode = htobs(cmd_opcode_pack(HCI_VENDOR_CMD_OGF,
						GET_DEV_TYPE_OCF));
	ch->plen = 5;
	ptr += HCI_COMMAND_HDR_SIZE;

	ptr[0] = (uint8_t)DEV_REGISTER;
	ptr[1] = (uint8_t)DEV_REGISTER >> 8;
	ptr[2] = (uint8_t)DEV_REGISTER >> 16;
	ptr[3] = (uint8_t)DEV_REGISTER >> 24;
	ptr[4] = 0x04;

	err = send_hci_cmd_sync(dev, cmd, sizeof(cmd), &event);
	if (err < 0)
		return err;

	err = read_ps_event(event, GET_DEV_TYPE_OCF);
	if (err < 0)
		goto cleanup;

	reg = event[10];
	reg = (reg << 8) | event[9];
	reg = (reg << 8) | event[8];
	reg = (reg << 8) | event[7];
	*code = reg;

cleanup:
	free(event);

	return err;
}

#define GET_VERSION_OCF 0x1E

static int read_ath3k_version(int pConfig, uint32_t *rom_version,
					uint32_t *build_version)
{
	uint8_t cmd[3];
	uint8_t *event;
	int err;
	int status;
	hci_command_hdr *ch = (void *)cmd;

	ch->opcode = htobs(cmd_opcode_pack(HCI_VENDOR_CMD_OGF,
						GET_VERSION_OCF));
	ch->plen = 0;

	err = send_hci_cmd_sync(pConfig, cmd, sizeof(cmd), &event);
	if (err < 0)
		return err;

	err = read_ps_event(event, GET_VERSION_OCF);
	if (err < 0)
		goto cleanup;

	status = event[10];
	status = (status << 8) | event[9];
	status = (status << 8) | event[8];
	status = (status << 8) | event[7];
	*rom_version = status;

	status = event[14];
	status = (status << 8) | event[13];
	status = (status << 8) | event[12];
	status = (status << 8) | event[11];
	*build_version = status;

cleanup:
	free(event);

	return err;
}

static void convert_bdaddr(char *str_bdaddr, char *bdaddr)
{
	char bdbyte[3];
	char *str_byte = str_bdaddr;
	int i, j;
	int colon_present = 0;

	if (strstr(str_bdaddr, ":"))
		colon_present = 1;

	bdbyte[2] = '\0';

	/* Reverse the BDADDR to LSB first */
	for (i = 0, j = 5; i < 6; i++, j--) {
		bdbyte[0] = str_byte[0];
		bdbyte[1] = str_byte[1];
		bdaddr[j] = strtol(bdbyte, NULL, 16);

		if (colon_present == 1)
			str_byte += 3;
		else
			str_byte += 2;
	}
}

static int write_bdaddr(int pConfig, char *bdaddr)
{
	uint8_t *event;
	int err;
	uint8_t cmd[13];
	uint8_t *ptr = cmd;
	hci_command_hdr *ch = (void *)cmd;

	memset(cmd, 0, sizeof(cmd));

	ch->opcode = htobs(cmd_opcode_pack(HCI_VENDOR_CMD_OGF,
						HCI_PS_CMD_OCF));
	ch->plen = 10;
	ptr += HCI_COMMAND_HDR_SIZE;

	ptr[0] = 0x01;
	ptr[1] = 0x01;
	ptr[2] = 0x00;
	ptr[3] = 0x06;

	convert_bdaddr(bdaddr, (char *)&ptr[4]);

	err = send_hci_cmd_sync(pConfig, cmd, sizeof(cmd), &event);
	if (err < 0)
		return err;

	err = read_ps_event(event, HCI_PS_CMD_OCF);

	free(event);

	return err;
}

#define BDADDR_FILE "ar3kbdaddr.pst"

static void write_bdaddr_from_file(int rom_version, int fd)
{
	FILE *stream;
	char bdaddr[PATH_MAX];
	char bdaddr_file[PATH_MAX];

	snprintf(bdaddr_file, MAXPATHLEN, "%s%x/%s",
			FW_PATH, rom_version, BDADDR_FILE);

	stream = fopen(bdaddr_file, "r");
	if (!stream)
		return;

	if (fgets(bdaddr, PATH_MAX - 1, stream))
		write_bdaddr(fd, bdaddr);

	fclose(stream);
}

static int ath_ps_download(int fd)
{
	int err = 0;
	int tag_count;
	int patch_count = 0;
	uint32_t rom_version = 0;
	uint32_t build_version = 0;
	uint32_t dev_type = 0;
	char patch_file[PATH_MAX];
	char ps_file[PATH_MAX];
	FILE *stream;

	/*
	 * Verify firmware version. depending on it select the PS
	 * config file to download.
	 */
	if (get_device_type(fd, &dev_type) < 0) {
		err = -EILSEQ;
		goto download_cmplete;
	}

	if (read_ath3k_version(fd, &rom_version, &build_version) < 0) {
		err = -EILSEQ;
		goto download_cmplete;
	}

	/* Do not download configuration if CRC passes */
	if (get_ath3k_crc(fd) < 0) {
		err = 0;
		goto download_cmplete;
	}

	get_ps_file_name(dev_type, rom_version, ps_file, NULL);
	get_patch_file_name(dev_type, rom_version, build_version, patch_file);

	stream = fopen(ps_file, "r");
	if (!stream) {
		perror("firmware file open error\n");
		err = -EILSEQ;
		goto download_cmplete;
	}
	tag_count = ath_parse_ps(stream);

	fclose(stream);

	if (tag_count < 0) {
		err = -EILSEQ;
		goto download_cmplete;
	}

	stream = fopen(patch_file, "r");
	if(stream) {
		patch_count = ps_patch_download(fd, stream);
		fclose(stream);

		if (patch_count < 0) {
			err = -EILSEQ;
			goto download_cmplete;
		}
	}

	err = ps_config_download(fd, tag_count);

download_cmplete:
	if (!err)
		write_bdaddr_from_file(rom_version, fd);

	return err;
}

#define HCI_SLEEP_CMD_OCF     0x04

/*
 * Atheros AR300x specific initialization post callback
 */
int ath3k_post(int fd, int pm)
{
	int dev_id, dd;
	struct timespec tm = { 0, 50000 };

	sleep(1);

	dev_id = ioctl(fd, HCIUARTGETDEVICE, 0);
	if (dev_id < 0) {
		perror("cannot get device id");
		return dev_id;
	}

	dd = hci_open_dev(dev_id);
	if (dd < 0) {
		perror("HCI device open failed");
		return dd;
	}

	if (ioctl(dd, HCIDEVUP, dev_id) < 0 && errno != EALREADY) {
		perror("hci down:Power management Disabled");
		hci_close_dev(dd);
		return -1;
	}

	/* send vendor specific command with Sleep feature Enabled */
	if (hci_send_cmd(dd, OGF_VENDOR_CMD, HCI_SLEEP_CMD_OCF, 1, &pm) < 0)
		perror("PM command failed, power management Disabled");

	nanosleep(&tm, NULL);
	hci_close_dev(dd);

	return 0;
}

#define HCI_VENDOR_CMD_OGF    0x3F
#define HCI_PS_CMD_OCF        0x0B
#define HCI_CHG_BAUD_CMD_OCF  0x0C

#define WRITE_BDADDR_CMD_LEN 14
#define WRITE_BAUD_CMD_LEN   6
#define MAX_CMD_LEN          WRITE_BDADDR_CMD_LEN

static int set_cntrlr_baud(int fd, int speed)
{
	int baud;
	struct timespec tm = { 0, 500000 };
	unsigned char cmd[MAX_CMD_LEN], rsp[HCI_MAX_EVENT_SIZE];
	unsigned char *ptr = cmd + 1;
	hci_command_hdr *ch = (void *)ptr;

	cmd[0] = HCI_COMMAND_PKT;

	/* set controller baud rate to user specified value */
	ptr = cmd + 1;
	ch->opcode = htobs(cmd_opcode_pack(HCI_VENDOR_CMD_OGF,
						HCI_CHG_BAUD_CMD_OCF));
	ch->plen = 2;
	ptr += HCI_COMMAND_HDR_SIZE;

	baud = speed/100;
	ptr[0] = (char)baud;
	ptr[1] = (char)(baud >> 8);

	if (write(fd, cmd, WRITE_BAUD_CMD_LEN) != WRITE_BAUD_CMD_LEN) {
		perror("Failed to write change baud rate command");
		return -ETIMEDOUT;
	}

	nanosleep(&tm, NULL);

	if (read_hci_event(fd, rsp, sizeof(rsp)) < 0)
		return -ETIMEDOUT;

	return 0;
}

/*
 * Atheros AR300x specific initialization and configuration file
 * download
 */
int ath3k_init(int fd, int speed, int init_speed, char *bdaddr,
						struct termios *ti)
{
	int r;
	int err = 0;
	struct timespec tm = { 0, 500000 };
	unsigned char cmd[MAX_CMD_LEN], rsp[HCI_MAX_EVENT_SIZE];
	unsigned char *ptr = cmd + 1;
	hci_command_hdr *ch = (void *)ptr;

	cmd[0] = HCI_COMMAND_PKT;

	/* set both controller and host baud rate to maximum possible value */
	err = set_cntrlr_baud(fd, speed);
	if (err < 0)
		return err;

	err = set_speed(fd, ti, speed);
	if (err < 0) {
		perror("Can't set required baud rate");
		return err;
	}

	/* Download PS and patch */
	r = ath_ps_download(fd);
	if (r < 0) {
		perror("Failed to Download configuration");
		err = -ETIMEDOUT;
		goto failed;
	}

	/* Write BDADDR */
	if (bdaddr) {
		ch->opcode = htobs(cmd_opcode_pack(HCI_VENDOR_CMD_OGF,
							HCI_PS_CMD_OCF));
		ch->plen = 10;
		ptr += HCI_COMMAND_HDR_SIZE;

		ptr[0] = 0x01;
		ptr[1] = 0x01;
		ptr[2] = 0x00;
		ptr[3] = 0x06;
		str2ba(bdaddr, (bdaddr_t *)(ptr + 4));

		if (write(fd, cmd, WRITE_BDADDR_CMD_LEN) !=
					WRITE_BDADDR_CMD_LEN) {
			perror("Failed to write BD_ADDR command\n");
			err = -ETIMEDOUT;
			goto failed;
		}

		if (read_hci_event(fd, rsp, sizeof(rsp)) < 0) {
			perror("Failed to set BD_ADDR\n");
			err = -ETIMEDOUT;
			goto failed;
		}
	}

	/* Send HCI Reset */
	cmd[1] = 0x03;
	cmd[2] = 0x0C;
	cmd[3] = 0x00;

	r = write(fd, cmd, 4);
	if (r != 4) {
		err = -ETIMEDOUT;
		goto failed;
	}

	nanosleep(&tm, NULL);
	if (read_hci_event(fd, rsp, sizeof(rsp)) < 0) {
		err = -ETIMEDOUT;
		goto failed;
	}

	err = set_cntrlr_baud(fd, speed);
	if (err < 0)
		return err;

failed:
	if (err < 0) {
		set_cntrlr_baud(fd, init_speed);
		set_speed(fd, ti, init_speed);
	}

	return err;
}
