/*
 * Copyright (c) 2018 PHYTEC Messtechnik GmbH
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 */

#include <stdint.h>

#include "os/mynewt.h"
#include "hal/hal_gpio.h"
#include "hal/hal_spi.h"
#include "modlog/modlog.h"

#include "display/display.h"
#include "display/cfb.h"

#include "ssd1673_regs.h"

#define EPD_PANEL_WIDTH			250
#define EPD_PANEL_HEIGHT		120
#define EPD_PANEL_NUMOF_COLUMS		250
#define EPD_PANEL_NUMOF_ROWS_PER_PAGE	8
#define EPD_PANEL_NUMOF_PAGES		(EPD_PANEL_HEIGHT / \
					 EPD_PANEL_NUMOF_ROWS_PER_PAGE)

#define SSD1673_PANEL_FIRST_PAGE	0
#define SSD1673_PANEL_LAST_PAGE		(EPD_PANEL_NUMOF_PAGES - 1)
#define SSD1673_PANEL_FIRST_GATE	0
#define SSD1673_PANEL_LAST_GATE		249

struct ssd1673_data {
	struct display_driver_api driver_api;
	struct hal_spi_settings spi_config;
	uint8_t contrast;
	uint8_t scan_mode;
	uint8_t last_lut;
	uint8_t numof_part_cycles;
};

static struct ssd1673_data ssd1673_driver;
static struct os_dev ssd1673;

#define SSD1673_LAST_LUT_INITIAL		0
#define SSD1673_LAST_LUT_DEFAULT		255
#define SSD1673_LUT_SIZE			29

static uint8_t ssd1673_lut_initial[SSD1673_LUT_SIZE] = {
	0x22, 0x55, 0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x11,
	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
	0x1E, 0x1E, 0x1E, 0x1E, 0x1E, 0x1E, 0x1E, 0x1E,
	0x01, 0x00, 0x00, 0x00, 0x00
};

static uint8_t ssd1673_lut_default[SSD1673_LUT_SIZE] = {
	0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
	0x0F, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
	0x00, 0x00, 0x00, 0x00, 0x00
};

#define SSD1673_BUSY_DELAY_TICKS os_time_ms_to_ticks32(SSD1673_BUSY_DELAY)
#define SSD1673_RESET_DELAY_TICKS os_time_ms_to_ticks32(SSD1673_RESET_DELAY)

#define CONFIG_SSD1673_OS_DEV_NAME	MYNEWT_VAL(SSD1673_OS_DEV_NAME)
#define CONFIG_SSD1673_BUSY_PIN		MYNEWT_VAL(SSD1673_BUSY_PIN)
#define CONFIG_SSD1673_RESET_PIN	MYNEWT_VAL(SSD1673_RESET_PIN)
#define CONFIG_SSD1673_DC_PIN		MYNEWT_VAL(SSD1673_DC_PIN)
#define CONFIG_SSD1673_CS_PIN		MYNEWT_VAL(SSD1673_CS_PIN)
#define CONFIG_SSD1673_SPI_FREQ		MYNEWT_VAL(SSD1673_SPI_FREQ)
#define CONFIG_SSD1673_SPI_DEV		MYNEWT_VAL(SSD1673_SPI_DEV)

static inline int ssd1673_write_cmd(struct ssd1673_data *driver,
				    uint8_t cmd, uint8_t *data, size_t len)
{
	hal_gpio_write(CONFIG_SSD1673_DC_PIN, 0);
	hal_gpio_write(CONFIG_SSD1673_CS_PIN, 0);

	if (hal_spi_txrx(CONFIG_SSD1673_SPI_DEV,
			 &cmd, NULL, sizeof(cmd))) {
		hal_gpio_write(CONFIG_SSD1673_CS_PIN, 1);
		return -1;
	}

	if (data != NULL) {
		hal_gpio_write(CONFIG_SSD1673_DC_PIN, 1);
		if (hal_spi_txrx(CONFIG_SSD1673_SPI_DEV,
				 data, NULL, len)) {
			hal_gpio_write(CONFIG_SSD1673_CS_PIN, 1);
			return -1;
		}
	}

	hal_gpio_write(CONFIG_SSD1673_CS_PIN, 1);
	return 0;
}

