// SPDX-License-Identifier: GPL-2.0
// Copyright (c) 2012 Intel Corporation

/*
 * Based on linux/modules/camera/drivers/media/i2c/imx/dw9719.c from:
 * https://github.com/ZenfoneArea/android_kernel_asus_zenfone5 and
 * latte-l-oss/drivers/external_drivers/camera/drivers/media/i2c/micam/dw9761.c
 * from: https://github.com/MiCode/Xiaomi_Kernel_OpenSource/
 */

#include <linux/delay.h>
#include <linux/i2c.h>
#include <linux/pm_runtime.h>
#include <linux/regulator/consumer.h>
#include <linux/types.h>

#include <media/v4l2-cci.h>
#include <media/v4l2-common.h>
#include <media/v4l2-ctrls.h>
#include <media/v4l2-subdev.h>

#define DW9719_MAX_FOCUS_POS	1023
#define DW9719_CTRL_STEPS	16
#define DW9719_CTRL_DELAY_US	1000

#define DW9718S_PD			CCI_REG8(0)

#define DW9718S_CONTROL			CCI_REG8(1)
#define DW9718S_CONTROL_SW_LINEAR	BIT(0)
#define DW9718S_CONTROL_SAC_SHIFT	1
#define DW9718S_CONTROL_SAC_MASK	0x7
#define DW9718S_CONTROL_OCP_DISABLE	BIT(4)
#define DW9718S_CONTROL_UVLO_DISABLE	BIT(5)
#define DW9718S_DEFAULT_SAC		4

#define DW9718S_VCM_CURRENT		CCI_REG16(2)

#define DW9718S_SW			CCI_REG8(4)
#define DW9718S_SW_VCM_FREQ_MASK	0xF
#define DW9718S_DEFAULT_VCM_FREQ	0

#define DW9718S_SACT			CCI_REG8(5)
#define DW9718S_SACT_PERIOD_8_8MS	0x19

#define DW9719_INFO			CCI_REG8(0)
#define DW9719_ID			0xF1
#define DW9761_ID			0xF4

#define DW9719_CONTROL			CCI_REG8(2)
#define DW9719_STANDBY			0x00
#define DW9719_SHUTDOWN			0x01
#define DW9719_ENABLE_RINGING		0x02

#define DW9719_VCM_CURRENT		CCI_REG16(3)

#define DW9719_STATUS			CCI_REG16(5)
#define DW9719_STATUS_BUSY		BIT(0)

#define DW9719_MODE			CCI_REG8(6)
#define DW9719_MODE_SAC_SHIFT		4
#define DW9719_DEFAULT_SAC		4
#define DW9761_DEFAULT_SAC		6

#define DW9719_VCM_FREQ			CCI_REG8(7)
#define DW9719_DEFAULT_VCM_FREQ		0x60
#define DW9761_DEFAULT_VCM_FREQ		0x3E

#define DW9761_VCM_PRELOAD		CCI_REG8(8)
#define DW9761_DEFAULT_VCM_PRELOAD	0x73

#define DW9800K_DEFAULT_SAC		1
#define DW9800K_MODE_SAC_SHIFT		6
#define DW9800K_DEFAULT_VCM_FREQ		0x10

#define to_dw9719_device(x) container_of(x, struct dw9719_device, sd)

enum dw9719_model {
	DW9718S,
	DW9719,
	DW9761,
	DW9800K,
};

struct dw9719_device {
	struct v4l2_subdev sd;
	struct device *dev;
	struct regmap *regmap;
	struct regulator *regulator;
	enum dw9719_model model;
	u32 mode_low_bits;
	u32 sac_mode;
	u32 vcm_freq;

	struct dw9719_v4l2_ctrls {
		struct v4l2_ctrl_handler handler;
		struct v4l2_ctrl *focus;
	} ctrls;
};

static int dw9719_power_down(struct dw9719_device *dw9719)
{
	u32 reg_pwr = dw9719->model == DW9718S ? DW9718S_PD : DW9719_CONTROL;

	/*
	 * Worth engaging the internal SHUTDOWN mode especially due to the
	 * regulator being potentially shared with other devices.
	 */
	if (cci_write(dw9719->regmap, reg_pwr, DW9719_SHUTDOWN, NULL))
		dev_err(dw9719->dev, "Error writing to power register\n");
	return regulator_disable(dw9719->regulator);
}

