/* Copyright 2018 The Chromium OS Authors. All rights reserved.
 * Use of this source code is governed by a BSD-style license that can be
 * found in the LICENSE file.
 */

#include "atomic.h"
#include "compile_time_macros.h"
#include "console.h"
#include "hbm.h"
#include "heci_client.h"
#include "ipc_heci.h"
#include "system_state.h"
#include "task.h"
#include "timer.h"
#include "util.h"

#define CPUTS(outstr) cputs(CC_LPC, outstr)
#define CPRINTS(format, args...) cprints(CC_LPC, format, ## args)
#define CPRINTF(format, args...) cprintf(CC_LPC, format, ## args)

struct heci_header {
	uint8_t fw_addr;
	uint8_t host_addr;
	uint16_t length; /* [8:0] length, [14:9] reserved, [15] msg_complete */
} __packed;
#define HECI_MSG_CMPL_SHIFT		15
#define HECI_MSG_LENGTH_MASK		0x01FF
#define HECI_MSG_LENGTH(length)		((length) & HECI_MSG_LENGTH_MASK)
#define HECI_MSG_IS_COMPLETED(length) \
				(!!((length) & (0x01 << HECI_MSG_CMPL_SHIFT)))

BUILD_ASSERT(HECI_IPC_PAYLOAD_SIZE ==
	     (IPC_MAX_PAYLOAD_SIZE - sizeof(struct heci_header)));

struct heci_msg {
	struct heci_header hdr;
	uint8_t payload[HECI_IPC_PAYLOAD_SIZE];
} __packed;

/* HECI addresses */
#define HECI_HBM_ADDRESS		0	/* HECI Bus Message */
#define HECI_DYN_CLIENT_ADDR_START	0x20	/* Dynamic client start addr */

/* A fw client has the same value for both handle and fw address */
#define TO_FW_ADDR(handle)		((uintptr_t)(handle))
#define TO_HECI_HANDLE(fw_addr)		((heci_handle_t)(uintptr_t)(fw_addr))
/* convert client fw address to client context index */
#define TO_CLIENT_CTX_IDX(fw_addr)	((fw_addr) - HECI_DYN_CLIENT_ADDR_START)

/* should be less than HECI_INVALID_HANDLE - 1 */
BUILD_ASSERT(HECI_MAX_NUM_OF_CLIENTS < 0x0FE);

struct heci_client_connect {
	uint8_t is_connected;	/* client is connected to host */
	uint8_t host_addr;	/* connected host address */

	/* receiving message */
	uint8_t ignore_rx_msg;
	uint8_t	rx_msg[HECI_MAX_MSG_SIZE];
	size_t  rx_msg_length;

	uint32_t flow_ctrl_creds; /* flow control */
	struct mutex lock; /* protects against 2 writers */
	struct mutex cred_lock; /* protects flow ctrl */
	int waiting_task;
};

struct heci_client_context {
	const struct heci_client *client;
	void *data;	/* client specific data */

	struct heci_client_connect connect; /* connection context */
	struct ss_subsys_device ss_device; /* system state receiver device */
};

struct heci_bus_context {
	ipc_handle_t ipc_handle; /* ipc handle for heci protocol */

	int num_of_clients;
	struct heci_client_context client_ctxs[HECI_MAX_NUM_OF_CLIENTS];
};

/* declare heci bus */
struct heci_bus_context heci_bus_ctx = {
		.ipc_handle = IPC_INVALID_HANDLE,
};

static inline struct heci_client_context *
heci_get_client_context(const uint8_t fw_addr)
{
	return &heci_bus_ctx.client_ctxs[TO_CLIENT_CTX_IDX(fw_addr)];
}

static inline struct heci_client_connect *
heci_get_client_connect(const uint8_t fw_addr)
{
	struct heci_client_context *cli_ctx = heci_get_client_context(fw_addr);
	return &cli_ctx->connect;
}

static inline int heci_is_client_connected(const uint8_t fw_addr)
{
	struct heci_client_context *cli_ctx = heci_get_client_context(fw_addr);
	return cli_ctx->connect.is_connected;
}

static inline int heci_is_valid_client_addr(const uint8_t fw_addr)
{
	uint8_t cli_idx = TO_CLIENT_CTX_IDX(fw_addr);

	return cli_idx < heci_bus_ctx.num_of_clients;
}

static inline int heci_is_valid_handle(const heci_handle_t handle)
{
	return heci_is_valid_client_addr((uintptr_t)(handle));
}