static inline void ssd1673_busy_wait(struct ssd1673_data *driver)
{
	int val = 0;

	val = hal_gpio_read(CONFIG_SSD1673_BUSY_PIN);
	while (val) {
		os_cputime_delay_ticks(SSD1673_BUSY_DELAY_TICKS);
		val = hal_gpio_read(CONFIG_SSD1673_BUSY_PIN);
	};
}

static inline int ssd1673_set_ram_param(struct ssd1673_data *driver,
					uint8_t sx, uint8_t ex,
					uint8_t sy, uint8_t ey)
{
	uint8_t tmp[2];

	tmp[0] = sx; tmp[1] = ex;
	if (ssd1673_write_cmd(driver, SSD1673_CMD_RAM_XPOS_CTRL,
			      tmp, sizeof(tmp))) {
		return -1;
	}

	tmp[0] = sy; tmp[1] = ey;
	if (ssd1673_write_cmd(driver, SSD1673_CMD_RAM_YPOS_CTRL,
			      tmp, sizeof(tmp))) {
		return -1;
	}

	return 0;
}

static inline int ssd1673_set_ram_ptr(struct ssd1673_data *driver,
				       uint8_t x, uint8_t y)
{
	if (ssd1673_write_cmd(driver, SSD1673_CMD_RAM_XPOS_CNTR,
			      &x, sizeof(x))) {
		return -1;
	}

	if (ssd1673_write_cmd(driver, SSD1673_CMD_RAM_YPOS_CNTR,
			      &y, sizeof(y))) {
		return -1;
	}

	return 0;
}

static inline void ssd1673_set_orientation(struct ssd1673_data *driver)

{
#if CONFIG_SSD1673_ORIENTATION_FLIPPED == 1
	driver->scan_mode = SSD1673_DATA_ENTRY_XIYDY;
#else
	driver->scan_mode = SSD1673_DATA_ENTRY_XDYIY;
#endif
}

int ssd1673_resume(const struct os_dev *dev)
{
	struct ssd1673_data *driver = dev->od_init_arg;
	uint8_t tmp;

	/*
	 * Uncomment for voltage measurement
	 * tmp = SSD1673_CTRL2_ENABLE_CLK;
	 * ssd1673_write_cmd(SSD1673_CMD_UPDATE_CTRL2, &tmp, sizeof(tmp));
	 * ssd1673_write_cmd(SSD1673_CMD_MASTER_ACTIVATION, NULL, 0);
	 */

	tmp = SSD1673_SLEEP_MODE_PON;
	return ssd1673_write_cmd(driver, SSD1673_CMD_SLEEP_MODE,
				 &tmp, sizeof(tmp));
}

static int ssd1673_suspend(const struct os_dev *dev)
{
	struct ssd1673_data *driver = dev->od_init_arg;
	uint8_t tmp = SSD1673_SLEEP_MODE_DSM;

	return ssd1673_write_cmd(driver, SSD1673_CMD_SLEEP_MODE,
				 &tmp, sizeof(tmp));
}

