/* SPDX-License-Identifier: GPL-2.0-only */
/*
 * Pinephone keyboard power manager driver.
 *
 * Ondrej Jirman <megi@xff.cz>
 */

//#define DEBUG

#include <linux/wait.h>
#include <linux/device.h>
#include <linux/debugfs.h>
#include <linux/kfifo.h>
#include <linux/module.h>
#include <linux/poll.h>
#include <linux/spinlock.h>
#include <linux/of.h>
#include <linux/of_device.h>
#include <linux/platform_device.h>
#include <linux/gpio/consumer.h>
#include <linux/regulator/consumer.h>
#include <linux/delay.h>
#include <linux/leds.h>
#include <linux/slab.h>
#include <linux/time.h>
#include <linux/reboot.h>
#include <linux/power_supply.h>

#define DRIVER_NAME "ppkb-power"

enum {
	KBPWR_F_DISABLED,
	KBPWR_F_EMERGENCY_SHUTDOWN,
};

enum {
	KBPWR_LED_TRIGGER_KB_VOUT_ON,
	KBPWR_LED_TRIGGER_KB_VIN_PRESENT,
	KBPWR_LED_TRIGGER_KB_OFFLINE,
	KBPWR_LED_TRIGGER_CAPACITY,

	KBPWR_LED_TRIGGER_COUNT,
};

static const char *trig_names[] = {
	"kbpwr-kb-vout-on",
	"kbpwr-kb-vin-present",
	"kbpwr-kb-offline",
	"kbpwr-capacity",
};

struct kbpwr_status {
	int kb_cap; // capacity in %  (when -1, keyboard charger is
		    // not accessible, and no kb_* properties are valid)
	int kb_cur; // current + charging, - discharging
	int kb_vol; // voltage at the battery terminals
	int kb_vol_ocv; // OCV voltage
	int kb_chg_behavior; // (writable) kb battery charger auto=0/inhibited=1
	int kb_cal; // (writable) battery internal resistance calibration value in mOhm
	int kb_out; // 5V output enabled/disabled
	int kb_in; // supply to VIN is connected
	int kb_max_uwh; // kb battery uWh total capacity

	int ph_cap; // capacity in %
	int ph_cur; // current (direction determined by ph_chg_status)
	int ph_vol; // voltage at the battery terminals
	int ph_chg_status; // POWER_SUPPLY_STATUS_CHARGING = charging,
			   // other statuses = discharging  (use it to
			   // interpret meaning of abs(ph_cur))
	int ph_chg_cur_limit; // (writable) max charging current for phone battery
	int ph_chg_behavior; // (writable) phone charger auto=0/inhibited=1

	int ph_inp_present; // phone USB supply input is present
	int ph_inp_en; // (writable) phone USB supply input is used for powering the phone
	int ph_inp_limit; // (writable) input current limit on phone's VBUS
	int ph_max_uwh; // phone battery uWh total capacity

	ktime_t ts;
};

// constants based on device type
struct kbpwr_machine {
	int inp_limit_normal;
	int inp_limit_mid;
	int inp_limit_high;
	int chg_limit_high;
	int chg_limit_low;
	bool (*has_prop)(const char* name);
};

struct kbpwr_dev {
	struct device *dev;
	struct dentry *debug_root;

	unsigned long flags[1];
	struct mutex lock;

	struct power_supply *phone_battery;
	struct power_supply *phone_usb;

	struct power_supply *kb_battery;
	struct power_supply *kb_boost;
	struct power_supply *kb_usb;

	struct workqueue_struct *wq;
	struct delayed_work work;

	struct led_trigger trigger[KBPWR_LED_TRIGGER_COUNT];

	struct kbpwr_status last_status;
	ktime_t ph_low_until;
	ktime_t shutdown_after;

	// total state of the battery system
	int capacity_total_uwh;
	int capacity_uwh;
	int capacity_pct;
	int power_uw;
	int time_left;

	// kb rint calibration
	ktime_t rint_valid_until;
	int kb_vol_now, kb_cur_now, rint;

	const struct kbpwr_machine* mach;
};

static bool kbpwr_has_prop_pp(const char* name)
{
	return true;
}

static bool kbpwr_has_prop_ppp(const char* name)
{
	return strcmp(name, "ph_inp_en");
}

static const struct kbpwr_machine kbpwr_pp = {
	.inp_limit_normal = 500000,
	.inp_limit_mid = 1000000,
	.inp_limit_high = 1500000,
	.chg_limit_high = 1200000,
	.chg_limit_low = 200000,
	.has_prop = kbpwr_has_prop_pp,
};

