/* $NetBSD: aibs_acpi.c,v 1.7 2021/01/29 15:49:55 thorpej Exp $ */

/*-
 * Copyright (c) 2011 The NetBSD Foundation, Inc.
 * All rights reserved.
 *
 * This code is derived from software contributed to The NetBSD Foundation
 * by Jukka Ruohonen.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE NETBSD FOUNDATION, INC. AND CONTRIBUTORS
 * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
 * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE FOUNDATION OR CONTRIBUTORS
 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */

/*	$OpenBSD: atk0110.c,v 1.1 2009/07/23 01:38:16 cnst Exp $	*/
/*
 * Copyright (c) 2009 Constantine A. Murenin <cnst+netbsd@bugmail.mojo.ru>
 *
 * Permission to use, copy, modify, and distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */

#include <sys/cdefs.h>
__KERNEL_RCSID(0, "$NetBSD: aibs_acpi.c,v 1.7 2021/01/29 15:49:55 thorpej Exp $");

#include <sys/param.h>
#include <sys/kmem.h>
#include <sys/module.h>

#include <dev/acpi/acpireg.h>
#include <dev/acpi/acpivar.h>

/*
 * ASUSTeK AI Booster (ACPI ASOC ATK0110).
 *
 * This code was originally written for OpenBSD after the techniques
 * described in the Linux's asus_atk0110.c and FreeBSD's acpi_aiboost.c
 * were verified to be accurate on the actual hardware kindly provided by
 * Sam Fourman Jr.  It was subsequently ported from OpenBSD to DragonFly BSD,
 * and then to the NetBSD's sysmon_envsys(9) framework.
 *
 *				  -- Constantine A. Murenin <http://cnst.su/>
 */

#define _COMPONENT		 ACPI_RESOURCE_COMPONENT
ACPI_MODULE_NAME		 ("acpi_aibs")

#define AIBS_MUX_HWMON		 0x00000006
#define AIBS_MUX_MGMT		 0x00000011

#define AIBS_TYPE(x)		 (((x) >> 16) & 0xff)
#define AIBS_TYPE_VOLT		 2
#define AIBS_TYPE_TEMP		 3
#define AIBS_TYPE_FAN		 4

struct aibs_sensor {
	envsys_data_t			 as_sensor;
	uint64_t			 as_type;
	uint64_t			 as_liml;
	uint64_t			 as_limh;

	SIMPLEQ_ENTRY(aibs_sensor)	 as_list;
};

struct aibs_softc {
	device_t			 sc_dev;
	struct acpi_devnode		*sc_node;
	struct sysmon_envsys		*sc_sme;
	bool				 sc_model;	/* new model = true */

	SIMPLEQ_HEAD(, aibs_sensor)	 as_head;
};

static int	aibs_match(device_t, cfdata_t, void *);
static void	aibs_attach(device_t, device_t, void *);
static int	aibs_detach(device_t, int);

static void	aibs_init(device_t);
static void	aibs_init_new(device_t);
static void	aibs_init_old(device_t, int);

static void	aibs_sensor_add(device_t, ACPI_OBJECT *);
static bool	aibs_sensor_value(device_t, struct aibs_sensor *, uint64_t *);
static void	aibs_sensor_refresh(struct sysmon_envsys *, envsys_data_t *);
static void	aibs_sensor_limits(struct sysmon_envsys *, envsys_data_t *,
				   sysmon_envsys_lim_t *, uint32_t *);

CFATTACH_DECL_NEW(aibs, sizeof(struct aibs_softc),
    aibs_match, aibs_attach, aibs_detach, NULL);

static const struct device_compatible_entry compat_data[] = {
	{ .compat = "ATK0110" },
	DEVICE_COMPAT_EOL
};

static int
aibs_match(device_t parent, cfdata_t match, void *aux)
{
	struct acpi_attach_args *aa = aux;

	return acpi_compatible_match(aa, compat_data);
}

