/*
 * Copyright (c) 2022 Nick Ward <nix.ward@gmail.com>
 *
 * SPDX-License-Identifier: Apache-2.0
 */

#define DT_DRV_COMPAT nxp_pca9685_pwm

#include <zephyr/device.h>
#include <zephyr/drivers/i2c.h>
#include <zephyr/drivers/pwm.h>
#include <zephyr/sys/util_macro.h>
#include <zephyr/kernel.h>

#include <zephyr/logging/log.h>
LOG_MODULE_REGISTER(pwm_pca9685, CONFIG_PWM_LOG_LEVEL);

#define OSC_CLOCK_MAX	  50000000
#define CHANNEL_CNT	  16

#define ADDR_MODE1	  0x00
#define SLEEP		  BIT(4)
#define AUTO_INC	  BIT(5)
#define RESTART		  BIT(7)

#define ADDR_MODE2	  0x01
#define OUTDRV_TOTEM_POLE BIT(2)
#define OCH_ON_ACK	  BIT(3)

#define ADDR_LED0_ON_L	  0x06
#define ADDR_LED_ON_L(n)  (ADDR_LED0_ON_L + (4 * n))
#define LED_FULL_ON	  BIT(4)
#define LED_FULL_OFF	  BIT(4)
#define BYTE_N(word, n)	  ((word >> (8 * n)) & 0xFF)

#define ADDR_PRE_SCALE	  0xFE


/* See PCA9585 datasheet Rev. 4 - 16 April 2015 section 7.3.5 */
#define OSC_CLOCK_INTERNAL 25000000

#define PWM_STEPS 4096

#define PRE_SCALE_MIN 0x03
#define PRE_SCALE_MAX 0xFF

#define PRE_SCALE_DEFAULT 0x1E

#define PWM_PERIOD_COUNT_PS(pre_scale) (PWM_STEPS * (pre_scale + 1))

/*
 * When using the internal oscillator this corresponds to an
 * update rate of 1526 Hz
 */
#define PWM_PERIOD_COUNT_MIN PWM_PERIOD_COUNT_PS(PRE_SCALE_MIN)

/*
 * When using the internal oscillator this corresponds to an
 * update rate of 24 Hz
 */
#define PWM_PERIOD_COUNT_MAX PWM_PERIOD_COUNT_PS(PRE_SCALE_MAX)

/*
 * When using the internal oscillator this corresponds to an
 * update rate of 200 Hz
 */
#define PWM_PERIOD_COUNT_DEFAULT PWM_PERIOD_COUNT_PS(PRE_SCALE_DEFAULT)


/*
 * Time allowed for the oscillator to stabilize after the PCA9685's
 * RESTART mode is used to wake the chip from SLEEP.
 * See PCA9685 datasheet section 7.3.1.1
 */
#define OSCILLATOR_STABILIZE K_USEC(500)

struct pca9685_config {
	struct i2c_dt_spec i2c;
	bool outdrv_open_drain;
	bool och_on_ack;
	bool invrt;
};

struct pca9685_data {
	struct k_mutex mutex;
	uint8_t pre_scale;
};

static int set_reg(const struct device *dev, uint8_t addr, uint8_t value)
{
	const struct pca9685_config *config = dev->config;
	uint8_t buf[] = {addr, value};
	int ret;

	ret = i2c_write_dt(&config->i2c, buf, sizeof(buf));
	if (ret != 0) {
		LOG_ERR("I2C write [0x%02X]=0x%02X: %d", addr, value, ret);
	} else {
		LOG_DBG("[0x%02X]=0x%02X", addr, value);
	}
	return ret;
}

static int get_reg(const struct device *dev, uint8_t addr, uint8_t *value)
{
	const struct pca9685_config *config = dev->config;
	int ret;

	ret = i2c_write_read_dt(&config->i2c, &addr, sizeof(addr), value,
				sizeof(*value));
	if (ret != 0) {
		LOG_ERR("I2C write [0x%02X]=0x%02X: %d", addr, *value, ret);
	}
	return ret;
}

static int set_pre_scale(const struct device *dev, uint8_t value)
{
	struct pca9685_data *data = dev->data;
	uint8_t mode1;
	int ret;
	uint8_t restart = RESTART;

	k_mutex_lock(&data->mutex, K_FOREVER);

	/* Unblock write to PRE_SCALE by setting SLEEP bit to logic 1 */
	ret = set_reg(dev, ADDR_MODE1, AUTO_INC | SLEEP);
	if (ret != 0) {
		goto out;
	}

	ret = get_reg(dev, ADDR_MODE1, &mode1);
	if (ret != 0) {
		goto out;
	}

	if ((mode1 & RESTART) == 0x00) {
		restart = 0;
	}

	ret = set_reg(dev, ADDR_PRE_SCALE, value);
	if (ret != 0) {
		goto out;
	}

	/* Clear SLEEP bit - See datasheet section 7.3.1.1 */
	ret = set_reg(dev, ADDR_MODE1, AUTO_INC);
	if (ret != 0) {
		goto out;
	}

	k_sleep(OSCILLATOR_STABILIZE);

	ret = set_reg(dev, ADDR_MODE1, AUTO_INC | restart);
	if (ret != 0) {
		goto out;
	}

out:
	k_mutex_unlock(&data->mutex);

	return ret;
}

