/* Copyright 2020 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 "common.h"
#include "device_event.h"
#include "hooks.h"
#include "host_command.h"
#include "peripheral_charger.h"
#include "queue.h"
#include "stdbool.h"
#include "task.h"
#include "timer.h"
#include "util.h"

/* Peripheral Charge Manager */

#define CPRINTS(fmt, args...) cprints(CC_PCHG, "PCHG: " fmt, ##args)

static int dropped_event;

static void pchg_queue_event(struct pchg *ctx, enum pchg_event event)
{
	mutex_lock(&ctx->mtx);
	if (queue_add_unit(&ctx->events, &event) == 0) {
		dropped_event++;
		CPRINTS("ERR: Queue is full");
	}
	mutex_unlock(&ctx->mtx);
}

static const char *_text_state(enum pchg_state state)
{
	/* TODO: Use "S%d" for normal build. */
	static const char * const state_names[] = EC_PCHG_STATE_TEXT;

	if (state >= sizeof(state_names))
		return "UNDEF";

	return state_names[state];
}

static const char *_text_event(enum pchg_event event)
{
	/* TODO: Use "S%d" for normal build. */
	static const char * const event_names[] = {
		[PCHG_EVENT_NONE] = "NONE",
		[PCHG_EVENT_IRQ] = "IRQ",
		[PCHG_EVENT_INITIALIZED] = "INITIALIZED",
		[PCHG_EVENT_ENABLED] = "ENABLED",
		[PCHG_EVENT_DISABLED] = "DISABLED",
		[PCHG_EVENT_DEVICE_DETECTED] = "DEVICE_DETECTED",
		[PCHG_EVENT_DEVICE_LOST] = "DEVICE_LOST",
		[PCHG_EVENT_CHARGE_STARTED] = "CHARGE_STARTED",
		[PCHG_EVENT_CHARGE_UPDATE] = "CHARGE_UPDATE",
		[PCHG_EVENT_CHARGE_ENDED] = "CHARGE_ENDED",
		[PCHG_EVENT_CHARGE_STOPPED] = "CHARGE_STOPPED",
		[PCHG_EVENT_CHARGE_ERROR] = "CHARGE_ERROR",
		[PCHG_EVENT_INITIALIZE] = "INITIALIZE",
		[PCHG_EVENT_ENABLE] = "ENABLE",
		[PCHG_EVENT_DISABLE] = "DISABLE",
	};

	if (event >= sizeof(event_names))
		return "UNDEF";

	return event_names[event];
}

static enum pchg_state pchg_state_reset(struct pchg *ctx)
{
	enum pchg_state state = PCHG_STATE_RESET;
	int rv;

	switch (ctx->event) {
	case PCHG_EVENT_INITIALIZE:
		rv = ctx->cfg->drv->init(ctx);
		if (rv == EC_SUCCESS) {
			pchg_queue_event(ctx, PCHG_EVENT_ENABLE);
			state = PCHG_STATE_INITIALIZED;
		} else if (rv != EC_SUCCESS_IN_PROGRESS) {
			CPRINTS("ERR: Failed to initialize");
		}
		break;
	case PCHG_EVENT_INITIALIZED:
		pchg_queue_event(ctx, PCHG_EVENT_ENABLE);
		state = PCHG_STATE_INITIALIZED;
		break;
	default:
		break;
	}

	return state;
}

static enum pchg_state pchg_state_initialized(struct pchg *ctx)
{
	enum pchg_state state = PCHG_STATE_INITIALIZED;
	int rv;

	if (ctx->event == PCHG_EVENT_ENABLE)
		ctx->error &= ~PCHG_ERROR_HOST;

	/* Spin in INITIALIZED until error condition is cleared. */
	if (ctx->error)
		return state;

	switch (ctx->event) {
	case PCHG_EVENT_ENABLE:
		rv = ctx->cfg->drv->enable(ctx, true);
		if (rv == EC_SUCCESS)
			state = PCHG_STATE_ENABLED;
		else if (rv != EC_SUCCESS_IN_PROGRESS)
			CPRINTS("ERR: Failed to enable");
		break;
	case PCHG_EVENT_ENABLED:
		state = PCHG_STATE_ENABLED;
		break;
	default:
		break;
	}

	return state;
}

static enum pchg_state pchg_state_enabled(struct pchg *ctx)
{
	enum pchg_state state = PCHG_STATE_ENABLED;
	int rv;

