// Copyright 2020 Espressif Systems (Shanghai) Co. Ltd.
//
// 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 <stdio.h>
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"
#include "driver/gpio.h"
#include "screen/screen_driver.h"
#include "screen/screen_utility/screen_utility.h"
#include "ssd1322.h"

static const char *TAG = "lcd ssd1322";

#define LCD_CHECK(a, str, ret)  if(!(a)) {                               \
        ESP_LOGE(TAG,"%s:%d (%s):%s", __FILE__, __LINE__, __FUNCTION__, str);   \
        return (ret);                                                           \
    }

// Some fundamental define for screen controller
#define SSD1322_HEIGHT                         128
#define SSD1322_WIDTH                          480
#define SSD1322_COLUMNS                        120
#define SSD1322_BITS_PER_PIXEL                 4

// Commands
#define SSD1322_SETCOMMANDLOCK            0xFD
#define SSD1322_DISPLAYOFF                0xAE
#define SSD1322_DISPLAYON                 0xAF
#define SSD1322_SETCLOCKDIVIDER           0xB3
#define SSD1322_SETDISPLAYOFFSET          0xA2
#define SSD1322_SETSTARTLINE              0xA1
#define SSD1322_SETREMAP                  0xA0
#define SSD1322_FUNCTIONSEL               0xAB
#define SSD1322_DISPLAYENHANCE            0xB4
#define SSD1322_SETCONTRASTCURRENT        0xC1
#define SSD1322_MASTERCURRENTCONTROL      0xC7
#define SSD1322_SETPHASELENGTH            0xB1
#define SSD1322_DISPLAYENHANCEB           0xD1
#define SSD1322_SETPRECHARGEVOLTAGE       0xBB
#define SSD1322_SETSECONDPRECHARGEPERIOD  0xB6
#define SSD1322_SETVCOMH                  0xBE
#define SSD1322_SETMUXRATIO               0xCA
#define SSD1322_SETCOLUMNADDR             0x15
#define SSD1322_SETROWADDR                0x75
#define SSD1322_WRITERAM                  0x5C
#define SSD1322_ENTIREDISPLAYOFF          0xA4
#define SSD1322_ENTIREDISPLAYON           0xA5
#define SSD1322_NORMALDISPLAY             0xA6
#define SSD1322_INVERSEDISPLAY            0xA7
#define SSD1322_SETGPIO                   0xB5
#define SSD1322_EXITPARTIALDISPLAY        0xA9
#define SSD1322_SELECTDEFAULTGRAYSCALE    0xB9

#define LCD_NAME "OLED SSD1322"
#define LCD_BPP  SSD1322_BITS_PER_PIXEL

static scr_handle_t g_lcd_handle;

/**
 * This header file is only used to redefine the function to facilitate the call.
 * It can only be placed in this position, not in the head of the file.
 */
#include "screen/screen_utility/screen_utility.h"

static esp_err_t lcd_ssd1322_write_ram_data(uint16_t color);

scr_driver_t lcd_ssd1322_default_driver = {
    .init = lcd_ssd1322_init,
    .deinit = lcd_ssd1322_deinit,
    .set_direction = lcd_ssd1322_set_rotate,
    .set_window = lcd_ssd1322_set_window,
    .write_ram_data = lcd_ssd1322_write_ram_data,
    .draw_pixel = lcd_ssd1322_draw_pixel,
    .draw_bitmap = lcd_ssd1322_draw_bitmap,
    .get_info = lcd_ssd1322_get_info,
};