static int ssd1673_update_display(const struct os_dev *dev, bool initial)
{
	struct ssd1673_data *driver = dev->od_init_arg;
	uint8_t tmp;

	tmp = SSD1673_CTRL1_INITIAL_UPDATE_LH;
	if (ssd1673_write_cmd(driver, SSD1673_CMD_UPDATE_CTRL1,
			      &tmp, sizeof(tmp))) {
		return -1;
	}

	if (initial) {
		driver->numof_part_cycles = 0;
		driver->last_lut = SSD1673_LAST_LUT_INITIAL;
		if (ssd1673_write_cmd(driver, SSD1673_CMD_UPDATE_LUT,
				      ssd1673_lut_initial,
				      sizeof(ssd1673_lut_initial))) {
			return -1;
		}

	} else {
		driver->numof_part_cycles++;
		if (driver->last_lut != SSD1673_LAST_LUT_DEFAULT) {
			driver->last_lut = SSD1673_LAST_LUT_DEFAULT;
			if (ssd1673_write_cmd(driver, SSD1673_CMD_UPDATE_LUT,
					      ssd1673_lut_default,
					      sizeof(ssd1673_lut_default))) {
				return -1;
			}
		}
	}

	tmp = (SSD1673_CTRL2_ENABLE_CLK |
	       SSD1673_CTRL2_ENABLE_ANALOG |
	       SSD1673_CTRL2_TO_PATTERN |
	       SSD1673_CTRL2_DISABLE_ANALOG |
	       SSD1673_CTRL2_DISABLE_CLK);
	if (ssd1673_write_cmd(driver, SSD1673_CMD_UPDATE_CTRL2,
			      &tmp, sizeof(tmp))) {
		return -1;
	}

	if (ssd1673_write_cmd(driver, SSD1673_CMD_MASTER_ACTIVATION,
			      NULL, 0)) {
		return -1;
	}

	return 0;
}

static int ssd1673_write(const struct os_dev *dev, const uint16_t x,
			 const uint16_t y,
			 const struct display_buffer_descriptor *desc,
			 const void *buf)
{
	struct ssd1673_data *driver = dev->od_init_arg;
	uint8_t cmd = SSD1673_CMD_WRITE_RAM;
	uint8_t dummy_page[SSD1673_RAM_YRES];
	bool update = true;

	if (desc->pitch < desc->width) {
		DFLT_LOG_ERROR("Pitch is smaller then width");
		return -1;
	}

	if (buf == NULL || desc->buf_size == 0) {
		DFLT_LOG_ERROR("Display buffer is not available");
		return -1;
	}

	if (desc->pitch > desc->width) {
		DFLT_LOG_ERROR("Unsupported mode");
		return -1;
	}

	if (x != 0 && y != 0) {
		DFLT_LOG_ERROR("Unsupported origin");
		return -1;
	}


	ssd1673_busy_wait(driver);
	memset(dummy_page, 0xff, sizeof(dummy_page));

	switch (driver->scan_mode) {
	case SSD1673_DATA_ENTRY_XIYDY:
		if (ssd1673_set_ram_param(driver,
					  SSD1673_PANEL_FIRST_PAGE,
					  SSD1673_PANEL_LAST_PAGE + 1,
					  SSD1673_PANEL_LAST_GATE,
					  SSD1673_PANEL_FIRST_GATE)) {
			return -1;
		}

		if (ssd1673_set_ram_ptr(driver,
					SSD1673_PANEL_FIRST_PAGE,
					SSD1673_PANEL_LAST_GATE)) {
			return -1;
		}

		break;

	case SSD1673_DATA_ENTRY_XDYIY:
		if (ssd1673_set_ram_param(driver,
					  SSD1673_PANEL_LAST_PAGE + 1,
					  SSD1673_PANEL_FIRST_PAGE,
					  SSD1673_PANEL_FIRST_GATE,
					  SSD1673_PANEL_LAST_GATE)) {
			return -1;
		}

		if (ssd1673_set_ram_ptr(driver,
					SSD1673_PANEL_LAST_PAGE + 1,
					SSD1673_PANEL_FIRST_GATE)) {
			return -1;
		}

		break;
	default:
		return -1;
	}

	if (ssd1673_write_cmd(driver, SSD1673_CMD_ENTRY_MODE,
			      &driver->scan_mode, sizeof(driver->scan_mode))) {
		return -1;
	}

	hal_gpio_write(CONFIG_SSD1673_DC_PIN, 0);
	hal_gpio_write(CONFIG_SSD1673_CS_PIN, 0);

	if (hal_spi_txrx(CONFIG_SSD1673_SPI_DEV, &cmd, NULL, sizeof(cmd))) {
		hal_gpio_write(CONFIG_SSD1673_CS_PIN, 1);
		return -1;
	}

	hal_gpio_write(CONFIG_SSD1673_DC_PIN, 1);
	/* clear unusable page */
	if (driver->scan_mode == SSD1673_DATA_ENTRY_XDYIY) {
		if (hal_spi_txrx(CONFIG_SSD1673_SPI_DEV, dummy_page,
				 NULL, sizeof(dummy_page))) {
			hal_gpio_write(CONFIG_SSD1673_CS_PIN, 1);
			return -1;
		}
	}

	if (hal_spi_txrx(CONFIG_SSD1673_SPI_DEV, (uint8_t *)buf,
			 NULL, desc->buf_size)) {
		hal_gpio_write(CONFIG_SSD1673_CS_PIN, 1);
		return -1;
	}

	/* clear unusable page */
	if (driver->scan_mode == SSD1673_DATA_ENTRY_XIYDY) {
		if (hal_spi_txrx(CONFIG_SSD1673_SPI_DEV, dummy_page,
				 NULL, sizeof(dummy_page))) {
			hal_gpio_write(CONFIG_SSD1673_CS_PIN, 1);
			return -1;
		}
	}

	hal_gpio_write(CONFIG_SSD1673_CS_PIN, 1);

	if (update) {
		if (driver->contrast) {
			return ssd1673_update_display(dev, true);
		}
		return ssd1673_update_display(dev, false);
	}

	return 0;
}