/* find heci device that contains this system state device in it */
#define ss_device_to_heci_client_context(ss_dev) \
	((struct heci_client_context *)((void *)(ss_dev) - \
	(void *)(&(((struct heci_client_context *)0)->ss_device))))
#define client_context_to_handle(cli_ctx) \
	((heci_handle_t)((uint32_t)((cli_ctx) - &heci_bus_ctx.client_ctxs[0]) \
	/ sizeof(heci_bus_ctx.client_ctxs[0]) + 1))

/*
 * each heci device registered as system state device which gets
 * system state(e.g. suspend/resume, portrait/landscape) events
 * through system state subsystem from host
 */
static int heci_client_suspend(struct ss_subsys_device *ss_device)
{
	struct heci_client_context *cli_ctx =
			ss_device_to_heci_client_context(ss_device);
	heci_handle_t handle = client_context_to_handle(cli_ctx);

	if (cli_ctx->client->cbs->suspend)
		cli_ctx->client->cbs->suspend(handle);

	return EC_SUCCESS;
}

static int heci_client_resume(struct ss_subsys_device *ss_device)
{
	struct heci_client_context *cli_ctx =
			ss_device_to_heci_client_context(ss_device);
	heci_handle_t handle = client_context_to_handle(cli_ctx);

	if (cli_ctx->client->cbs->resume)
		cli_ctx->client->cbs->resume(handle);

	return EC_SUCCESS;
}

struct system_state_callbacks heci_ss_cbs = {
	.suspend = heci_client_suspend,
	.resume = heci_client_resume,
};

/*
 * This function should be called only by HECI_CLIENT_ENTRY()
 */
heci_handle_t heci_register_client(const struct heci_client *client)
{
	int ret;
	heci_handle_t handle;
	struct heci_client_context *cli_ctx;

	if (client == NULL || client->cbs == NULL)
		return HECI_INVALID_HANDLE;

	/*
	 * we don't need mutex here since this function is called by
	 * entry function which is serialized among heci clients.
	 */
	if (heci_bus_ctx.num_of_clients >= HECI_MAX_NUM_OF_CLIENTS)
		return HECI_INVALID_HANDLE;

	/* we only support 1 connection */
	if (client->max_n_of_connections > 1)
		return HECI_INVALID_HANDLE;

	if (client->max_msg_size > HECI_MAX_MSG_SIZE)
		return HECI_INVALID_HANDLE;

	/* create handle with the same value of fw address */
	handle = (heci_handle_t)(heci_bus_ctx.num_of_clients +
				 HECI_DYN_CLIENT_ADDR_START);
	cli_ctx = &heci_bus_ctx.client_ctxs[heci_bus_ctx.num_of_clients++];
	cli_ctx->client = client;

	if (client->cbs->initialize) {
		ret = client->cbs->initialize(handle);
		if (ret) {
			heci_bus_ctx.num_of_clients--;
			return HECI_INVALID_HANDLE;
		}
	}

	if (client->cbs->suspend || client->cbs->resume) {
		cli_ctx->ss_device.cbs = &heci_ss_cbs;
		ss_subsys_register_client(&cli_ctx->ss_device);
	}

	return handle;
}

static void heci_build_hbm_header(struct heci_header *hdr, uint32_t length)
{
	hdr->fw_addr = HECI_HBM_ADDRESS;
	hdr->host_addr = HECI_HBM_ADDRESS;
	hdr->length = length;
	/* payload of hbm is less than IPC payload */
	hdr->length |= (uint16_t)1 << HECI_MSG_CMPL_SHIFT;
}

static void heci_build_fixed_client_header(struct heci_header *hdr,
					   const uint8_t fw_addr,
					   const uint32_t length)
{
	hdr->fw_addr = fw_addr;
	hdr->host_addr = 0;
	hdr->length = length;
	/* Fixed client payload < IPC payload */
	hdr->length |= (uint16_t)1 << HECI_MSG_CMPL_SHIFT;
}

static int heci_send_heci_msg_timestamp(struct heci_msg *msg,
					uint32_t *timestamp)
{
	int length, written;

	if (heci_bus_ctx.ipc_handle == IPC_INVALID_HANDLE)
		return -1;

	length = sizeof(msg->hdr) + HECI_MSG_LENGTH(msg->hdr.length);
	written = ipc_write_timestamp(heci_bus_ctx.ipc_handle, msg, length,
				      timestamp);

	if (written != length) {
		CPRINTF("%s error : len = %d err = %d\n", __func__,
			(int)length, written);
		return -EC_ERROR_UNKNOWN;
	}

	return EC_SUCCESS;
}