static int pca9685_set_cycles(const struct device *dev,
			      uint32_t channel, uint32_t period_count,
			      uint32_t pulse_count, pwm_flags_t flags)
{
	const struct pca9685_config *config = dev->config;
	struct pca9685_data *data = dev->data;
	uint8_t buf[5] = { 0 };
	uint32_t led_off_count;
	int32_t pre_scale;
	int ret;

	ARG_UNUSED(flags);

	if (channel >= CHANNEL_CNT) {
		LOG_WRN("channel out of range: %u", channel);
		return -EINVAL;
	}

	pre_scale = DIV_ROUND_UP((int64_t)period_count, PWM_STEPS) - 1;

	if (pre_scale < PRE_SCALE_MIN) {
		LOG_ERR("period_count %u < %u (min)", period_count,
			PWM_PERIOD_COUNT_MIN);
		return -ENOTSUP;
	} else if (pre_scale > PRE_SCALE_MAX) {
		LOG_ERR("period_count %u > %u (max)", period_count,
			PWM_PERIOD_COUNT_MAX);
		return -ENOTSUP;
	}

	/* Only one output frequency - simple conversion to equivalent PWM */
	if (pre_scale != data->pre_scale) {
		data->pre_scale = pre_scale;
		ret = set_pre_scale(dev, pre_scale);
		if (ret != 0) {
			return ret;
		}
	}

	/* Adjust PWM output for the resolution of the PCA9685 */
	led_off_count = DIV_ROUND_UP(pulse_count * PWM_STEPS, period_count);

	buf[0] = ADDR_LED_ON_L(channel);
	if (led_off_count == 0) {
		buf[4] = LED_FULL_OFF;
	} else if (led_off_count < PWM_STEPS) {
		/* LED turns on at count 0 */
		buf[3] = BYTE_N(led_off_count, 0);
		buf[4] = BYTE_N(led_off_count, 1);
	} else {
		buf[2] = LED_FULL_ON;
	}

	return i2c_write_dt(&config->i2c, buf, sizeof(buf));
}

static int pca9685_get_cycles_per_sec(const struct device *dev,
				      uint32_t channel, uint64_t *cycles)
{
	ARG_UNUSED(dev);
	ARG_UNUSED(channel);

	*cycles = OSC_CLOCK_INTERNAL;

	return 0;
}

static const struct pwm_driver_api pca9685_api = {
	.set_cycles = pca9685_set_cycles,
	.get_cycles_per_sec = pca9685_get_cycles_per_sec,
};

static int pca9685_init(const struct device *dev)
{
	const struct pca9685_config *config = dev->config;
	struct pca9685_data *data = dev->data;
	uint8_t value;
	int ret;

	(void)k_mutex_init(&data->mutex);

	if (!i2c_is_ready_dt(&config->i2c)) {
		LOG_ERR("I2C device not ready");
		return -ENODEV;
	}

	ret = set_reg(dev, ADDR_MODE1, AUTO_INC);
	if (ret != 0) {
		LOG_ERR("set_reg MODE1: %d", ret);
		return ret;
	}

	value = 0x00;
	if (!config->outdrv_open_drain) {
		value |= OUTDRV_TOTEM_POLE;
	}
	if (config->och_on_ack) {
		value |= OCH_ON_ACK;
	}
	ret = set_reg(dev, ADDR_MODE2, value);
	if (ret != 0) {
		LOG_ERR("set_reg MODE2: %d", ret);
		return ret;
	}

	return 0;
}

#define PCA9685_INIT(inst)                                              \
	static const struct pca9685_config pca9685_##inst##_config = {  \
		.i2c = I2C_DT_SPEC_INST_GET(inst),                      \
		.outdrv_open_drain = DT_INST_PROP(inst, open_drain),    \
		.och_on_ack = DT_INST_PROP(inst, och_on_ack),           \
		.invrt = DT_INST_PROP(inst, invert),                    \
	};                                                              \
                                                                        \
	static struct pca9685_data pca9685_##inst##_data;               \
                                                                        \
	DEVICE_DT_INST_DEFINE(inst, pca9685_init, NULL,                 \
			      &pca9685_##inst##_data,                   \
			      &pca9685_##inst##_config, POST_KERNEL,    \
			      CONFIG_PWM_INIT_PRIORITY,                 \
			      &pca9685_api);

DT_INST_FOREACH_STATUS_OKAY(PCA9685_INIT);