static int ssd1673_read(const struct os_dev *dev, const uint16_t x,
			const uint16_t y,
			const struct display_buffer_descriptor *desc,
			void *buf)
{
	DFLT_LOG_ERROR("not supported");
	return -1;
}

static void *ssd1673_get_framebuffer(const struct os_dev *dev)
{
	DFLT_LOG_ERROR("not supported");
	return NULL;
}

static int ssd1673_set_brightness(const struct os_dev *dev,
				  const uint8_t brightness)
{
	DFLT_LOG_WARN("not supported");
	return -1;
}

static int ssd1673_set_contrast(const struct os_dev *dev, uint8_t contrast)
{
	struct ssd1673_data *driver = dev->od_init_arg;

	driver->contrast = contrast;

	return 0;
}

static void ssd1673_get_capabilities(const struct os_dev *dev,
				     struct display_capabilities *caps)
{
	memset(caps, 0, sizeof(struct display_capabilities));
	caps->x_resolution = EPD_PANEL_WIDTH;
	caps->y_resolution = EPD_PANEL_HEIGHT;
	caps->supported_pixel_formats = PIXEL_FORMAT_MONO10;
	caps->current_pixel_format = PIXEL_FORMAT_MONO10;
	caps->screen_info = SCREEN_INFO_MONO_VTILED |
			    SCREEN_INFO_MONO_MSB_FIRST |
			    SCREEN_INFO_EPD;
}

static int ssd1673_set_pixel_format(const struct os_dev *dev,
				    const enum display_pixel_format pf)
{
	DFLT_LOG_ERROR("not supported");
	return -1;
}