static int heci_send_heci_msg(struct heci_msg *msg)
{
	return heci_send_heci_msg_timestamp(msg, NULL);
}

int heci_set_client_data(const heci_handle_t handle, void *data)
{
	struct heci_client_context *cli_ctx;
	const uint8_t fw_addr = TO_FW_ADDR(handle);

	if (!heci_is_valid_handle(handle))
		return -EC_ERROR_INVAL;

	cli_ctx = heci_get_client_context(fw_addr);
	cli_ctx->data = data;

	return EC_SUCCESS;
}

void *heci_get_client_data(const heci_handle_t handle)
{
	struct heci_client_context *cli_ctx;
	const uint8_t fw_addr = TO_FW_ADDR(handle);

	if (!heci_is_valid_handle(handle))
		return NULL;

	cli_ctx = heci_get_client_context(fw_addr);
	return cli_ctx->data;
}

/*
 * Waits for flow control credit that allows TX transactions
 *
 * Returns true if credit was acquired, otherwise false
 */
static int wait_for_flow_ctrl_cred(struct heci_client_connect *connect)
{
	int need_to_wait;

	do {
		mutex_lock(&connect->cred_lock);
		need_to_wait = !connect->flow_ctrl_creds;
		if (need_to_wait) {
			connect->waiting_task = task_get_current();
		} else {
			connect->flow_ctrl_creds = 0;
			connect->waiting_task = 0;
		}
		mutex_unlock(&connect->cred_lock);
		if (need_to_wait) {
			/*
			 * A second is more than enough, otherwise if will
			 * probably never happen.
			 */
			int ev = task_wait_event_mask(TASK_EVENT_IPC_READY,
						      SECOND);
			if (ev & TASK_EVENT_TIMER) {
				/* Return false, not able to get credit */
				return 0;
			}
		}
	} while (need_to_wait);

	/* We successfully got flow control credit */
	return 1;
}

int heci_send_msg_timestamp(const heci_handle_t handle, uint8_t *buf,
			    const size_t buf_size, uint32_t *timestamp)
{
	int buf_offset = 0, ret = 0, remain, payload_size;
	struct heci_client_connect *connect;
	struct heci_msg msg;
	const uint8_t fw_addr = TO_FW_ADDR(handle);

	if (!heci_is_valid_handle(handle))
		return -EC_ERROR_INVAL;

	if (buf_size > HECI_MAX_MSG_SIZE)
		return -EC_ERROR_OVERFLOW;

	connect = heci_get_client_connect(fw_addr);
	mutex_lock(&connect->lock);

	if (!heci_is_client_connected(fw_addr)) {
		ret = -HECI_ERR_CLIENT_IS_NOT_CONNECTED;
		goto err_locked;
	}

	if (!wait_for_flow_ctrl_cred(connect)) {
		CPRINTF("no cred\n");
		ret = -HECI_ERR_NO_CRED_FROM_CLIENT_IN_HOST;
		goto err_locked;
	}

	msg.hdr.fw_addr = fw_addr;
	msg.hdr.host_addr = connect->host_addr;

	remain = buf_size;
	while (remain) {
		if (remain > HECI_IPC_PAYLOAD_SIZE) {
			msg.hdr.length = HECI_IPC_PAYLOAD_SIZE;
			payload_size = HECI_IPC_PAYLOAD_SIZE;
		} else {
			msg.hdr.length = remain;
			/* set as last heci msg */
			msg.hdr.length |= (uint16_t)1 << HECI_MSG_CMPL_SHIFT;
			payload_size = remain;
		}

		memcpy(msg.payload, buf + buf_offset, payload_size);

		heci_send_heci_msg_timestamp(&msg, timestamp);

		remain -= payload_size;
		buf_offset += payload_size;
	}
	mutex_unlock(&connect->lock);

	return buf_size;

err_locked:
	mutex_unlock(&connect->lock);

	return ret;
}

int heci_send_msg(const heci_handle_t handle, uint8_t *buf,
		  const size_t buf_size)
{
	return heci_send_msg_timestamp(handle, buf, buf_size, NULL);
}