static int dw9719_power_up(struct dw9719_device *dw9719, bool detect)
{
	u32 reg_pwr = dw9719->model == DW9718S ? DW9718S_PD : DW9719_CONTROL;
	u64 val;
	int ret;
	int err;

	ret = regulator_enable(dw9719->regulator);
	if (ret)
		return ret;

	/*
	 * Need 100us to transition from SHUTDOWN to STANDBY.
	 * Jiggle the SCL pin to wake up the device (even when the regulator is
	 * shared) and wait double the time to be sure, as 100us is not enough
	 * at least on the DW9718S as found on the motorola-nora smartphone,
	 * then retry the write.
	 */
	cci_write(dw9719->regmap, reg_pwr, DW9719_STANDBY, NULL);
	/* the jiggle is expected to fail, don't even log that as error */
	fsleep(200);
	cci_write(dw9719->regmap, reg_pwr, DW9719_STANDBY, &ret);

	if (detect) {
		/* These models do not have an INFO register */
		switch (dw9719->model) {
		case DW9718S:
			dw9719->sac_mode = DW9718S_DEFAULT_SAC;
			dw9719->vcm_freq = DW9718S_DEFAULT_VCM_FREQ;
			goto props;
		case DW9800K:
			dw9719->sac_mode = DW9800K_DEFAULT_SAC;
			dw9719->vcm_freq = DW9800K_DEFAULT_VCM_FREQ;
			goto props;
		default:
			break;
		}

		ret = cci_read(dw9719->regmap, DW9719_INFO, &val, NULL);
		if (ret < 0)
			return ret;

		switch (val) {
		case DW9719_ID:
			dw9719->model = DW9719;
			dw9719->mode_low_bits = 0x00;
			dw9719->sac_mode = DW9719_DEFAULT_SAC;
			dw9719->vcm_freq = DW9719_DEFAULT_VCM_FREQ;
			break;
		case DW9761_ID:
			dw9719->model = DW9761;
			dw9719->mode_low_bits = 0x01;
			dw9719->sac_mode = DW9761_DEFAULT_SAC;
			dw9719->vcm_freq = DW9761_DEFAULT_VCM_FREQ;
			break;
		default:
			dev_err(dw9719->dev,
				"Error unknown device id 0x%02llx\n", val);
			return -ENXIO;
		}

props:
		/* Optional indication of SAC mode select */
		device_property_read_u32(dw9719->dev, "dongwoon,sac-mode",
					 &dw9719->sac_mode);

		/* Optional indication of VCM frequency */
		err = device_property_read_u32(dw9719->dev, "dongwoon,vcm-freq",
					       &dw9719->vcm_freq);
		if (err == 0)
			dev_warn(dw9719->dev, "dongwoon,vcm-freq property is deprecated, please use dongwoon,vcm-prescale\n");

		/* Optional indication of VCM prescale */
		device_property_read_u32(dw9719->dev, "dongwoon,vcm-prescale",
					 &dw9719->vcm_freq);
	}

	switch (dw9719->model) {
	case DW9800K:
		cci_write(dw9719->regmap, DW9719_CONTROL, DW9719_ENABLE_RINGING, &ret);
		cci_write(dw9719->regmap, DW9719_MODE,
			  dw9719->sac_mode << DW9800K_MODE_SAC_SHIFT, &ret);
		cci_write(dw9719->regmap, DW9719_VCM_FREQ, dw9719->vcm_freq, &ret);
		break;
	case DW9718S:
		/* Datasheet says [OCP/UVLO] should be disabled below 2.5V */
		dw9719->sac_mode &= DW9718S_CONTROL_SAC_MASK;
		cci_write(dw9719->regmap, DW9718S_CONTROL,
			  DW9718S_CONTROL_SW_LINEAR |
			  (dw9719->sac_mode << DW9718S_CONTROL_SAC_SHIFT) |
			  DW9718S_CONTROL_OCP_DISABLE |
			  DW9718S_CONTROL_UVLO_DISABLE, &ret);
		cci_write(dw9719->regmap, DW9718S_SACT,
			  DW9718S_SACT_PERIOD_8_8MS, &ret);
		cci_write(dw9719->regmap, DW9718S_SW,
			  dw9719->vcm_freq & DW9718S_SW_VCM_FREQ_MASK, &ret);
		break;
	case DW9761:
		cci_write(dw9719->regmap, DW9761_VCM_PRELOAD,
			  DW9761_DEFAULT_VCM_PRELOAD, &ret);
		fallthrough;
	case DW9719:
		cci_write(dw9719->regmap, DW9719_CONTROL, DW9719_ENABLE_RINGING, &ret);
		cci_write(dw9719->regmap, DW9719_MODE, dw9719->mode_low_bits |
				  (dw9719->sac_mode << DW9719_MODE_SAC_SHIFT), &ret);
		cci_write(dw9719->regmap, DW9719_VCM_FREQ, dw9719->vcm_freq, &ret);
	}

	if (ret)
		dw9719_power_down(dw9719);

	return ret;
}