static int ssd1673_controller_init(struct os_dev *dev)
{
	struct ssd1673_data *driver = dev->od_init_arg;
	uint8_t tmp[3];

	DFLT_LOG_DEBUG("");

	hal_gpio_write(CONFIG_SSD1673_RESET_PIN, 0);
	os_time_delay(SSD1673_RESET_DELAY_TICKS);
	hal_gpio_write(CONFIG_SSD1673_RESET_PIN, 1);
	os_time_delay(SSD1673_RESET_DELAY_TICKS);
	ssd1673_busy_wait(driver);

	if (ssd1673_write_cmd(driver, SSD1673_CMD_SW_RESET, NULL, 0)) {
		return -1;
	}
	ssd1673_busy_wait(driver);

	tmp[0] = (SSD1673_RAM_YRES - 1);
	tmp[1] = 0;
	if (ssd1673_write_cmd(driver, SSD1673_CMD_GDO_CTRL, tmp, 2)) {
		return -1;
	}

	tmp[0] = SSD1673_VAL_GDV_CTRL_A;
	tmp[1] = SSD1673_VAL_GDV_CTRL_B;
	if (ssd1673_write_cmd(driver, SSD1673_CMD_GDV_CTRL, tmp, 2)) {
		return -1;
	}

	tmp[0] = SSD1673_VAL_SDV_CTRL;
	if (ssd1673_write_cmd(driver, SSD1673_CMD_SDV_CTRL, tmp, 1)) {
		return -1;
	}

	tmp[0] = SSD1673_VAL_VCOM_VOLTAGE;
	if (ssd1673_write_cmd(driver, SSD1673_CMD_VCOM_VOLTAGE, tmp, 1)) {
		return -1;
	}

	tmp[0] = SSD1673_VAL_DUMMY_LINE;
	if (ssd1673_write_cmd(driver, SSD1673_CMD_DUMMY_LINE, tmp, 1)) {
		return -1;
	}

	tmp[0] = SSD1673_VAL_GATE_LWIDTH;
	if (ssd1673_write_cmd(driver, SSD1673_CMD_GATE_LINE_WIDTH, tmp, 1)) {
		return -1;
	}

	ssd1673_set_orientation(driver);
	driver->numof_part_cycles = 0;
	driver->last_lut = SSD1673_LAST_LUT_INITIAL;
	driver->contrast = 0;

	return 0;
}

static int ssd1673_init(struct os_dev *dev, void *arg)
{
	struct ssd1673_data *driver = dev->od_init_arg;
	int rc;

	DFLT_LOG_DEBUG("");

	driver->spi_config.baudrate = CONFIG_SSD1673_SPI_FREQ;
	driver->spi_config.data_mode = HAL_SPI_MODE0;
	driver->spi_config.data_order = HAL_SPI_MSB_FIRST;
	driver->spi_config.word_size = HAL_SPI_WORD_SIZE_8BIT;
	rc = hal_spi_config(0, &driver->spi_config);
	assert(rc == 0);

	driver->driver_api.blanking_on = ssd1673_resume,
	driver->driver_api.blanking_off = ssd1673_suspend,
	driver->driver_api.write = ssd1673_write,
	driver->driver_api.read = ssd1673_read,
	driver->driver_api.get_framebuffer = ssd1673_get_framebuffer,
	driver->driver_api.set_brightness = ssd1673_set_brightness,
	driver->driver_api.set_contrast = ssd1673_set_contrast,
	driver->driver_api.get_capabilities = ssd1673_get_capabilities,
	driver->driver_api.set_pixel_format = ssd1673_set_pixel_format,
	driver->driver_api.set_orientation = NULL,

	rc = hal_gpio_init_out(CONFIG_SSD1673_RESET_PIN, 1);
	assert(rc == 0);
	rc = hal_gpio_init_out(CONFIG_SSD1673_DC_PIN, 1);
	assert(rc == 0);
	rc = hal_gpio_init_out(CONFIG_SSD1673_CS_PIN, 1);
	assert(rc == 0);
	rc = hal_gpio_init_in(CONFIG_SSD1673_BUSY_PIN, HAL_GPIO_PULL_NONE);
	assert(rc == 0);

	ssd1673_controller_init(dev);

	return 0;
}

int ssd1673_pkg_init(void)
{
	int rc;

	/* Ensure this function only gets called by sysinit. */
	SYSINIT_ASSERT_ACTIVE();

	rc = os_dev_create(&ssd1673, CONFIG_SSD1673_OS_DEV_NAME,
			   OS_DEV_INIT_SECONDARY, OS_DEV_INIT_PRIO_DEFAULT,
			   ssd1673_init, &ssd1673_driver);
	SYSINIT_PANIC_ASSERT(rc == 0);

	return 0;
}