static void
aibs_attach(device_t parent, device_t self, void *aux)
{
	struct aibs_softc *sc = device_private(self);
	struct acpi_attach_args *aa = aux;

	sc->sc_dev = self;
	sc->sc_node = aa->aa_node;

	aprint_naive("\n");
	aprint_normal(": ASUSTeK AI Booster\n");

	sc->sc_sme = sysmon_envsys_create();

	sc->sc_sme->sme_cookie = sc;
	sc->sc_sme->sme_name = device_xname(self);
	sc->sc_sme->sme_refresh = aibs_sensor_refresh;
	sc->sc_sme->sme_get_limits = aibs_sensor_limits;

	aibs_init(self);
	SIMPLEQ_INIT(&sc->as_head);

	if (sc->sc_model != false)
		aibs_init_new(self);
	else {
		aibs_init_old(self, AIBS_TYPE_FAN);
		aibs_init_old(self, AIBS_TYPE_TEMP);
		aibs_init_old(self, AIBS_TYPE_VOLT);
	}

	(void)pmf_device_register(self, NULL, NULL);

	if (sc->sc_sme->sme_nsensors == 0) {
		aprint_error_dev(self, "no sensors found\n");
		sysmon_envsys_destroy(sc->sc_sme);
		sc->sc_sme = NULL;
		return;
	}

	if (sysmon_envsys_register(sc->sc_sme) != 0)
		aprint_error_dev(self, "failed to register with sysmon\n");
}

static int
aibs_detach(device_t self, int flags)
{
	struct aibs_softc *sc = device_private(self);
	struct aibs_sensor *as;

	pmf_device_deregister(self);

	if (sc->sc_sme != NULL)
		sysmon_envsys_unregister(sc->sc_sme);

	while (SIMPLEQ_FIRST(&sc->as_head) != NULL) {
		as = SIMPLEQ_FIRST(&sc->as_head);
		SIMPLEQ_REMOVE_HEAD(&sc->as_head, as_list);
		kmem_free(as, sizeof(*as));
	}

	return 0;
}

static void
aibs_init(device_t self)
{
	struct aibs_softc *sc = device_private(self);
	ACPI_HANDLE tmp;
	ACPI_STATUS rv;

	/*
	 * Old model uses the tuple { TSIF, VSIF, FSIF } to
	 * enumerate the sensors and { RTMP, RVLT, RFAN }
	 * to obtain the values. New mode uses GGRP for the
	 * enumeration and { GITM, SITM } as accessors.
	 */
	rv = AcpiGetHandle(sc->sc_node->ad_handle, "GGRP", &tmp);

	if (ACPI_FAILURE(rv)) {
		sc->sc_model = false;
		return;
	}

	rv = AcpiGetHandle(sc->sc_node->ad_handle, "GITM", &tmp);

	if (ACPI_FAILURE(rv)) {
		sc->sc_model = false;
		return;
	}

	rv = AcpiGetHandle(sc->sc_node->ad_handle, "SITM", &tmp);

	if (ACPI_FAILURE(rv)) {
		sc->sc_model = false;
		return;
	}

	sc->sc_model = true;

	/*
	 * If both the new and the old methods are present, prefer
	 * the old one; GGRP/GITM may not be functional in this case.
	 */
	rv = AcpiGetHandle(sc->sc_node->ad_handle, "FSIF", &tmp);

	if (ACPI_FAILURE(rv))
		return;

	rv = AcpiGetHandle(sc->sc_node->ad_handle, "TSIF", &tmp);

	if (ACPI_FAILURE(rv))
		return;

	rv = AcpiGetHandle(sc->sc_node->ad_handle, "VSIF", &tmp);

	if (ACPI_FAILURE(rv))
		return;

	rv = AcpiGetHandle(sc->sc_node->ad_handle, "RFAN", &tmp);

	if (ACPI_FAILURE(rv))
		return;

	rv = AcpiGetHandle(sc->sc_node->ad_handle, "RTMP", &tmp);

	if (ACPI_FAILURE(rv))
		return;

	rv = AcpiGetHandle(sc->sc_node->ad_handle, "RVLT", &tmp);

	if (ACPI_FAILURE(rv))
		return;

	sc->sc_model = false;
}