int heci_send_msgs(const heci_handle_t handle,
		   const struct heci_msg_list *msg_list)
{
	struct heci_msg_item *cur_item;
	int total_size = 0;
	int i, msg_cur_pos, buf_size, copy_size, msg_sent;
	struct heci_client_connect *connect;
	struct heci_msg msg;
	const uint8_t fw_addr = TO_FW_ADDR(handle);

	if (!heci_is_valid_handle(handle))
		return -EC_ERROR_INVAL;

	for (i = 0; i < msg_list->num_of_items; i++) {
		if (!msg_list->items[i]->size || !msg_list->items[i]->buf)
			return -EC_ERROR_INVAL;

		total_size += msg_list->items[i]->size;
	}

	if (total_size > HECI_MAX_MSG_SIZE)
		return -EC_ERROR_OVERFLOW;

	if (msg_list->num_of_items > HECI_MAX_MSGS)
		return -HECI_ERR_TOO_MANY_MSG_ITEMS;

	connect = heci_get_client_connect(fw_addr);
	mutex_lock(&connect->lock);

	if (!heci_is_client_connected(fw_addr)) {
		total_size = -HECI_ERR_CLIENT_IS_NOT_CONNECTED;
		goto err_locked;
	}

	if (!wait_for_flow_ctrl_cred(connect)) {
		CPRINTF("no cred\n");
		total_size = -HECI_ERR_NO_CRED_FROM_CLIENT_IN_HOST;
		goto err_locked;
	}

	msg.hdr.fw_addr = fw_addr;
	msg.hdr.host_addr = connect->host_addr;

	i = 1;
	msg_cur_pos = 0;
	buf_size = 0;
	cur_item = msg_list->items[0];
	msg_sent = 0;
	while (1) {
		/* get next item if current item is consumed */
		if (msg_cur_pos == cur_item->size) {
			/*
			 * break if no more item.
			 * if "msg" contains data to be sent
			 * it will be sent after break.
			 */
			if (i == msg_list->num_of_items)
				break;

			/* get next item and reset msg_cur_pos */
			cur_item = msg_list->items[i++];
			msg_cur_pos = 0;
		}

		/* send data in ipc buf if it's completely filled */
		if (buf_size == HECI_IPC_PAYLOAD_SIZE) {
			msg.hdr.length = buf_size;
			msg_sent += buf_size;

			/* no leftovers, send the last msg here */
			if (msg_sent == total_size) {
				msg.hdr.length |=
					(uint16_t)1 << HECI_MSG_CMPL_SHIFT;
			}

			heci_send_heci_msg(&msg);
			buf_size = 0;
		}

		/* fill ipc msg buffer */
		if (cur_item->size - msg_cur_pos >
		    HECI_IPC_PAYLOAD_SIZE - buf_size) {
			copy_size = HECI_IPC_PAYLOAD_SIZE - buf_size;
		} else {
			copy_size = cur_item->size - msg_cur_pos;
		}

		memcpy(msg.payload + buf_size, cur_item->buf + msg_cur_pos,
		       copy_size);

		msg_cur_pos += copy_size;
		buf_size += copy_size;
	}

	/* leftovers ? send last msg */
	if (buf_size != 0) {
		msg.hdr.length = buf_size;
		msg.hdr.length |= (uint16_t)1 << HECI_MSG_CMPL_SHIFT;

		heci_send_heci_msg(&msg);
	}

err_locked:
	mutex_unlock(&connect->lock);

	return total_size;

}

/* For now, we only support fixed client payload size < IPC payload size */
int heci_send_fixed_client_msg(const uint8_t fw_addr, uint8_t *buf,
			       const size_t buf_size)
{
	struct heci_msg msg;

	heci_build_fixed_client_header(&msg.hdr, fw_addr, buf_size);

	memcpy(msg.payload, buf, buf_size);

	heci_send_heci_msg(&msg);

	return EC_SUCCESS;
}

static int handle_version_req(struct hbm_version_req *ver_req)
{
	struct hbm_version_res *ver_res;
	struct heci_msg heci_msg;
	struct hbm_i2h *i2h;

	heci_build_hbm_header(&heci_msg.hdr,
			      sizeof(i2h->cmd) + sizeof(*ver_res));

	i2h = (struct hbm_i2h *)heci_msg.payload;
	i2h->cmd = HECI_BUS_MSG_VERSION_RESP;
	ver_res = (struct hbm_version_res *)&i2h->data;

	memset(ver_res, 0, sizeof(*ver_res));

	ver_res->version.major = HBM_MAJOR_VERSION;
	ver_res->version.minor = HBM_MINOR_VERSION;
	if (ver_req->version.major == HBM_MAJOR_VERSION &&
	    ver_req->version.minor == HBM_MINOR_VERSION) {
		ver_res->supported = 1;
	} else {
		ver_res->supported = 0;
	}

	heci_send_heci_msg(&heci_msg);

	return EC_SUCCESS;
}