esp_err_t lcd_ssd1322_init(const scr_controller_config_t *lcd_conf)
{
    LCD_CHECK(lcd_conf->width <= SSD1322_WIDTH, "Width greater than maximum", ESP_ERR_INVALID_ARG);
    LCD_CHECK(lcd_conf->height <= SSD1322_HEIGHT, "Height greater than maximum", ESP_ERR_INVALID_ARG);
    // Reset the display
    if (lcd_conf->pin_num_rst >= 0) {
        gpio_pad_select_gpio(lcd_conf->pin_num_rst);
        gpio_set_direction(lcd_conf->pin_num_rst, GPIO_MODE_OUTPUT);
        gpio_set_level(lcd_conf->pin_num_rst, (lcd_conf->rst_active_level) & 0x1);
        vTaskDelay(100 / portTICK_RATE_MS);
        gpio_set_level(lcd_conf->pin_num_rst, (~(lcd_conf->rst_active_level)) & 0x1);
        vTaskDelay(100 / portTICK_RATE_MS);
    }
    g_lcd_handle.interface_drv = lcd_conf->interface_drv;
    g_lcd_handle.original_width = lcd_conf->width;
    g_lcd_handle.original_height = lcd_conf->height;
    g_lcd_handle.offset_hor = lcd_conf->offset_hor;
    g_lcd_handle.offset_ver = lcd_conf->offset_ver;

    LCD_WRITE_CMD(SSD1322_SETCOMMANDLOCK);// 0xFD
    LCD_WRITE_DATA(0x12);// Unlock OLED driver IC

    LCD_WRITE_CMD(SSD1322_DISPLAYOFF);// 0xAE

    LCD_WRITE_CMD(SSD1322_SETCLOCKDIVIDER);// 0xB3
    LCD_WRITE_DATA(0x91);// 0xB3

    LCD_WRITE_CMD(SSD1322_SETMUXRATIO);// 0xCA
    LCD_WRITE_DATA(0x3F);// duty = 1/64

    LCD_WRITE_CMD(SSD1322_SETDISPLAYOFFSET);// 0xA2
    LCD_WRITE_DATA(0x00);

    LCD_WRITE_CMD(SSD1322_SETSTARTLINE);// 0xA1
    LCD_WRITE_DATA(0x00);

    LCD_WRITE_CMD(SSD1322_SETREMAP);// 0xA0
    LCD_WRITE_DATA(0x14);//Horizontal address increment,Disable Column Address Re-map,Enable Nibble Re-map,Scan from COM[N-1] to COM0,Disable COM Split Odd Even
    LCD_WRITE_DATA(0x11);//Enable Dual COM mode

    LCD_WRITE_CMD(SSD1322_SETGPIO);// 0xB5
    LCD_WRITE_DATA(0x00);// Disable GPIO Pins Input

    LCD_WRITE_CMD(SSD1322_FUNCTIONSEL);// 0xAB
    LCD_WRITE_DATA(0x01);// selection external vdd

    LCD_WRITE_CMD(SSD1322_DISPLAYENHANCE);// 0xB4
    LCD_WRITE_DATA(0xA0);// enables the external VSL
    LCD_WRITE_DATA(0xFD);// 0xfFD,Enhanced low GS display quality;default is 0xb5(normal),

    LCD_WRITE_CMD(SSD1322_SETCONTRASTCURRENT);// 0xC1
    LCD_WRITE_DATA(0xFF);// 0xFF - default is 0x7f

    LCD_WRITE_CMD(SSD1322_MASTERCURRENTCONTROL);// 0xC7
    LCD_WRITE_DATA(0x0F);// default is 0x0F

    // Set grayscale
    LCD_WRITE_CMD(SSD1322_SELECTDEFAULTGRAYSCALE); // 0xB9

    LCD_WRITE_CMD(SSD1322_SETPHASELENGTH);// 0xB1
    LCD_WRITE_DATA(0xE2);// default is 0x74

    LCD_WRITE_CMD(SSD1322_DISPLAYENHANCEB);// 0xD1
    LCD_WRITE_DATA(0x82);// Reserved;default is 0xa2(normal)
    LCD_WRITE_DATA(0x20);//

    LCD_WRITE_CMD(SSD1322_SETPRECHARGEVOLTAGE);// 0xBB
    LCD_WRITE_DATA(0x1F);// 0.6xVcc

    LCD_WRITE_CMD(SSD1322_SETSECONDPRECHARGEPERIOD);// 0xB6
    LCD_WRITE_DATA(0x08);// default

    LCD_WRITE_CMD(SSD1322_SETVCOMH);// 0xBE
    LCD_WRITE_DATA(0x07);// 0.86xVcc;default is 0x04

    LCD_WRITE_CMD(SSD1322_NORMALDISPLAY);// 0xA6

    LCD_WRITE_CMD(SSD1322_EXITPARTIALDISPLAY);// 0xA9
    LCD_WRITE_CMD(SSD1322_DISPLAYON);   //Sleep Out

    lcd_ssd1322_set_rotate(lcd_conf->rotate);
    return ESP_OK;
}

esp_err_t lcd_ssd1322_deinit(void)
{
    memset(&g_lcd_handle, 0, sizeof(scr_handle_t));
    return ESP_OK;
}

esp_err_t lcd_ssd1322_get_info(scr_info_t *info)
{
    LCD_CHECK(NULL != info, "info pointer invalid", ESP_ERR_INVALID_ARG);
    info->width = g_lcd_handle.width;
    info->height = g_lcd_handle.height;
    info->dir = g_lcd_handle.dir;
    info->name = LCD_NAME;
    info->color_type = SCR_COLOR_TYPE_GRAY;
    info->bpp = LCD_BPP;
    return ESP_OK;
}