static void
aibs_init_new(device_t self)
{
	struct aibs_softc *sc = device_private(self);
	ACPI_OBJECT_LIST arg;
	ACPI_OBJECT id, *obj;
	ACPI_BUFFER buf;
	ACPI_STATUS rv;
	uint32_t i, n;

	arg.Count = 1;
	arg.Pointer = &id;

	id.Type = ACPI_TYPE_INTEGER;
	id.Integer.Value = AIBS_MUX_HWMON;

	buf.Pointer = NULL;
	buf.Length = ACPI_ALLOCATE_LOCAL_BUFFER;

	rv = AcpiEvaluateObject(sc->sc_node->ad_handle, "GGRP", &arg, &buf);

	if (ACPI_FAILURE(rv))
		goto out;

	obj = buf.Pointer;

	if (obj->Type != ACPI_TYPE_PACKAGE) {
		rv = AE_TYPE;
		goto out;
	}

	if (obj->Package.Count > UINT32_MAX) {
		rv = AE_AML_NUMERIC_OVERFLOW;
		goto out;
	}

	n = obj->Package.Count;

	if (n == 0) {
		rv = AE_NOT_EXIST;
		goto out;
	}

	for (i = 0; i < n; i++)
		aibs_sensor_add(self, &obj->Package.Elements[i]);

out:
	if (buf.Pointer != NULL)
		ACPI_FREE(buf.Pointer);

	if (ACPI_FAILURE(rv)) {

		aprint_error_dev(self, "failed to evaluate "
		    "GGRP: %s\n", AcpiFormatException(rv));
	}
}

static void
aibs_init_old(device_t self, int type)
{
	struct aibs_softc *sc = device_private(self);
	char path[] = "?SIF";
	ACPI_OBJECT *elm, *obj;
	ACPI_BUFFER buf;
	ACPI_STATUS rv;
	uint32_t i, n;

	switch (type) {

	case AIBS_TYPE_FAN:
		path[0] = 'F';
		break;

	case AIBS_TYPE_TEMP:
		path[0] = 'T';
		break;

	case AIBS_TYPE_VOLT:
		path[0] = 'V';
		break;

	default:
		return;
	}

	rv = acpi_eval_struct(sc->sc_node->ad_handle, path, &buf);

	if (ACPI_FAILURE(rv))
		goto out;

	obj = buf.Pointer;

	if (obj->Type != ACPI_TYPE_PACKAGE) {
		rv = AE_TYPE;
		goto out;
	}

	elm = obj->Package.Elements;

	if (elm[0].Type != ACPI_TYPE_INTEGER) {
		rv = AE_TYPE;
		goto out;
	}

	if (elm[0].Integer.Value > UINT32_MAX) {
		rv = AE_AML_NUMERIC_OVERFLOW;
		goto out;
	}

	n = elm[0].Integer.Value;

	if (n == 0) {
		rv = AE_NOT_EXIST;
		goto out;
	}

	if (obj->Package.Count - 1 != n) {
		rv = AE_BAD_VALUE;
		goto out;
	}

	for (i = 1; i < obj->Package.Count; i++) {

		if (elm[i].Type != ACPI_TYPE_PACKAGE)
			continue;

		aibs_sensor_add(self, &elm[i]);
	}

out:
	if (buf.Pointer != NULL)
		ACPI_FREE(buf.Pointer);

	if (ACPI_FAILURE(rv)) {

		aprint_error_dev(self, "failed to evaluate "
		    "%s: %s\n", path, AcpiFormatException(rv));
	}
}