#define BITS_PER_BYTE			8
/* get number of bits for one element of "valid_addresses" array */
#define	BITS_PER_ELEMENT \
	(sizeof(((struct hbm_enum_res *)0)->valid_addresses[0]) * BITS_PER_BYTE)

static int handle_enum_req(struct hbm_enum_req *enum_req)
{
	struct hbm_enum_res *enum_res;
	struct heci_msg heci_msg;
	struct hbm_i2h *i2h;
	int i;

	heci_build_hbm_header(&heci_msg.hdr,
			      sizeof(i2h->cmd) + sizeof(*enum_res));

	i2h = (struct hbm_i2h *)heci_msg.payload;
	i2h->cmd = HECI_BUS_MSG_HOST_ENUM_RESP;
	enum_res = (struct hbm_enum_res *)&i2h->data;

	memset(enum_res, 0, sizeof(*enum_res));

	/*
	 * fw address 0 is reserved for HECI Bus Message
	 * fw address 1 ~ 0x1f are reserved for fixed clients
	 * fw address 0x20 ~ 0xFF is for dynamic clients
	 * bit-0 set -> fw address "0", bit-1 set -> fw address "1"
	 */
	for (i = HECI_DYN_CLIENT_ADDR_START;
	     i < heci_bus_ctx.num_of_clients + HECI_DYN_CLIENT_ADDR_START;
	     i++) {
		enum_res->valid_addresses[i / BITS_PER_ELEMENT] |=
			1 << (i & (BITS_PER_ELEMENT - 1));
	}

	heci_send_heci_msg(&heci_msg);

	return EC_SUCCESS;
}

static int handle_client_prop_req(struct hbm_client_prop_req *client_prop_req)
{
	struct hbm_client_prop_res *client_prop_res;
	struct heci_msg heci_msg;
	struct hbm_i2h *i2h;
	struct heci_client_context *client_ctx;
	const struct heci_client *client;

	heci_build_hbm_header(&heci_msg.hdr,
			      sizeof(i2h->cmd) + sizeof(*client_prop_res));

	i2h = (struct hbm_i2h *)heci_msg.payload;
	i2h->cmd = HECI_BUS_MSG_HOST_CLIENT_PROP_RESP;
	client_prop_res = (struct hbm_client_prop_res *)&i2h->data;

	memset(client_prop_res, 0, sizeof(*client_prop_res));

	client_prop_res->address = client_prop_req->address;
	if (!heci_is_valid_client_addr(client_prop_req->address)) {
		client_prop_res->status = HECI_CONNECT_STATUS_CLIENT_NOT_FOUND;
	} else {
		struct hbm_client_properties *client_prop;

		client_ctx = heci_get_client_context(client_prop_req->address);
		client = client_ctx->client;
		client_prop = &client_prop_res->client_prop;

		client_prop->protocol_name = client->protocol_id;
		client_prop->protocol_version = client->protocol_ver;
		client_prop->max_number_of_connections =
						client->max_n_of_connections;
		client_prop->max_msg_length = client->max_msg_size;
		client_prop->dma_hdr_len = client->dma_header_length;
		client_prop->dma_hdr_len |= client->dma_enabled ?
					    CLIENT_DMA_ENABLE : 0;
	}

	heci_send_heci_msg(&heci_msg);

	return EC_SUCCESS;
}

static int heci_send_flow_control(uint8_t fw_addr)
{
	struct heci_client_connect *connect;
	struct hbm_i2h *i2h;
	struct hbm_flow_control *flow_ctrl;
	struct heci_msg heci_msg;

	connect = heci_get_client_connect(fw_addr);

	heci_build_hbm_header(&heci_msg.hdr,
			      sizeof(i2h->cmd) + sizeof(*flow_ctrl));

	i2h = (struct hbm_i2h *)heci_msg.payload;
	i2h->cmd = HECI_BUS_MSG_FLOW_CONTROL;
	flow_ctrl = (struct hbm_flow_control *)&i2h->data;

	memset(flow_ctrl, 0, sizeof(*flow_ctrl));

	flow_ctrl->fw_addr = fw_addr;
	flow_ctrl->host_addr = connect->host_addr;

	heci_send_heci_msg(&heci_msg);

	return EC_SUCCESS;
}

static int handle_client_connect_req(
			struct hbm_client_connect_req *client_connect_req)
{
	struct hbm_client_connect_res *client_connect_res;
	struct heci_msg heci_msg;
	struct hbm_i2h *i2h;
	struct heci_client_connect *connect;

