// SPDX-License-Identifier: GPL-2.0
/*
 * Copyright (C) 2020 Marek Vasut <marex@denx.de>
 *
 * Based on rpi_touchscreen.c by Eric Anholt <eric@anholt.net>
 */

#include <linux/backlight.h>
#include <linux/err.h>
#include <linux/gpio.h>
#include <linux/i2c.h>
#include <linux/init.h>
#include <linux/interrupt.h>
#include <linux/module.h>
#include <linux/regmap.h>
#include <linux/regulator/driver.h>
#include <linux/regulator/machine.h>
#include <linux/regulator/of_regulator.h>
#include <linux/slab.h>

/* I2C registers of the Atmel microcontroller. */
#define REG_ID		0x80
#define REG_PORTA	0x81
#define REG_PORTA_HF	BIT(2)
#define REG_PORTA_VF	BIT(3)
#define REG_PORTB	0x82
#define REG_POWERON	0x85
#define REG_PWM		0x86

static const struct regmap_config attiny_regmap_config = {
	.reg_bits = 8,
	.val_bits = 8,
	.max_register = REG_PWM,
	.cache_type = REGCACHE_NONE,
};

static int attiny_lcd_power_enable(struct regulator_dev *rdev)
{
	unsigned int data;
	int ret, i;

	regmap_write(rdev->regmap, REG_POWERON, 1);
	msleep(80);

	/* Wait for nPWRDWN to go low to indicate poweron is done. */
	for (i = 0; i < 20; i++) {
		ret = regmap_read(rdev->regmap, REG_PORTB, &data);
		if (!ret) {
			if (data & BIT(0))
				break;
		}
		usleep_range(10000, 12000);
	}
	usleep_range(10000, 12000);

	if (ret)
		pr_err("%s: regmap_read_poll_timeout failed %d\n", __func__, ret);

	/* Default to the same orientation as the closed source
	 * firmware used for the panel.  Runtime rotation
	 * configuration will be supported using VC4's plane
	 * orientation bits.
	 */
	regmap_write(rdev->regmap, REG_PORTA, BIT(2));

	return 0;
}

static int attiny_lcd_power_disable(struct regulator_dev *rdev)
{
	regmap_write(rdev->regmap, REG_PWM, 0);
	regmap_write(rdev->regmap, REG_POWERON, 0);
	msleep(30);
	return 0;
}

static int attiny_lcd_power_is_enabled(struct regulator_dev *rdev)
{
	unsigned int data;
	int ret, i;

	for (i = 0; i < 10; i++) {
		ret = regmap_read(rdev->regmap, REG_POWERON, &data);
		if (!ret)
			break;
		usleep_range(10000, 12000);
	}
	if (ret < 0)
		return ret;

	if (!(data & BIT(0)))
		return 0;

	for (i = 0; i < 10; i++) {
		ret = regmap_read(rdev->regmap, REG_PORTB, &data);
		if (!ret)
			break;
		usleep_range(10000, 12000);
	}

	if (ret < 0)
		return ret;

	return data & BIT(0);
}

static const struct regulator_init_data attiny_regulator_default = {
	.constraints = {
		.valid_ops_mask = REGULATOR_CHANGE_STATUS,
	},
};

static const struct regulator_ops attiny_regulator_ops = {
	.enable = attiny_lcd_power_enable,
	.disable = attiny_lcd_power_disable,
	.is_enabled = attiny_lcd_power_is_enabled,
};

static const struct regulator_desc attiny_regulator = {
	.name	= "tc358762-power",
	.ops	= &attiny_regulator_ops,
	.type	= REGULATOR_VOLTAGE,
	.owner	= THIS_MODULE,
};

static int attiny_update_status(struct backlight_device *bl)
{
	struct regmap *regmap = bl_get_data(bl);
	int brightness = bl->props.brightness;
	int ret, i;

	if (bl->props.power != FB_BLANK_UNBLANK ||
	    bl->props.fb_blank != FB_BLANK_UNBLANK)
		brightness = 0;

	for (i = 0; i < 10; i++) {
		ret = regmap_write(regmap, REG_PWM, brightness);
		if (!ret)
			break;
	}

	return ret;
}

static int attiny_get_brightness(struct backlight_device *bl)
{
	struct regmap *regmap = bl_get_data(bl);
	int ret, brightness, i;

	for (i = 0; i < 10; i++) {
		ret = regmap_read(regmap, REG_PWM, &brightness);
		if (!ret)
			break;
	}

	if (ret)
		return ret;

	return brightness;
}

static const struct backlight_ops attiny_bl = {
	.update_status	= attiny_update_status,
	.get_brightness	= attiny_get_brightness,
};

/*
 * I2C driver interface functions
 */
static int attiny_i2c_probe(struct i2c_client *i2c,
		const struct i2c_device_id *id)
{
	struct backlight_properties props = { };
	struct regulator_config config = { };
	struct backlight_device *bl;
	struct regulator_dev *rdev;
	struct regmap *regmap;
	unsigned int data;
	int ret;

	regmap = devm_regmap_init_i2c(i2c, &attiny_regmap_config);
	if (IS_ERR(regmap)) {
		ret = PTR_ERR(regmap);
		dev_err(&i2c->dev, "Failed to allocate register map: %d\n",
			ret);
		return ret;
	}

	ret = regmap_read(regmap, REG_ID, &data);
	if (ret < 0) {
		dev_err(&i2c->dev, "Failed to read REG_ID reg: %d\n", ret);
		return ret;
	}

	switch (data) {
	case 0xde: /* ver 1 */
	case 0xc3: /* ver 2 */
		break;
	default:
		dev_err(&i2c->dev, "Unknown Atmel firmware revision: 0x%02x\n", data);
		return -ENODEV;
	}

	regmap_write(regmap, REG_POWERON, 0);
	msleep(30);

	config.dev = &i2c->dev;
	config.regmap = regmap;
	config.of_node = i2c->dev.of_node;
	config.init_data = &attiny_regulator_default;

	rdev = devm_regulator_register(&i2c->dev, &attiny_regulator, &config);
	if (IS_ERR(rdev)) {
		dev_err(&i2c->dev, "Failed to register ATTINY regulator\n");
		return PTR_ERR(rdev);
	}

	props.type = BACKLIGHT_RAW;
	props.max_brightness = 0xff;
	bl = devm_backlight_device_register(&i2c->dev,
					    "7inch-touchscreen-panel-bl",
					    &i2c->dev, regmap, &attiny_bl,
					    &props);
	if (IS_ERR(bl))
		return PTR_ERR(bl);

	bl->props.brightness = 0xff;

	return 0;
}

static const struct of_device_id attiny_dt_ids[] = {
	{ .compatible = "raspberrypi,7inch-touchscreen-panel-regulator" },
	{},
};
MODULE_DEVICE_TABLE(of, attiny_dt_ids);

static struct i2c_driver attiny_regulator_driver = {
	.driver = {
		.name = "rpi_touchscreen_attiny",
		.of_match_table = of_match_ptr(attiny_dt_ids),
	},
	.probe = attiny_i2c_probe,
};

module_i2c_driver(attiny_regulator_driver);

MODULE_AUTHOR("Marek Vasut <marex@denx.de>");
MODULE_DESCRIPTION("Regulator device driver for Raspberry Pi 7-inch touchscreen");
MODULE_LICENSE("GPL v2");