static void
aibs_sensor_add(device_t self, ACPI_OBJECT *obj)
{
	struct aibs_softc *sc = device_private(self);
	struct aibs_sensor *as;
	int ena, len, lhi, llo;
	const char *name;
	ACPI_STATUS rv;

	as = NULL;
	rv = AE_OK;

	if (obj->Type != ACPI_TYPE_PACKAGE) {
		rv = AE_TYPE;
		goto out;
	}

	/*
	 * The known formats are:
	 *
	 *	index		type		old		new
	 *	-----		----		---		---
	 *	0		integer		flags		flags
	 *	1		string		name		name
	 *	2		integer		limit1		unknown
	 *	3		integer		limit2		unknown
	 *	4		integer		enable		limit1
	 *	5		integer		-		limit2
	 *	6		integer		-		enable
	 */
	if (sc->sc_model != false) {
		len = 7;
		llo = 4;
		lhi = 5;
		ena = 6;
	} else {
		len = 5;
		llo = 2;
		lhi = 3;
		ena = 4;
	}

	if (obj->Package.Count != (uint32_t)len) {
		rv = AE_LIMIT;
		goto out;
	}

	if (obj->Package.Elements[0].Type != ACPI_TYPE_INTEGER ||
	    obj->Package.Elements[1].Type != ACPI_TYPE_STRING ||
	    obj->Package.Elements[llo].Type != ACPI_TYPE_INTEGER ||
	    obj->Package.Elements[lhi].Type != ACPI_TYPE_INTEGER ||
	    obj->Package.Elements[ena].Type != ACPI_TYPE_INTEGER) {
		rv = AE_TYPE;
		goto out;
	}

	as = kmem_zalloc(sizeof(*as), KM_SLEEP);

	name = obj->Package.Elements[1].String.Pointer;

	as->as_type = obj->Package.Elements[0].Integer.Value;
	as->as_liml = obj->Package.Elements[llo].Integer.Value;
	as->as_limh = obj->Package.Elements[lhi].Integer.Value;

	if (sc->sc_model != false)
		as->as_limh += as->as_liml;	/* A range in the new model. */

	as->as_sensor.state = ENVSYS_SINVALID;

	switch (AIBS_TYPE(as->as_type)) {

	case AIBS_TYPE_FAN:
		as->as_sensor.units = ENVSYS_SFANRPM;
		as->as_sensor.flags = ENVSYS_FMONLIMITS | ENVSYS_FHAS_ENTROPY;
		break;

	case AIBS_TYPE_TEMP:
		as->as_sensor.units = ENVSYS_STEMP;
		as->as_sensor.flags = ENVSYS_FMONLIMITS | ENVSYS_FHAS_ENTROPY;
		break;

	case AIBS_TYPE_VOLT:
		as->as_sensor.units = ENVSYS_SVOLTS_DC;
		as->as_sensor.flags = ENVSYS_FMONLIMITS | ENVSYS_FHAS_ENTROPY;
		break;

	default:
		rv = AE_TYPE;
		goto out;
	}

	(void)strlcpy(as->as_sensor.desc, name, sizeof(as->as_sensor.desc));

	if (sysmon_envsys_sensor_attach(sc->sc_sme, &as->as_sensor) != 0) {
		rv = AE_AML_INTERNAL;
		goto out;
	}

	SIMPLEQ_INSERT_TAIL(&sc->as_head, as, as_list);

out:
	if (ACPI_FAILURE(rv)) {

		if (as != NULL)
			kmem_free(as, sizeof(*as));

		aprint_error_dev(self, "failed to add "
		    "sensor: %s\n",  AcpiFormatException(rv));
	}
}

static bool
aibs_sensor_value(device_t self, struct aibs_sensor *as, uint64_t *val)
{
	struct aibs_softc *sc = device_private(self);
	uint32_t type, *ret, cmb[3];
	ACPI_OBJECT_LIST arg;
	ACPI_OBJECT cmi, tmp;
	ACPI_OBJECT *obj;
	ACPI_BUFFER buf;
	ACPI_STATUS rv;
	const char *path;

	if (sc->sc_model != false) {

		path = "GITM";

		cmb[0] = as->as_type;
		cmb[1] = 0;
		cmb[2] = 0;

		arg.Count = 1;
		arg.Pointer = &tmp;

		tmp.Buffer.Length = sizeof(cmb);
		tmp.Buffer.Pointer = (uint8_t *)cmb;
		tmp.Type = type = ACPI_TYPE_BUFFER;

	} else {

		arg.Count = 1;
		arg.Pointer = &cmi;

		cmi.Integer.Value = as->as_type;
		cmi.Type = type = ACPI_TYPE_INTEGER;

		switch (AIBS_TYPE(as->as_type)) {

		case AIBS_TYPE_FAN:
			path = "RFAN";
			break;

		case AIBS_TYPE_TEMP:
			path = "RTMP";
			break;

		case AIBS_TYPE_VOLT:
			path = "RVLT";
			break;

		default:
			return false;
		}
	}

	buf.Pointer = NULL;
	buf.Length = ACPI_ALLOCATE_LOCAL_BUFFER;

	rv = AcpiEvaluateObject(sc->sc_node->ad_handle, path, &arg, &buf);

	if (ACPI_FAILURE(rv))
		goto out;

	obj = buf.Pointer;

	if (obj->Type != type) {
		rv = AE_TYPE;
		goto out;
	}

	if (sc->sc_model != true)
		*val = obj->Integer.Value;
	else {
		/*
		 * The return buffer contains at least:
		 *
		 *	uint32_t buf[0]	 flags
		 *	uint32_t buf[1]	 return value
		 *	uint8_t  buf[2-] unknown
		 */
		if (obj->Buffer.Length < 8) {
			rv = AE_BUFFER_OVERFLOW;
			goto out;
		}

		ret = (uint32_t *)obj->Buffer.Pointer;

		if (ret[0] == 0) {
			rv = AE_BAD_VALUE;
			goto out;
		}

		*val = ret[1];
	}

out:
	if (buf.Pointer != NULL)
		ACPI_FREE(buf.Pointer);

	if (ACPI_FAILURE(rv)) {

		aprint_error_dev(self, "failed to evaluate "
		    "%s: %s\n", path, AcpiFormatException(rv));

		return false;
	}

	return true;
}