	switch (ctx->event) {
	case PCHG_EVENT_DISABLE:
		ctx->error |= PCHG_ERROR_HOST;
		rv = ctx->cfg->drv->enable(ctx, false);
		if (rv == EC_SUCCESS)
			state = PCHG_STATE_INITIALIZED;
		else if (rv != EC_SUCCESS_IN_PROGRESS)
			CPRINTS("ERR: Failed to disable");
		break;
	case PCHG_EVENT_DISABLED:
		state = PCHG_STATE_INITIALIZED;
		break;
	case PCHG_EVENT_DEVICE_DETECTED:
		state = PCHG_STATE_DETECTED;
		break;
	case PCHG_EVENT_CHARGE_STARTED:
		state = PCHG_STATE_CHARGING;
		break;
	default:
		break;
	}

	return state;
}

static enum pchg_state pchg_state_detected(struct pchg *ctx)
{
	enum pchg_state state = PCHG_STATE_DETECTED;
	int rv;

	switch (ctx->event) {
	case PCHG_EVENT_DISABLE:
		ctx->error |= PCHG_ERROR_HOST;
		rv = ctx->cfg->drv->enable(ctx, false);
		if (rv == EC_SUCCESS)
			state = PCHG_STATE_INITIALIZED;
		else if (rv != EC_SUCCESS_IN_PROGRESS)
			CPRINTS("ERR: Failed to disable");
		break;
	case PCHG_EVENT_DISABLED:
		state = PCHG_STATE_INITIALIZED;
		break;
	case PCHG_EVENT_CHARGE_STARTED:
		state = PCHG_STATE_CHARGING;
		break;
	case PCHG_EVENT_DEVICE_LOST:
		state = PCHG_STATE_ENABLED;
		break;
	case PCHG_EVENT_CHARGE_ERROR:
		state = PCHG_STATE_INITIALIZED;
		break;
	default:
		break;
	}

	return state;
}

static enum pchg_state pchg_state_charging(struct pchg *ctx)
{
	enum pchg_state state = PCHG_STATE_CHARGING;
	int rv;

	switch (ctx->event) {
	case PCHG_EVENT_DISABLE:
		ctx->error |= PCHG_ERROR_HOST;
		rv = ctx->cfg->drv->enable(ctx, false);
		if (rv == EC_SUCCESS)
			state = PCHG_STATE_INITIALIZED;
		else if (rv != EC_SUCCESS_IN_PROGRESS)
			CPRINTS("ERR: Failed to disable");
		break;
	case PCHG_EVENT_DISABLED:
		state = PCHG_STATE_INITIALIZED;
		break;
	case PCHG_EVENT_CHARGE_UPDATE:
		CPRINTS("Battery %d%%", ctx->battery_percent);
		break;
	case PCHG_EVENT_DEVICE_LOST:
		state = PCHG_STATE_ENABLED;
		break;
	case PCHG_EVENT_CHARGE_ERROR:
		state = PCHG_STATE_INITIALIZED;
		break;
	case PCHG_EVENT_CHARGE_ENDED:
	case PCHG_EVENT_CHARGE_STOPPED:
		state = PCHG_STATE_DETECTED;
		break;
	default:
		break;
	}

	return state;
}

static void pchg_run(struct pchg *ctx)
{
	enum pchg_state previous_state = ctx->state;
	int port = PCHG_CTX_TO_PORT(ctx);
	int rv;

	mutex_lock(&ctx->mtx);
	if (!queue_remove_unit(&ctx->events, &ctx->event)) {
		mutex_unlock(&ctx->mtx);
		CPRINTS("P%d No event in queue", port);
		return;
	}
	mutex_unlock(&ctx->mtx);

	CPRINTS("P%d Run in %s for EVENT_%s", port,
		_text_state(ctx->state), _text_event(ctx->event));

	if (ctx->event == PCHG_EVENT_IRQ) {
		rv = ctx->cfg->drv->get_event(ctx);
		if (rv) {
			CPRINTS("ERR: get_event (%d)", rv);
			ctx->event = PCHG_EVENT_NONE;
			return;
		}
		CPRINTS("(IRQ:EVENT_%s)", _text_event(ctx->event));
	}

	switch (ctx->state) {
	case PCHG_STATE_RESET:
		ctx->state = pchg_state_reset(ctx);
		break;
	case PCHG_STATE_INITIALIZED:
		ctx->state = pchg_state_initialized(ctx);
		break;
	case PCHG_STATE_ENABLED:
		ctx->state = pchg_state_enabled(ctx);
		break;
	case PCHG_STATE_DETECTED:
		ctx->state = pchg_state_detected(ctx);
		break;
	case PCHG_STATE_CHARGING:
		ctx->state = pchg_state_charging(ctx);
		break;
	default:
		CPRINTS("ERR: Unknown state (%d)", ctx->state);
		break;
	}

	if (previous_state != ctx->state)
		CPRINTS("->%s", _text_state(ctx->state));

	ctx->event = PCHG_EVENT_NONE;
	CPRINTS("Done");
}