static int dw9719_t_focus_abs(struct dw9719_device *dw9719, s32 value)
{
	u32 reg = dw9719->model == DW9718S ? DW9718S_VCM_CURRENT
					   : DW9719_VCM_CURRENT;
	return cci_write(dw9719->regmap, reg, value, NULL);
}

static int dw9719_set_ctrl(struct v4l2_ctrl *ctrl)
{
	struct dw9719_device *dw9719 = container_of(ctrl->handler,
						    struct dw9719_device,
						    ctrls.handler);
	int ret;

	/* Only apply changes to the controls if the device is powered up */
	if (!pm_runtime_get_if_in_use(dw9719->dev))
		return 0;

	switch (ctrl->id) {
	case V4L2_CID_FOCUS_ABSOLUTE:
		ret = dw9719_t_focus_abs(dw9719, ctrl->val);
		break;
	default:
		ret = -EINVAL;
	}

	pm_runtime_put(dw9719->dev);

	return ret;
}

static const struct v4l2_ctrl_ops dw9719_ctrl_ops = {
	.s_ctrl = dw9719_set_ctrl,
};

static int dw9719_suspend(struct device *dev)
{
	struct v4l2_subdev *sd = dev_get_drvdata(dev);
	struct dw9719_device *dw9719 = to_dw9719_device(sd);
	int ret;
	int val;

	for (val = dw9719->ctrls.focus->val; val >= 0;
	     val -= DW9719_CTRL_STEPS) {
		ret = dw9719_t_focus_abs(dw9719, val);
		if (ret)
			return ret;

		usleep_range(DW9719_CTRL_DELAY_US, DW9719_CTRL_DELAY_US + 10);
	}

	return dw9719_power_down(dw9719);
}

static int dw9719_resume(struct device *dev)
{
	struct v4l2_subdev *sd = dev_get_drvdata(dev);
	struct dw9719_device *dw9719 = to_dw9719_device(sd);
	int current_focus = dw9719->ctrls.focus->val;
	int ret;
	int val;

	ret = dw9719_power_up(dw9719, false);
	if (ret)
		return ret;

	for (val = current_focus % DW9719_CTRL_STEPS; val < current_focus;
	     val += DW9719_CTRL_STEPS) {
		ret = dw9719_t_focus_abs(dw9719, val);
		if (ret)
			goto err_power_down;

		usleep_range(DW9719_CTRL_DELAY_US, DW9719_CTRL_DELAY_US + 10);
	}

	return 0;

err_power_down:
	dw9719_power_down(dw9719);
	return ret;
}

static int dw9719_open(struct v4l2_subdev *sd, struct v4l2_subdev_fh *fh)
{
	return pm_runtime_resume_and_get(sd->dev);
}

static int dw9719_close(struct v4l2_subdev *sd, struct v4l2_subdev_fh *fh)
{
	pm_runtime_put_autosuspend(sd->dev);

	return 0;
}

static const struct v4l2_subdev_internal_ops dw9719_internal_ops = {
	.open = dw9719_open,
	.close = dw9719_close,
};

static int dw9719_init_controls(struct dw9719_device *dw9719)
{
	const struct v4l2_ctrl_ops *ops = &dw9719_ctrl_ops;
	int ret;

	v4l2_ctrl_handler_init(&dw9719->ctrls.handler, 1);

	dw9719->ctrls.focus = v4l2_ctrl_new_std(&dw9719->ctrls.handler, ops,
						V4L2_CID_FOCUS_ABSOLUTE, 0,
						DW9719_MAX_FOCUS_POS, 1, 0);

	if (dw9719->ctrls.handler.error) {
		dev_err(dw9719->dev, "Error initialising v4l2 ctrls\n");
		ret = dw9719->ctrls.handler.error;
		goto err_free_handler;
	}

	dw9719->sd.ctrl_handler = &dw9719->ctrls.handler;
	return 0;

err_free_handler:
	v4l2_ctrl_handler_free(&dw9719->ctrls.handler);
	return ret;
}