	heci_build_hbm_header(&heci_msg.hdr,
			      sizeof(i2h->cmd) + sizeof(*client_connect_res));

	i2h = (struct hbm_i2h *)heci_msg.payload;
	i2h->cmd = HECI_BUS_MSG_CLIENT_CONNECT_RESP;
	client_connect_res = (struct hbm_client_connect_res *)&i2h->data;

	memset(client_connect_res, 0, sizeof(*client_connect_res));

	client_connect_res->fw_addr = client_connect_req->fw_addr;
	client_connect_res->host_addr = client_connect_req->host_addr;
	if (!heci_is_valid_client_addr(client_connect_req->fw_addr)) {
		client_connect_res->status =
					HECI_CONNECT_STATUS_CLIENT_NOT_FOUND;
	} else if (!client_connect_req->host_addr) {
		client_connect_res->status =
			HECI_CONNECT_STATUS_INVALID_PARAMETER;
	} else {
		connect = heci_get_client_connect(client_connect_req->fw_addr);
		if (connect->is_connected) {
			client_connect_res->status =
					HECI_CONNECT_STATUS_ALREADY_EXISTS;
		} else {
			connect->is_connected = 1;
			connect->host_addr = client_connect_req->host_addr;
		}
	}

	heci_send_heci_msg(&heci_msg);

	/* no error, send flow control */
	if (!client_connect_res->status)
		heci_send_flow_control(client_connect_req->fw_addr);

	return EC_SUCCESS;
}

static int handle_flow_control_cmd(struct hbm_flow_control *flow_ctrl)
{
	struct heci_client_connect *connect;
	int waiting_task;

	if (!heci_is_valid_client_addr(flow_ctrl->fw_addr))
		return -1;

	if (!heci_is_client_connected(flow_ctrl->fw_addr))
		return -1;

	connect = heci_get_client_connect(flow_ctrl->fw_addr);

	mutex_lock(&connect->cred_lock);
	connect->flow_ctrl_creds = 1;
	waiting_task = connect->waiting_task;
	mutex_unlock(&connect->cred_lock);

	if (waiting_task)
		task_set_event(waiting_task, TASK_EVENT_IPC_READY);

	return EC_SUCCESS;
}

static void heci_handle_client_msg(struct heci_msg *msg, size_t length)
{
	struct heci_client_context *cli_ctx;
	struct heci_client_connect *connect;
	const struct heci_client_callbacks *cbs;
	int payload_size;

	if (!heci_is_valid_client_addr(msg->hdr.fw_addr))
		return;

	if (!heci_is_client_connected(msg->hdr.fw_addr))
		return;

	cli_ctx = heci_get_client_context(msg->hdr.fw_addr);
	cbs = cli_ctx->client->cbs;
	connect = &cli_ctx->connect;

	payload_size = HECI_MSG_LENGTH(msg->hdr.length);
	if (connect->is_connected &&
	    msg->hdr.host_addr == connect->host_addr) {
		if (!connect->ignore_rx_msg &&
		    connect->rx_msg_length + payload_size > HECI_MAX_MSG_SIZE) {
			connect->ignore_rx_msg = 1; /* too big. discard */
		}

		if (!connect->ignore_rx_msg) {
			memcpy(connect->rx_msg + connect->rx_msg_length,
			       msg->payload, payload_size);

			connect->rx_msg_length += payload_size;
		}

		if (HECI_MSG_IS_COMPLETED(msg->hdr.length)) {
			if (!connect->ignore_rx_msg) {
				cbs->new_msg_received(
					TO_HECI_HANDLE(msg->hdr.fw_addr),
					connect->rx_msg,
					connect->rx_msg_length);
			}

			connect->rx_msg_length = 0;
			connect->ignore_rx_msg = 0;

			heci_send_flow_control(msg->hdr.fw_addr);
		}
	}
}

static int handle_client_disconnect_req(
			struct hbm_client_disconnect_req *client_disconnect_req)
{
	struct hbm_client_disconnect_res *client_disconnect_res;
	struct heci_msg heci_msg;
	struct hbm_i2h *i2h;
	struct heci_client_context *cli_ctx;
	struct heci_client_connect *connect;
	const struct heci_client_callbacks *cbs;
	uint8_t fw_addr, host_addr;

	CPRINTS("Got HECI disconnect request");

	heci_build_hbm_header(&heci_msg.hdr, sizeof(i2h->cmd) +
					     sizeof(*client_disconnect_res));