void pchg_irq(enum gpio_signal signal)
{
	struct pchg *ctx;
	int i;

	for (i = 0; i < pchg_count; i++) {
		ctx = &pchgs[i];
		if (signal == ctx->cfg->irq_pin) {
			ctx->irq = 1;
			task_wake(TASK_ID_PCHG);
			return;
		}
	}
}

void pchg_task(void *u)
{
	struct pchg *ctx;
	int p;

	/* TODO: i2c is wedged for a while after reset. investigate. */
	msleep(500);

	for (p = 0; p < pchg_count; p++) {
		ctx = &pchgs[p];
		ctx->state = PCHG_STATE_RESET;
		queue_init(&ctx->events);
		pchg_queue_event(ctx, PCHG_EVENT_INITIALIZE);
		gpio_enable_interrupt(ctx->cfg->irq_pin);
	}

	while (true) {
		/* Process pending events for all ports. */
		for (p = 0; p < pchg_count; p++) {
			ctx = &pchgs[p];
			do {
				if (atomic_clear(&ctx->irq))
					pchg_queue_event(ctx, PCHG_EVENT_IRQ);
				pchg_run(ctx);
			} while (queue_count(&ctx->events));
		}
		/*
		 * Send one host event for all ports. Currently PCHG supports
		 * only WLC but in the future other types (e.g. WPC Qi) should
		 * send different device events.
		 */
		device_set_single_event(EC_DEVICE_EVENT_WLC);
		task_wait_event(-1);
	}
}

static enum ec_status hc_pchg_count(struct host_cmd_handler_args *args)
{
	struct ec_response_pchg_count *r = args->response;

	r->port_count = pchg_count;
	args->response_size = sizeof(*r);

	return EC_RES_SUCCESS;
}
DECLARE_HOST_COMMAND(EC_CMD_PCHG_COUNT, hc_pchg_count, EC_VER_MASK(0));

static enum ec_status hc_pchg(struct host_cmd_handler_args *args)
{
	const struct ec_params_pchg *p = args->params;
	struct ec_response_pchg *r = args->response;
	int port = p->port;
	struct pchg *ctx;

	if (port >= pchg_count)
		return EC_RES_INVALID_PARAM;

	ctx = &pchgs[port];

	r->state = ctx->state;
	r->battery_percentage = ctx->battery_percent;
	r->error = ctx->error;

	args->response_size = sizeof(*r);

	return EC_RES_SUCCESS;
}
DECLARE_HOST_COMMAND(EC_CMD_PCHG, hc_pchg, EC_VER_MASK(0));

static int cc_pchg(int argc, char **argv)
{
	int port;
	char *end;
	struct pchg *ctx;

	if (argc < 2 || 3 < argc)
		return EC_ERROR_PARAM_COUNT;

	port = strtoi(argv[1], &end, 0);
	if (*end || port < 0 || port >= pchg_count)
		return EC_ERROR_PARAM2;
	ctx = &pchgs[port];

	if (argc == 2) {
		ccprintf("P%d %s %s\n", port,
			 _text_state(ctx->state), _text_event(ctx->event));
		return EC_SUCCESS;
	}

	if (!strcasecmp(argv[2], "init")) {
		ctx->state = PCHG_STATE_RESET;
		pchg_queue_event(ctx, PCHG_EVENT_INITIALIZE);
	} else if (!strcasecmp(argv[2], "enable")) {
		pchg_queue_event(ctx, PCHG_EVENT_ENABLE);
	} else if (!strcasecmp(argv[2], "disable")) {
		pchg_queue_event(ctx, PCHG_EVENT_DISABLE);
	} else {
		return EC_ERROR_PARAM1;
	}

	task_wake(TASK_ID_PCHG);

	return EC_SUCCESS;
}
DECLARE_CONSOLE_COMMAND(pchg, cc_pchg,
			"<port> [init/enable/disable]"
			"\n\t<port>"
			"\n\t<port> init"
			"\n\t<port> enable"
			"\n\t<port> disable",
			"Control peripheral chargers");