esp_err_t lcd_ssd1322_set_rotate(scr_dir_t dir)
{
    esp_err_t ret = ESP_OK;
    if (SCR_DIR_MAX < dir) {
        dir >>= 5;
    }
    LCD_CHECK(dir < 8, "Unsupport rotate direction", ESP_ERR_INVALID_ARG);
    uint8_t reg_data = 0x04;
    switch (dir) {
    case SCR_DIR_LRTB:
        reg_data |= 0x00;
        g_lcd_handle.width = g_lcd_handle.original_width;
        g_lcd_handle.height = g_lcd_handle.original_height;
        break;
    case SCR_DIR_LRBT:
        reg_data |= 0x10;
        g_lcd_handle.width = g_lcd_handle.original_width;
        g_lcd_handle.height = g_lcd_handle.original_height;
        break;
    case SCR_DIR_RLTB:
        reg_data |= 0x02;
        g_lcd_handle.width = g_lcd_handle.original_width;
        g_lcd_handle.height = g_lcd_handle.original_height;
        break;
    case SCR_DIR_RLBT:
        reg_data |= 0x12;
        g_lcd_handle.width = g_lcd_handle.original_width;
        g_lcd_handle.height = g_lcd_handle.original_height;
        break;
    default:
        ESP_LOGE(TAG, "Unsupport rotate direction");
        g_lcd_handle.width = g_lcd_handle.original_width;
        g_lcd_handle.height = g_lcd_handle.original_height;
        return ESP_ERR_INVALID_ARG;
    }
    ESP_LOGI(TAG, "Set rotate 0x%x", reg_data);
    ret |= LCD_WRITE_CMD(SSD1322_SETREMAP);
    ret |= LCD_WRITE_DATA(reg_data);
    ret |= LCD_WRITE_DATA(0x11);
    LCD_CHECK(ESP_OK == ret, "Set screen rotate failed", ESP_FAIL);
    g_lcd_handle.dir = dir;
    return ESP_OK;
}

esp_err_t lcd_ssd1322_set_window(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1)
{
    esp_err_t ret = ESP_OK;
    x1 += 1;
    LCD_CHECK((0 == (x0 % 4)), "x0 should be a multiple of 4", ESP_ERR_INVALID_ARG);
    LCD_CHECK((0 == (x1 % 4)), "(x1+1) should be a multiple of 4", ESP_ERR_INVALID_ARG);
    uint8_t col0 = x0 / 4;
    uint8_t col1 = x1 / 4;
    ret |= LCD_WRITE_CMD(SSD1322_SETCOLUMNADDR);
    ret |= LCD_WRITE_DATA(0x1c + col0);
    ret |= LCD_WRITE_DATA(0x1c + col1 - 1);
    ret |= LCD_WRITE_CMD(SSD1322_SETROWADDR);
    ret |= LCD_WRITE_DATA(y0);
    ret |= LCD_WRITE_DATA(y1);
    ret |= LCD_WRITE_CMD(SSD1322_WRITERAM);
    LCD_CHECK(ESP_OK == ret, "Set window failed", ESP_FAIL);
    return ESP_OK;
}

static esp_err_t lcd_ssd1322_write_ram_data(uint16_t color)
{
    ESP_LOGW(TAG, "Unsupport write ram data");
    return ESP_ERR_NOT_SUPPORTED;
}

esp_err_t lcd_ssd1322_draw_pixel(uint16_t x, uint16_t y, uint16_t color)
{
    ESP_LOGW(TAG, "Unsupport draw pixel without buffer");
    return ESP_ERR_NOT_SUPPORTED;
}

esp_err_t lcd_ssd1322_draw_bitmap(uint16_t x, uint16_t y, uint16_t w, uint16_t h, uint16_t *bitmap)
{
    LCD_CHECK((x + w <= g_lcd_handle.width) && (y + h <= g_lcd_handle.height), "The set coordinates exceed the screen size", ESP_ERR_INVALID_ARG);
    esp_err_t ret = ESP_OK;
    uint8_t *p = (uint8_t *)bitmap;

    ret = lcd_ssd1322_set_window(x, y, x + w - 1, y + h - 1);
    if (ESP_OK != ret) {
        return ESP_FAIL;
    }
    ret = LCD_WRITE(p, w * LCD_BPP / 8 * h);
    LCD_CHECK(ESP_OK == ret, "Draw bitmap failed", ESP_FAIL);
    return ESP_OK;
}

esp_err_t lcd_ssd1322_set_contrast(uint8_t contrast)
{
    esp_err_t ret;
    ret = LCD_WRITE_CMD(SSD1322_SETCONTRASTCURRENT);
    ret |= LCD_WRITE_DATA(contrast);
    LCD_CHECK(ESP_OK == ret, "Set contrast failed", ESP_FAIL);
    return ESP_OK;
}

esp_err_t lcd_ssd1322_set_invert(uint8_t is_invert)
{
    esp_err_t ret;
    ret = LCD_WRITE_CMD(is_invert ? SSD1322_INVERSEDISPLAY : SSD1322_NORMALDISPLAY);
    LCD_CHECK(ESP_OK == ret, "Set contrast failed", ESP_FAIL);
    return ESP_OK;
}