static void
aibs_sensor_refresh(struct sysmon_envsys *sme, envsys_data_t *edata)
{
	struct aibs_softc *sc = sme->sme_cookie;
	struct aibs_sensor *tmp, *as = NULL;
	envsys_data_t *s = edata;
	uint64_t val = 0;

	SIMPLEQ_FOREACH(tmp, &sc->as_head, as_list) {

		if (tmp->as_sensor.sensor == s->sensor) {
			as = tmp;
			break;
		}
	}

	if (as == NULL) {
		aprint_debug_dev(sc->sc_dev, "failed to find sensor\n");
		return;
	}

	as->as_sensor.state = ENVSYS_SINVALID;
	as->as_sensor.flags |= ENVSYS_FMONNOTSUPP;

	if (aibs_sensor_value(sc->sc_dev, as, &val) != true)
		return;

	switch (as->as_sensor.units) {

	case ENVSYS_SFANRPM:
		as->as_sensor.value_cur = val;
		break;

	case ENVSYS_STEMP:

		if (val == 0)
			return;

		as->as_sensor.value_cur = val * 100 * 1000 + 273150000;
		break;

	case ENVSYS_SVOLTS_DC:
		as->as_sensor.value_cur = val * 1000;
		break;

	default:
		return;
	}

	as->as_sensor.state = ENVSYS_SVALID;
	as->as_sensor.flags &= ~ENVSYS_FMONNOTSUPP;
}

static void
aibs_sensor_limits(struct sysmon_envsys *sme, envsys_data_t *edata,
    sysmon_envsys_lim_t *limits, uint32_t *props)
{
	struct aibs_softc *sc = sme->sme_cookie;
	struct aibs_sensor *tmp, *as = NULL;
	sysmon_envsys_lim_t *lim = limits;
	envsys_data_t *s = edata;

	SIMPLEQ_FOREACH(tmp, &sc->as_head, as_list) {

		if (tmp->as_sensor.sensor == s->sensor) {
			as = tmp;
			break;
		}
	}

	if (as == NULL) {
		aprint_debug_dev(sc->sc_dev, "failed to find sensor\n");
		return;
	}

	switch (as->as_sensor.units) {

	case ENVSYS_SFANRPM:

		/*
		 * Some boards have strange limits for fans.
		 */
		if (as->as_liml == 0) {
			lim->sel_warnmin = as->as_limh;
			*props = PROP_WARNMIN;

		} else {
			lim->sel_warnmin = as->as_liml;
			lim->sel_warnmax = as->as_limh;
			*props = PROP_WARNMIN | PROP_WARNMAX;
		}

		break;

	case ENVSYS_STEMP:
		lim->sel_critmax = as->as_limh * 100 * 1000 + 273150000;
		lim->sel_warnmax = as->as_liml * 100 * 1000 + 273150000;

		*props = PROP_CRITMAX | PROP_WARNMAX;
		break;

	case ENVSYS_SVOLTS_DC:
		lim->sel_critmin = as->as_liml * 1000;
		lim->sel_critmax = as->as_limh * 1000;
		*props = PROP_CRITMIN | PROP_CRITMAX;
		break;

	default:
		return;
	}
}

MODULE(MODULE_CLASS_DRIVER, aibs, "sysmon_envsys");

#ifdef _MODULE
#include "ioconf.c"
#endif

static int
aibs_modcmd(modcmd_t cmd, void *aux)
{
	int rv = 0;

	switch (cmd) {

	case MODULE_CMD_INIT:

#ifdef _MODULE
		rv = config_init_component(cfdriver_ioconf_aibs,
		    cfattach_ioconf_aibs, cfdata_ioconf_aibs);
#endif
		break;

	case MODULE_CMD_FINI:

#ifdef _MODULE
		rv = config_fini_component(cfdriver_ioconf_aibs,
		    cfattach_ioconf_aibs, cfdata_ioconf_aibs);
#endif
		break;

	default:
		rv = ENOTTY;
	}

	return rv;
}