	i2h = (struct hbm_i2h *)heci_msg.payload;
	i2h->cmd = HECI_BUS_MSG_CLIENT_DISCONNECT_RESP;
	client_disconnect_res = (struct hbm_client_disconnect_res *)&i2h->data;

	memset(client_disconnect_res, 0, sizeof(*client_disconnect_res));

	fw_addr = client_disconnect_req->fw_addr;
	host_addr = client_disconnect_req->host_addr;

	client_disconnect_res->fw_addr = fw_addr;
	client_disconnect_res->host_addr = host_addr;
	if (!heci_is_valid_client_addr(fw_addr) ||
	    !heci_is_client_connected(fw_addr)) {
		client_disconnect_res->status =
					HECI_CONNECT_STATUS_CLIENT_NOT_FOUND;
	} else {
		connect = heci_get_client_connect(fw_addr);
		if (connect->host_addr != host_addr) {
			client_disconnect_res->status =
				HECI_CONNECT_STATUS_INVALID_PARAMETER;
		} else {
			cli_ctx = heci_get_client_context(fw_addr);
			cbs = cli_ctx->client->cbs;
			mutex_lock(&connect->lock);
			if (connect->is_connected) {
				cbs->disconnected(TO_HECI_HANDLE(fw_addr));
				connect->is_connected = 0;
			}
			mutex_unlock(&connect->lock);
		}
	}

	heci_send_heci_msg(&heci_msg);

	return EC_SUCCESS;
}

/* host stops due to version mismatch */
static int handle_host_stop_req(struct hbm_host_stop_req *host_stop_req)
{
	struct hbm_host_stop_res *host_stop_res;
	struct heci_msg heci_msg;
	struct hbm_i2h *i2h;

	heci_build_hbm_header(&heci_msg.hdr,
			      sizeof(i2h->cmd) + sizeof(*host_stop_res));

	i2h = (struct hbm_i2h *)heci_msg.payload;
	i2h->cmd = HECI_BUS_MSG_HOST_STOP_RESP;
	host_stop_res = (struct hbm_host_stop_res *)&i2h->data;

	memset(host_stop_res, 0, sizeof(*host_stop_res));

	heci_send_heci_msg(&heci_msg);

	return EC_SUCCESS;
}

static int is_hbm_validity(struct hbm_h2i *h2i, size_t length)
{
	int valid_msg_len;

	valid_msg_len = sizeof(h2i->cmd);

	switch (h2i->cmd) {
	case HECI_BUS_MSG_VERSION_REQ:
		valid_msg_len += sizeof(struct hbm_version_req);
		break;

	case HECI_BUS_MSG_HOST_ENUM_REQ:
		valid_msg_len += sizeof(struct hbm_enum_req);
		break;

	case HECI_BUS_MSG_HOST_CLIENT_PROP_REQ:
		valid_msg_len += sizeof(struct hbm_client_prop_req);
		break;

	case HECI_BUS_MSG_CLIENT_CONNECT_REQ:
		valid_msg_len += sizeof(struct hbm_client_connect_req);
		break;

	case HECI_BUS_MSG_FLOW_CONTROL:
		valid_msg_len += sizeof(struct hbm_flow_control);
		break;

	case HECI_BUS_MSG_CLIENT_DISCONNECT_REQ:
		valid_msg_len += sizeof(struct hbm_client_disconnect_req);
		break;

	case HECI_BUS_MSG_HOST_STOP_REQ:
		valid_msg_len += sizeof(struct hbm_host_stop_req);
		break;

/* TODO: DMA support for large data */
#if 0
	case HECI_BUS_MSG_DMA_REQ:
		valid_msg_len += sizeof(struct hbm_dma_req);
		break;

	case HECI_BUS_MSG_DMA_ALLOC_NOTIFY:
		valid_msg_len += sizeof(struct hbm_dma_alloc_notify);
		break;

	case HECI_BUS_MSG_DMA_XFER_REQ: /* DMA transfer to FW */
		valid_msg_len += sizeof(struct hbm_dma_xfer_req);
		break;

	case HECI_BUS_MSG_DMA_XFER_RESP: /* Ack for DMA transfer from FW */
		valid_msg_len += sizeof(struct hbm_dma_xfer_resp);
		break;
#endif
	default:
		break;
	}

	if (valid_msg_len != length) {
		CPRINTF("invalid cmd(%d) valid : %d, cur : %zd\n",
			h2i->cmd, valid_msg_len, length);
		/* TODO: invalid cmd. not sure to reply with error ? */
		return 0;
	}

	return 1;
}

