/*
 * PowerNV OPAL power control for graceful shutdown handling
 *
 * Copyright 2015 IBM Corp.
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version
 * 2 of the License, or (at your option) any later version.
 */

#define pr_fmt(fmt)	"opal-power: "	fmt

#include <linux/kernel.h>
#include <linux/reboot.h>
#include <linux/notifier.h>
#include <linux/of.h>

#include <asm/opal.h>
#include <asm/machdep.h>

#define SOFT_OFF 0x00
#define SOFT_REBOOT 0x01

/* Detect EPOW event */
static bool detect_epow(void)
{
	u16 epow;
	int i, rc;
	__be16 epow_classes;
	__be16 opal_epow_status[OPAL_SYSEPOW_MAX] = {0};

	/*
	* Check for EPOW event. Kernel sends supported EPOW classes info
	* to OPAL. OPAL returns EPOW info along with classes present.
	*/
	epow_classes = cpu_to_be16(OPAL_SYSEPOW_MAX);
	rc = opal_get_epow_status(opal_epow_status, &epow_classes);
	if (rc != OPAL_SUCCESS) {
		pr_err("Failed to get EPOW event information\n");
		return false;
	}

	/* Look for EPOW events present */
	for (i = 0; i < be16_to_cpu(epow_classes); i++) {
		epow = be16_to_cpu(opal_epow_status[i]);

		/* Filter events which do not need shutdown. */
		if (i == OPAL_SYSEPOW_POWER)
			epow &= ~(OPAL_SYSPOWER_CHNG | OPAL_SYSPOWER_FAIL |
					OPAL_SYSPOWER_INCL);
		if (epow)
			return true;
	}

	return false;
}

/* Check for existing EPOW, DPO events */
static bool poweroff_pending(void)
{
	int rc;
	__be64 opal_dpo_timeout;

	/* Check for DPO event */
	rc = opal_get_dpo_status(&opal_dpo_timeout);
	if (rc == OPAL_SUCCESS) {
		pr_info("Existing DPO event detected.\n");
		return true;
	}

	/* Check for EPOW event */
	if (detect_epow()) {
		pr_info("Existing EPOW event detected.\n");
		return true;
	}

	return false;
}

/* OPAL power-control events notifier */
static int opal_power_control_event(struct notifier_block *nb,
					unsigned long msg_type, void *msg)
{
	uint64_t type;

	switch (msg_type) {
	case OPAL_MSG_EPOW:
		if (detect_epow()) {
			pr_info("EPOW msg received. Powering off system\n");
			orderly_poweroff(true);
		}
		break;
	case OPAL_MSG_DPO:
		pr_info("DPO msg received. Powering off system\n");
		orderly_poweroff(true);
		break;
	case OPAL_MSG_SHUTDOWN:
		type = be64_to_cpu(((struct opal_msg *)msg)->params[0]);
		switch (type) {
		case SOFT_REBOOT:
			pr_info("Reboot requested\n");
			orderly_reboot();
			break;
		case SOFT_OFF:
			pr_info("Poweroff requested\n");
			orderly_poweroff(true);
			break;
		default:
			pr_err("Unknown power-control type %llu\n", type);
		}
		break;
	default:
		pr_err("Unknown OPAL message type %lu\n", msg_type);
	}

	return 0;
}

/* OPAL EPOW event notifier block */
static struct notifier_block opal_epow_nb = {
	.notifier_call	= opal_power_control_event,
	.next		= NULL,
	.priority	= 0,
};

/* OPAL DPO event notifier block */
static struct notifier_block opal_dpo_nb = {
	.notifier_call	= opal_power_control_event,
	.next		= NULL,
	.priority	= 0,
};

/* OPAL power-control event notifier block */
static struct notifier_block opal_power_control_nb = {
	.notifier_call	= opal_power_control_event,
	.next		= NULL,
	.priority	= 0,
};

static int __init opal_power_control_init(void)
{
	int ret, supported = 0;
	struct device_node *np;

	/* Register OPAL power-control events notifier */
	ret = opal_message_notifier_register(OPAL_MSG_SHUTDOWN,
						&opal_power_control_nb);
	if (ret)
		pr_err("Failed to register SHUTDOWN notifier, ret = %d\n", ret);

	/* Determine OPAL EPOW, DPO support */
	np = of_find_node_by_path("/ibm,opal/epow");
	if (np) {
		supported = of_device_is_compatible(np, "ibm,opal-v3-epow");
		of_node_put(np);
	}

	if (!supported)
		return 0;
	pr_info("OPAL EPOW, DPO support detected.\n");

	/* Register EPOW event notifier */
	ret = opal_message_notifier_register(OPAL_MSG_EPOW, &opal_epow_nb);
	if (ret)
		pr_err("Failed to register EPOW notifier, ret = %d\n", ret);

	/* Register DPO event notifier */
	ret = opal_message_notifier_register(OPAL_MSG_DPO, &opal_dpo_nb);
	if (ret)
		pr_err("Failed to register DPO notifier, ret = %d\n", ret);

	/* Check for any pending EPOW or DPO events. */
	if (poweroff_pending())
		orderly_poweroff(true);

	return 0;
}
machine_subsys_initcall(powernv, opal_power_control_init);