static const struct v4l2_subdev_ops dw9719_ops = { };

static int dw9719_probe(struct i2c_client *client)
{
	struct dw9719_device *dw9719;
	int ret;

	dw9719 = devm_kzalloc(&client->dev, sizeof(*dw9719), GFP_KERNEL);
	if (!dw9719)
		return -ENOMEM;

	dw9719->model = (enum dw9719_model)(uintptr_t)i2c_get_match_data(client);

	dw9719->regmap = devm_cci_regmap_init_i2c(client, 8);
	if (IS_ERR(dw9719->regmap))
		return PTR_ERR(dw9719->regmap);

	dw9719->dev = &client->dev;

	dw9719->regulator = devm_regulator_get(&client->dev, "vdd");
	if (IS_ERR(dw9719->regulator))
		return dev_err_probe(&client->dev, PTR_ERR(dw9719->regulator),
				     "getting regulator\n");

	v4l2_i2c_subdev_init(&dw9719->sd, client, &dw9719_ops);
	dw9719->sd.flags |= V4L2_SUBDEV_FL_HAS_DEVNODE;
	dw9719->sd.internal_ops = &dw9719_internal_ops;

	ret = dw9719_init_controls(dw9719);
	if (ret)
		return ret;

	ret = media_entity_pads_init(&dw9719->sd.entity, 0, NULL);
	if (ret < 0)
		goto err_free_ctrl_handler;

	dw9719->sd.entity.function = MEDIA_ENT_F_LENS;

	/*
	 * We need the driver to work in the event that pm runtime is disable in
	 * the kernel, so power up and verify the chip now. In the event that
	 * runtime pm is disabled this will leave the chip on, so that the lens
	 * will work.
	 */

	ret = dw9719_power_up(dw9719, true);
	if (ret)
		goto err_cleanup_media;

	pm_runtime_set_active(&client->dev);
	pm_runtime_get_noresume(&client->dev);
	pm_runtime_enable(&client->dev);

	ret = v4l2_async_register_subdev(&dw9719->sd);
	if (ret < 0)
		goto err_pm_runtime;

	pm_runtime_set_autosuspend_delay(&client->dev, 1000);
	pm_runtime_use_autosuspend(&client->dev);
	pm_runtime_put_autosuspend(&client->dev);

	return ret;

err_pm_runtime:
	pm_runtime_disable(&client->dev);
	pm_runtime_put_noidle(&client->dev);
	dw9719_power_down(dw9719);
err_cleanup_media:
	media_entity_cleanup(&dw9719->sd.entity);
err_free_ctrl_handler:
	v4l2_ctrl_handler_free(&dw9719->ctrls.handler);

	return ret;
}

static void dw9719_remove(struct i2c_client *client)
{
	struct v4l2_subdev *sd = i2c_get_clientdata(client);
	struct dw9719_device *dw9719 =
		container_of(sd, struct dw9719_device, sd);

	v4l2_async_unregister_subdev(sd);
	v4l2_ctrl_handler_free(&dw9719->ctrls.handler);
	media_entity_cleanup(&dw9719->sd.entity);

	pm_runtime_disable(&client->dev);
	if (!pm_runtime_status_suspended(&client->dev))
		dw9719_power_down(dw9719);
	pm_runtime_set_suspended(&client->dev);
}

static const struct of_device_id dw9719_of_table[] = {
	{ .compatible = "dongwoon,dw9718s", .data = (const void *)DW9718S },
	{ .compatible = "dongwoon,dw9719", .data = (const void *)DW9719 },
	{ .compatible = "dongwoon,dw9761", .data = (const void *)DW9761 },
	{ .compatible = "dongwoon,dw9800k", .data = (const void *)DW9800K },
	{ }
};
MODULE_DEVICE_TABLE(of, dw9719_of_table);

static DEFINE_RUNTIME_DEV_PM_OPS(dw9719_pm_ops, dw9719_suspend, dw9719_resume,
				 NULL);

static struct i2c_driver dw9719_i2c_driver = {
	.driver = {
		.name = "dw9719",
		.pm = pm_sleep_ptr(&dw9719_pm_ops),
		.of_match_table = dw9719_of_table,
	},
	.probe = dw9719_probe,
	.remove = dw9719_remove,
};
module_i2c_driver(dw9719_i2c_driver);

MODULE_AUTHOR("Daniel Scally <djrscally@gmail.com>");
MODULE_DESCRIPTION("DW9719 VCM Driver");
MODULE_LICENSE("GPL");