static void heci_handle_hbm(struct hbm_h2i *h2i, size_t length)
{
	void *data = (void *)&h2i->data;

	if (!is_hbm_validity(h2i, length))
		return;

	switch (h2i->cmd) {
	case HECI_BUS_MSG_VERSION_REQ:
		handle_version_req((struct hbm_version_req *)data);
		break;

	case HECI_BUS_MSG_HOST_ENUM_REQ:
		handle_enum_req((struct hbm_enum_req *)data);
		break;

	case HECI_BUS_MSG_HOST_CLIENT_PROP_REQ:
		handle_client_prop_req((struct hbm_client_prop_req *)data);
		break;

	case HECI_BUS_MSG_CLIENT_CONNECT_REQ:
		handle_client_connect_req(
					(struct hbm_client_connect_req *)data);
		break;

	case HECI_BUS_MSG_FLOW_CONTROL:
		handle_flow_control_cmd((struct hbm_flow_control *)data);
		break;

	case HECI_BUS_MSG_CLIENT_DISCONNECT_REQ:
		handle_client_disconnect_req(
				(struct hbm_client_disconnect_req *)data);
		break;

	case HECI_BUS_MSG_HOST_STOP_REQ:
		handle_host_stop_req((struct hbm_host_stop_req *)data);
		break;

/* TODO: DMA transfer if data is too big >= ? KB */
#if 0
	case HECI_BUS_MSG_DMA_REQ:
		handle_dma_req((struct hbm_dma_req *)data);
		break;

	case HECI_BUS_MSG_DMA_ALLOC_NOTIFY:
		handle_dma_alloc_notify((struct hbm_dma_alloc_notify *));
		break;

	case HECI_BUS_MSG_DMA_XFER_REQ: /* DMA transfer to FW */
		handle_dma_xfer_req((struct hbm_dma_xfer_req *)data);
		break;

	case HECI_BUS_MSG_DMA_XFER_RESP: /* Ack for DMA transfer from FW */
		handle_dma_xfer_resp((struct hbm_dma_xfer_resp *)data);
		break;
#endif
	default:
		break;
	}
}

static void heci_handle_heci_msg(struct heci_msg *heci_msg, size_t msg_length)
{
	if (!heci_msg->hdr.host_addr) {
		/*
		 * message for HECI bus or a fixed client should fit
		 * into one IPC message
		 */
		if (!HECI_MSG_IS_COMPLETED(heci_msg->hdr.length)) {
			CPRINTS("message not completed");
			return;
		}

		if (heci_msg->hdr.fw_addr == HECI_FIXED_SYSTEM_STATE_ADDR)
			heci_handle_system_state_msg(
				heci_msg->payload,
				HECI_MSG_LENGTH(heci_msg->hdr.length));
		else if (!heci_msg->hdr.fw_addr)
			/* HECI Bus Message(fw_addr == 0 && host_addr == 0) */
			heci_handle_hbm((struct hbm_h2i *)heci_msg->payload,
					HECI_MSG_LENGTH(heci_msg->hdr.length));
		else
			CPRINTS("not supported fixed client(%d)",
				heci_msg->hdr.fw_addr);
	} else {
		/* host_addr != 0 : Msg for Dynamic client */
		heci_handle_client_msg(heci_msg, msg_length);
	}
}

/* event flag for HECI msg */
#define EVENT_FLAG_BIT_HECI_MSG			TASK_EVENT_CUSTOM_BIT(0)

void heci_rx_task(void)
{
	int msg_len;
	struct heci_msg heci_msg;
	ipc_handle_t ipc_handle;

	/* open IPC for HECI protocol */
	heci_bus_ctx.ipc_handle = ipc_open(IPC_PEER_ID_HOST, IPC_PROTOCOL_HECI,
					   EVENT_FLAG_BIT_HECI_MSG);

	ASSERT(heci_bus_ctx.ipc_handle != IPC_INVALID_HANDLE);

	/* get ipc handle */
	ipc_handle = heci_bus_ctx.ipc_handle;

	while (1) {
		/* task will be blocked here, waiting for event */
		msg_len = ipc_read(ipc_handle, &heci_msg, sizeof(heci_msg), -1);

		if (msg_len <= 0) {
			CPRINTS("discard heci packet");
			continue;
		}

		if (HECI_MSG_LENGTH(heci_msg.hdr.length) + sizeof(heci_msg.hdr)
		    == msg_len)
			heci_handle_heci_msg(&heci_msg, msg_len);
		else
			CPRINTS("msg len mismatch.. discard..");
	}
}