static const struct kbpwr_machine kbpwr_ppp = {
	.inp_limit_normal = 450000,
	.inp_limit_mid = 850000,
	.inp_limit_high = 1500000,
	.chg_limit_high = 1200000,
	.chg_limit_low = 1000000,
	.has_prop = kbpwr_has_prop_ppp,
};

static void kbpwr_uevent(struct kbpwr_dev *kbpwr, const char* name)
{
	char *env[] = {
		"DRIVER=" DRIVER_NAME,
		NULL,
		NULL,
	};

	env[1] = kasprintf(GFP_KERNEL, "POWER_EVENT=%s", name);
	if (!env[1])
		return;

	kobject_uevent_env(&kbpwr->dev->kobj, KOBJ_CHANGE, env);

	kfree(env[1]);
}

#define STATUS_PROP(member, sup, sup_prop) \
	{ &s->member, #member, kbpwr->sup, sup_prop, },

static int kbpwr_snaphost(struct kbpwr_dev *kbpwr, struct kbpwr_status* s)
{
	bool kb_fail = false;
	int i, j, ret;
	struct {
		int *out;
		const char* name;
		struct power_supply *psy;
		enum power_supply_property prop;
	} props[] = {
		STATUS_PROP(kb_cap, kb_battery, POWER_SUPPLY_PROP_CAPACITY)
		STATUS_PROP(kb_cur, kb_battery, POWER_SUPPLY_PROP_CURRENT_NOW)
		STATUS_PROP(kb_vol, kb_battery, POWER_SUPPLY_PROP_VOLTAGE_NOW)
		STATUS_PROP(kb_vol_ocv, kb_battery, POWER_SUPPLY_PROP_VOLTAGE_OCV)
		STATUS_PROP(kb_cal, kb_battery, POWER_SUPPLY_PROP_CALIBRATE)
		STATUS_PROP(kb_chg_behavior, kb_battery, POWER_SUPPLY_PROP_CHARGE_BEHAVIOUR)
		STATUS_PROP(kb_max_uwh, kb_battery, POWER_SUPPLY_PROP_ENERGY_FULL_DESIGN)

		STATUS_PROP(kb_out, kb_boost, POWER_SUPPLY_PROP_ONLINE)

		STATUS_PROP(kb_in, kb_usb, POWER_SUPPLY_PROP_PRESENT)

		STATUS_PROP(ph_cap, phone_battery, POWER_SUPPLY_PROP_CAPACITY)
		STATUS_PROP(ph_cur, phone_battery, POWER_SUPPLY_PROP_CURRENT_NOW)
		STATUS_PROP(ph_vol, phone_battery, POWER_SUPPLY_PROP_VOLTAGE_NOW)
		STATUS_PROP(ph_chg_status, phone_battery, POWER_SUPPLY_PROP_STATUS)
		STATUS_PROP(ph_chg_cur_limit, phone_battery, POWER_SUPPLY_PROP_CONSTANT_CHARGE_CURRENT)
		STATUS_PROP(ph_chg_behavior, phone_battery, POWER_SUPPLY_PROP_CHARGE_BEHAVIOUR)
		STATUS_PROP(ph_max_uwh, phone_battery, POWER_SUPPLY_PROP_ENERGY_FULL_DESIGN)

		STATUS_PROP(ph_inp_present, phone_usb, POWER_SUPPLY_PROP_PRESENT)
		STATUS_PROP(ph_inp_en, phone_usb, POWER_SUPPLY_PROP_ONLINE)
		STATUS_PROP(ph_inp_limit, phone_usb, POWER_SUPPLY_PROP_INPUT_CURRENT_LIMIT)
	};

	dev_dbg(kbpwr->dev, "snapshot:\n");

	for (i = 0; i < ARRAY_SIZE(props); i++) {
		union power_supply_propval val = {0,};

		if (!kbpwr->mach->has_prop(props[i].name)) {
			*props[i].out = -1;
			continue;
		}

		/*
		 * Skip reading kb_* properties after the first failure.
		 */
		if (strstarts(props[i].name, "kb_") && kb_fail)
			continue;

		ret = power_supply_get_property(props[i].psy, props[i].prop, &val);
		if (ret) {
			/*
			 * Failure to read kb_* properties is expected and
			 * common. When it happens, we clear all the kb_
			 * properties, so that algorithm behaves as if keyboard
			 * charger is sleeping.
			 */
			if (strstarts(props[i].name, "kb_")) {
				kb_fail = true;
				for (j = 0; j < ARRAY_SIZE(props); j++)
					if (strstarts(props[j].name, "kb_"))
						*props[j].out = -1;
				continue;
			} else {
				/*
				 * Other properties should never fail to read,
				 * so make that a fatal issue.
				 */
				dev_err(kbpwr->dev, "Can't read %s (%d)\n", props[i].name, ret);
				return ret;
			}
		}

		*props[i].out = val.intval;

		dev_dbg(kbpwr->dev, "  %s = %d\n", props[i].name, val.intval);
	}

	s->ts = ktime_get();

	return 0;
}

#define UPDATE_PROP(member, sup, sup_prop) \
	{ &prev->member, &cur->member, #member, kbpwr->sup, sup_prop, },

static int kbpwr_update(struct kbpwr_dev *kbpwr,
			struct kbpwr_status* prev,
			struct kbpwr_status* cur)
{
	bool updated = false;
	int i, ret;
	struct {
		int *cmp;
		int *out;
		const char* name;
		struct power_supply *psy;
		enum power_supply_property prop;
	} props[] = {
		UPDATE_PROP(kb_chg_behavior, kb_battery, POWER_SUPPLY_PROP_CHARGE_BEHAVIOUR)
		UPDATE_PROP(kb_cal, kb_battery, POWER_SUPPLY_PROP_CALIBRATE)
		UPDATE_PROP(ph_chg_cur_limit, phone_battery, POWER_SUPPLY_PROP_CONSTANT_CHARGE_CURRENT)
		UPDATE_PROP(ph_chg_behavior, phone_battery, POWER_SUPPLY_PROP_CHARGE_BEHAVIOUR)
		UPDATE_PROP(ph_inp_en, phone_usb, POWER_SUPPLY_PROP_ONLINE)
		UPDATE_PROP(ph_inp_limit, phone_usb, POWER_SUPPLY_PROP_INPUT_CURRENT_LIMIT)
	};

	// check if there are changes
	for (i = 0; i < ARRAY_SIZE(props); i++) {
		if (!kbpwr->mach->has_prop(props[i].name))
			continue;

		if (*props[i].out != *props[i].cmp) {
			dev_dbg(kbpwr->dev, "updating:\n");
			break;
		}
	}

	for (i = 0; i < ARRAY_SIZE(props); i++) {
		union power_supply_propval val = {0,};

		if (!kbpwr->mach->has_prop(props[i].name))
			continue;

		if (*props[i].out == *props[i].cmp)
			continue;

		val.intval = *props[i].out;

		/*
		 * Error handling here is "do as much as we can". Any write
		 * issue will hopefully be corrected on the next iteration
		 * of the polling algorithm.
		 */
		ret = power_supply_set_property(props[i].psy, props[i].prop, &val);
		if (ret) {
			dev_warn(kbpwr->dev, "Can't write %s (%d)\n", props[i].name, ret);
			continue;
		}

		updated = true;
		dev_dbg(kbpwr->dev, "  %s = %d\n", props[i].name, val.intval);
	}

	if (updated)
		kbpwr_uevent(kbpwr, "update");

	return 0;
}

static int kbpwr_handle_critical(struct kbpwr_dev *kbpwr)
{
	kbpwr_uevent(kbpwr, "critical");

	if (!kbpwr->shutdown_after)
		kbpwr->shutdown_after = ktime_add_ms(ktime_get(), 60000);

	if (ktime_after(ktime_get(), kbpwr->shutdown_after)) {
		dev_emerg(kbpwr->dev,
			  "critically low capacity reached\n");

		//hw_protection_shutdown("Critical capacity", 30000);
		return true;
	}

	return false;
}

static void kbpwr_work(struct work_struct *work)
{
	struct kbpwr_dev *kbpwr = container_of(work, struct kbpwr_dev, work.work);
	unsigned long delay_on, delay_off;
	struct kbpwr_status cur, upd, prev;
	int ret;

	if (test_bit(KBPWR_F_DISABLED, kbpwr->flags))
		return;

	mutex_lock(&kbpwr->lock);

	ret = kbpwr_snaphost(kbpwr, &cur);
	if (ret)
		goto out_try_later;

	prev = kbpwr->last_status.ts ? kbpwr->last_status : cur;
	upd = cur;
	kbpwr->last_status = cur;

	/*
	 * We calculate keyboard battery internal resistance based on captured
	 * keyboard current/voltage at two differnt times after the current
	 * changes by a largish degree.
	 */
	if (!kbpwr->rint_valid_until ||
	    ktime_after(ktime_get(), kbpwr->rint_valid_until)) {
		kbpwr->kb_vol_now = cur.kb_vol;
		kbpwr->kb_cur_now = cur.kb_cur;
		kbpwr->rint_valid_until = ktime_add_ms(ktime_get(),
						       5 * 60 * 1000);
	} else {
		s64 diff_vol = cur.kb_vol - kbpwr->kb_vol_now;
		s64 diff_cur = cur.kb_cur - kbpwr->kb_cur_now;

		if (abs(diff_cur) > 150000) {
			s64 rint = diff_vol * 1000 / diff_cur;
			if (rint > 30 && rint < 1000) {
				dev_warn(kbpwr->dev,
					 "calibrating rint=%lld mOhm\n", rint);
				kbpwr->rint = rint;
				upd.kb_cal = rint;
			}

			kbpwr->kb_vol_now = cur.kb_vol;
			kbpwr->kb_cur_now = cur.kb_cur;
			kbpwr->rint_valid_until = ktime_add_ms(ktime_get(),
							       5 * 60 * 1000);
		}
	}

	/*
	 * The algorithm here tries to ensure that:
	 *
	 * When the power supply is plugged into the keyboard:
	 *
	 * 1) Phone's internal battery is charged as fast as possible
	 * 2) When the internal battery is fully charged, keyboard battery starts charging, while 
	 *    still supplying enough power to the phone so that internal battery doesn't start
	 *    discharging, until both batteries are fully charged.
	 *
	 * When it's unplugged:
	 *
	 * 1) Keyboard battery discharges first, preserving phone battery
	 *    as much as possible.
	 * 2) Phone battery starts discharging after the keyboard battery
	 *    is emptied.
	 *
	 * There are a few corner cases handled:
	 *
	 * - It's not a great thing to drain the batteries completely, since Pinephone Pro
	 *   can't recover from this state gracefully, and keyboard also has some issues
	 *   with it, requiring prolonged trickle charging, etc.
	 *   - The driver tries to keep some residual charge in both batteries.
	 * - On phone power off, keyboard charger output is turned off, so that:
	 *   - Pinephone Pro can be turned off (it can't with voltage present on VBUS)
	 *   - Keyboard battery will not keep charging the phone for no reason.
	 *   - This is only done when the keyboard battery is not plugged in to a power supply.
	 * - On suspend/resume:
	 *   - Phone battery charger and keyboard battery output are turned off.
	 *
	 * LED trigger:
	 *
	 * The driver provides a LED trigger to communicate to the user that keyboard
	 * power button should be pressed to enable the keyboard charger.
	 */

	/* check and update the situation */

	if (cur.kb_cap < 0) {
		// keyboard charger is sleeping or no keyboard is detected

                //XXX: check for lack of keyboard

		// restore sane defaults (only sensible if phone is in the
		// keyboard)
		upd.ph_chg_behavior = POWER_SUPPLY_CHARGE_BEHAVIOUR_AUTO;
		upd.ph_chg_cur_limit = kbpwr->mach->chg_limit_high;
		upd.ph_inp_limit = kbpwr->mach->inp_limit_normal;
	} else {
		// keyboard is connected to the phone
		bool kb_in_change = cur.kb_in != prev.kb_in;

		if (cur.kb_in) {
			// keyboard is connected to USB PSU (we are charging)
			bool kb_chg, ph_chg;

			/*
			 * Make ph_low comparison stick for 5 minutes in low
			 * postition, once it crosses the threshold, unless
			 * kb_in just changed.
			 */
			bool ph_low = kb_in_change ? false : ktime_before(ktime_get(), kbpwr->ph_low_until);
                        if (!ph_low) {
				ph_low = cur.ph_cap < 80;
				if (ph_low)
					kbpwr->ph_low_until = ktime_add_ms(ktime_get(), 5 * 60000);
				else
					kbpwr->ph_low_until = 0;
			}

			kb_chg = !ph_low;
			ph_chg = ph_low || cur.kb_cap > 90;

			upd.kb_out = 1;
			upd.ph_inp_en = 1;

			if (ph_chg) {
				// charge the phone
				upd.ph_chg_behavior = POWER_SUPPLY_CHARGE_BEHAVIOUR_AUTO;
				upd.ph_chg_cur_limit = kbpwr->mach->chg_limit_high;
				upd.ph_inp_limit = kbpwr->mach->inp_limit_high;
			} else {
				// supply the phone, but don't charge it
				upd.ph_chg_behavior = POWER_SUPPLY_CHARGE_BEHAVIOUR_INHIBIT_CHARGE;
				upd.ph_chg_cur_limit = kbpwr->mach->chg_limit_low;
				upd.ph_inp_limit = kbpwr->mach->inp_limit_mid;
			}

			// charge the keyboard when the KB battery is low or
			// phone battery is high
			if (kb_chg) {
				upd.kb_chg_behavior = POWER_SUPPLY_CHARGE_BEHAVIOUR_AUTO;
			} else {
				upd.kb_chg_behavior = POWER_SUPPLY_CHARGE_BEHAVIOUR_INHIBIT_CHARGE;
			}
		} else {
			// keyboard is mobile (we're discharging)
			//
			// Generally we want to avoid shifting charge between the phone
			// and keyboard batteries, so we disable the phone charger in this
			// situation and set high input current limit, so that the phone
			// is primarily supplied from the keyboard
			bool kb_low = cur.kb_vol < 3100000;

			/*
			 * Make ph_low comparison stick for 5 minutes in low
			 * postition, once it crosses the threshold, unless
			 * kb_in just changed.
			 */
			bool ph_low = kb_in_change ? false : ktime_before(ktime_get(), kbpwr->ph_low_until);
                        if (!ph_low) {
				ph_low = cur.ph_cap < 10;
				if (ph_low)
					kbpwr->ph_low_until = ktime_add_ms(ktime_get(), 5 * 60000);
				else
					kbpwr->ph_low_until = 0;
			}

			//                 kb_out
			// kb_low ph_low | ph_inp_en  ph_chg
			// 0      0      |    1         0
			// 0      1      |    1         1
			// 1      1      |    1         1
			// 1      0      |    0         0

			upd.ph_chg_behavior = POWER_SUPPLY_CHARGE_BEHAVIOUR_INHIBIT_CHARGE;
			upd.ph_inp_limit = kbpwr->mach->inp_limit_high;
			upd.ph_inp_en = 1;
			upd.kb_out = 1;

			// charge phone battery a little if it's charge is too low (we
			// need to keep the phone battery somewhat charged at all times,
			// if possible)
			if (ph_low) {
				upd.ph_chg_behavior = POWER_SUPPLY_CHARGE_BEHAVIOUR_AUTO;
			}

			if (kb_low && !ph_low) {
				upd.ph_inp_en = 0;
				upd.kb_out = 0;
				upd.ph_inp_limit = kbpwr->mach->inp_limit_normal;
			}
		}
	}

	kbpwr->capacity_total_uwh = cur.ph_max_uwh +
		(cur.kb_cap >= 0 ? cur.kb_max_uwh : 0);
	kbpwr->capacity_uwh = cur.ph_max_uwh * cur.ph_cap / 100 +
		(cur.kb_cap >= 0 ? cur.kb_max_uwh * cur.kb_cap / 100 : 0);
	kbpwr->capacity_pct = kbpwr->capacity_uwh /
		(kbpwr->capacity_total_uwh / 100);

	kbpwr->power_uw = (cur.ph_cur / 1000) * (cur.ph_vol / 1000);
	if (cur.kb_cap >= 0)
		kbpwr->power_uw += (cur.kb_cur / 1000) * (cur.kb_vol / 1000);

	kbpwr->time_left = kbpwr->power_uw > 0 ?
		kbpwr->capacity_total_uwh - kbpwr->capacity_uwh :
		kbpwr->capacity_uwh;
	kbpwr->time_left *= 60;
	kbpwr->time_left /= abs(kbpwr->power_uw);

	// critical shutdown handler

	if (kbpwr->power_uw < 0 && kbpwr->capacity_pct < 5) {
		if (kbpwr_handle_critical(kbpwr))
			goto out_unlock;
	} else {
		kbpwr->shutdown_after = 0;
	}

	// update LED triggers

	// capacity
	if (kbpwr->power_uw > 0) {
		if (kbpwr->capacity_pct > 95) {
			delay_on = 500; delay_off = 0;
		} else {
			delay_on = delay_off = 500;
		}
	} else if (kbpwr->capacity_pct < 5) {
		delay_on = delay_off = 100;
	} else if (kbpwr->capacity_pct < 10) {
		delay_on = 100; delay_off = 400;
	} else {
		delay_on = 0; delay_off = 100;
	}

	led_trigger_blink(&kbpwr->trigger[KBPWR_LED_TRIGGER_CAPACITY],
			  delay_on, delay_off);

	led_trigger_event(&kbpwr->trigger[KBPWR_LED_TRIGGER_KB_VOUT_ON],
			  cur.kb_out > 0 ? LED_FULL : LED_OFF);

	led_trigger_event(&kbpwr->trigger[KBPWR_LED_TRIGGER_KB_VIN_PRESENT],
			  cur.kb_in > 0 ? LED_FULL : LED_OFF);

	led_trigger_event(&kbpwr->trigger[KBPWR_LED_TRIGGER_KB_OFFLINE],
			  cur.kb_cap < 0 ? LED_FULL : LED_OFF);

	kbpwr_update(kbpwr, &cur, &upd);
	kbpwr_uevent(kbpwr, "refresh");

out_try_later:
	queue_delayed_work(kbpwr->wq, &kbpwr->work, msecs_to_jiffies(10000));
out_unlock:
	mutex_unlock(&kbpwr->lock);
}

static ssize_t help_show(struct device *dev,
			 struct device_attribute *attr, char *buf)
{
	return scnprintf(buf, PAGE_SIZE,
		"Pinephone Keyboard Power Manager\n"
		"================================\n"
		"disabled - enable/disable the power manager\n"
		"shutdown - enable/disable emergency shutdown on low capacity\n"
		"help     - this help file\n"
	);
}

static ssize_t disabled_store(struct device *dev,
			      struct device_attribute *attr,
			      const char *buf, size_t len)
{
	struct kbpwr_dev *kbpwr = platform_get_drvdata(to_platform_device(dev));
	bool val;
	int ret;

	ret = kstrtobool(buf, &val);
	if (ret)
		return ret;

	if (val) {
		set_bit(KBPWR_F_DISABLED, kbpwr->flags);
		cancel_delayed_work_sync(&kbpwr->work);
		kbpwr->ph_low_until = 0;
		kbpwr->shutdown_after = 0;
	} else {
		clear_bit(KBPWR_F_DISABLED, kbpwr->flags);
		queue_delayed_work(kbpwr->wq, &kbpwr->work, msecs_to_jiffies(1000));
		//XXX: do we want to put the system into some particular state?
	}

	return len;
}

static ssize_t disabled_show(struct device *dev,
			     struct device_attribute *attr, char *buf)
{
	struct kbpwr_dev *kbpwr = platform_get_drvdata(to_platform_device(dev));

	return scnprintf(buf, PAGE_SIZE, "%d\n",
			 !!test_bit(KBPWR_F_DISABLED, kbpwr->flags));
}

static ssize_t emergency_store(struct device *dev,
			       struct device_attribute *attr,
			       const char *buf, size_t len)
{
	struct kbpwr_dev *kbpwr = platform_get_drvdata(to_platform_device(dev));
	bool val;
	int ret;

	ret = kstrtobool(buf, &val);
	if (ret)
		return ret;

	if (val)
		set_bit(KBPWR_F_EMERGENCY_SHUTDOWN, kbpwr->flags);
	else
		clear_bit(KBPWR_F_EMERGENCY_SHUTDOWN, kbpwr->flags);

	return len;
}

static ssize_t emergency_show(struct device *dev,
			      struct device_attribute *attr, char *buf)
{
	struct kbpwr_dev *kbpwr = platform_get_drvdata(to_platform_device(dev));

	return scnprintf(buf, PAGE_SIZE, "%d\n",
			 !!test_bit(KBPWR_F_EMERGENCY_SHUTDOWN, kbpwr->flags));
}

static DEVICE_ATTR_RO(help);
static DEVICE_ATTR_RW(disabled);
static DEVICE_ATTR_RW(emergency);

static struct attribute *kbpwr_attrs[] = {
	&dev_attr_help.attr,
	&dev_attr_disabled.attr,
	&dev_attr_emergency.attr,
	NULL,
};

static const struct attribute_group kbpwr_group = {
	.attrs = kbpwr_attrs,
};

static void devm_power_supply_put(struct device *dev, void *res)
{
	struct power_supply **psy = res;

	power_supply_put(*psy);
}

struct power_supply *devm_power_supply_get_by_name(struct device *dev,
						   const char *name)
{
	struct power_supply **ptr, *psy;

	ptr = devres_alloc(devm_power_supply_put, sizeof(*ptr), GFP_KERNEL);
	if (!ptr)
		return ERR_PTR(-ENOMEM);

	psy = power_supply_get_by_name(name);
	if (IS_ERR_OR_NULL(psy)) {
		devres_free(ptr);
	} else {
		*ptr = psy;
		devres_add(dev, ptr);
	}

	return psy;
}

static int kbpwr_status_show(struct seq_file *s, void *data)
{
	struct kbpwr_dev *kbpwr = s->private;
	struct kbpwr_status st;

	mutex_lock(&kbpwr->lock);
	st = kbpwr->last_status;
	mutex_unlock(&kbpwr->lock);

	seq_printf(s, "{\n");

#define SHOW_PROP(name) \
	seq_printf(s, "\t\"" #name "\": %d,\n", st.name)

	SHOW_PROP(kb_cap);
	SHOW_PROP(kb_cur);
	SHOW_PROP(kb_vol);
	SHOW_PROP(kb_vol_ocv);
	SHOW_PROP(kb_chg_behavior);
	SHOW_PROP(kb_cal);
	SHOW_PROP(kb_out);
	SHOW_PROP(kb_in);
	SHOW_PROP(kb_max_uwh);
	SHOW_PROP(ph_cap);
	SHOW_PROP(ph_cur);
	SHOW_PROP(ph_vol);
	SHOW_PROP(ph_chg_status);
	SHOW_PROP(ph_chg_cur_limit);
	SHOW_PROP(ph_chg_behavior);
	SHOW_PROP(ph_inp_present);
	SHOW_PROP(ph_inp_en);
	SHOW_PROP(ph_inp_limit);
	SHOW_PROP(ph_max_uwh);

	seq_printf(s, "\t\"disabled\": %s,\n",
		   test_bit(KBPWR_F_DISABLED, kbpwr->flags) ? "true" : "false");
	seq_printf(s, "\t\"emergency_shutdown_enable\": %s,\n",
		   test_bit(KBPWR_F_EMERGENCY_SHUTDOWN, kbpwr->flags) ? "true" : "false");

	seq_printf(s, "\t\"capacity_total_uwh\": %d,\n", kbpwr->capacity_total_uwh);
	seq_printf(s, "\t\"capacity_uwh\": %d,\n", kbpwr->capacity_uwh);
	seq_printf(s, "\t\"capacity_pct\": %d,\n", kbpwr->capacity_pct);
	seq_printf(s, "\t\"power_uw\": %d,\n", kbpwr->power_uw);
	seq_printf(s, "\t\"time_left\": %d,\n", kbpwr->time_left);

	seq_printf(s, "\t\"ts\": %lld\n", st.ts / 1000000);

	seq_printf(s, "}\n");

	return 0;
}
DEFINE_SHOW_ATTRIBUTE(kbpwr_status);

static int kbpwr_probe(struct platform_device *pdev)
{
	struct device *dev = &pdev->dev;
	struct device_node *np = dev->of_node;
	struct kbpwr_dev *kbpwr;
	int ret, i;

	kbpwr = devm_kzalloc(dev, sizeof(*kbpwr), GFP_KERNEL);
	if (!kbpwr)
		return -ENOMEM;

	if (of_machine_is_compatible("pine64,pinephone-pro") > 0) {
		kbpwr->mach = &kbpwr_ppp;
	} else if (of_machine_is_compatible("pine64,pinephone") > 0) {
		kbpwr->mach = &kbpwr_pp;
	} else {
		return dev_err_probe(dev, -EINVAL, "unsupported machine\n");
	}

	kbpwr->dev = dev;
	mutex_init(&kbpwr->lock);
	INIT_DELAYED_WORK(&kbpwr->work, kbpwr_work);
	platform_set_drvdata(pdev, kbpwr);

	struct {
		const char* prop;
		struct power_supply **psy;
	} supplies[] = {
		{ "phone-battery", &kbpwr->phone_battery, },
		{ "phone-usb", &kbpwr->phone_usb, },
		{ "kb-battery", &kbpwr->kb_battery, },
		{ "kb-boost", &kbpwr->kb_boost, },
		{ "kb-usb", &kbpwr->kb_usb, },
	};

	for (i = 0; i < ARRAY_SIZE(supplies); i++) {
		const char* prop = supplies[i].prop;
		struct power_supply** psy = supplies[i].psy;
		const char* name;

		ret = of_property_read_string(np, prop, &name);
		if (ret)
			return dev_err_probe(dev, ret, "Can't find supply name for %s\n", prop);

		*psy = devm_power_supply_get_by_name(dev, name);
		if (IS_ERR_OR_NULL(*psy))
			return dev_err_probe(dev, -EPROBE_DEFER,
					     "Couldn't get '%s' power supply\n", name);
	}

	ret = devm_device_add_group(dev, &kbpwr_group);
	if (ret)
		return ret;

	for (i = 0; i < KBPWR_LED_TRIGGER_COUNT; i++) {
		kbpwr->trigger[i].name = trig_names[i];

		ret = devm_led_trigger_register(dev, &kbpwr->trigger[i]);
		if (ret)
			return dev_err_probe(dev, ret, "failed to register LED trigger %s\n",
					     kbpwr->trigger[i].name);
	}

	kbpwr->wq = alloc_ordered_workqueue("ppkb-power-wq", 0);
	if (!kbpwr->wq)
		return dev_err_probe(dev, -ENOMEM, "failed to allocate workqueue\n");

	kbpwr->debug_root = debugfs_create_dir("kbpwr", NULL);
	debugfs_create_file("state", 0444, kbpwr->debug_root, kbpwr,
			    &kbpwr_status_fops);

	dev_info(dev, "Pinephone keyboard power manager ready\n");

	//set_bit(KBPWR_F_EMERGENCY_SHUTDOWN, kbpwr->flags);
	set_bit(KBPWR_F_DISABLED, kbpwr->flags);

	queue_delayed_work(kbpwr->wq, &kbpwr->work, msecs_to_jiffies(10000));

	return 0;
}

static int kbpwr_remove(struct platform_device *pdev)
{
	struct kbpwr_dev *kbpwr = platform_get_drvdata(pdev);

	cancel_delayed_work_sync(&kbpwr->work);

	mutex_lock(&kbpwr->lock);
	//XXX: turn off charging from kb? turn off VOUT if possible
	mutex_unlock(&kbpwr->lock);

	destroy_workqueue(kbpwr->wq);

	debugfs_remove(kbpwr->debug_root);

	return 0;
}

static void kbpwr_shutdown(struct platform_device *pdev)
{
	struct kbpwr_dev *kbpwr = platform_get_drvdata(pdev);

	cancel_delayed_work_sync(&kbpwr->work);

	mutex_lock(&kbpwr->lock);
	//XXX: turn off charging from kb? turn off VOUT if possible
	mutex_unlock(&kbpwr->lock);
}

static int __maybe_unused kbpwr_suspend(struct device *dev)
{
	struct kbpwr_dev *kbpwr = dev_get_drvdata(dev);
	int ret = 0;

	cancel_delayed_work_sync(&kbpwr->work);

	mutex_lock(&kbpwr->lock);
	//XXX: turn off charging from kb?
	mutex_unlock(&kbpwr->lock);

	return ret;
}

static int __maybe_unused kbpwr_resume(struct device *dev)
{
	struct kbpwr_dev *kbpwr = dev_get_drvdata(dev);
	int ret = 0;

	//XXX: during quick suspend/resume cycles the work may never run

	// schedule update soon
	queue_delayed_work(kbpwr->wq, &kbpwr->work, msecs_to_jiffies(5000));

	return ret;
}

static const struct dev_pm_ops kbpwr_pm_ops = {
	SET_SYSTEM_SLEEP_PM_OPS(kbpwr_suspend, kbpwr_resume)
};

static const struct of_device_id kbpwr_of_match[] = {
	{ .compatible = "megi,pinephone-keyboard-power-manager" },
	{},
};
MODULE_DEVICE_TABLE(of, kbpwr_of_match);

static struct platform_driver kbpwr_driver = {
	.probe = kbpwr_probe,
	.remove = kbpwr_remove,
	.shutdown = kbpwr_shutdown,
	.driver = {
		.name = DRIVER_NAME,
		.of_match_table = kbpwr_of_match,
		.pm = &kbpwr_pm_ops,
	},
};

module_platform_driver(kbpwr_driver);

MODULE_DESCRIPTION("Pinephone keyboard power manager");
MODULE_AUTHOR("Ondrej Jirman <megi@xff.cz>");
MODULE_LICENSE("GPL v2");
