/*
 * Display.cpp
 *
 *  Created on: Aug 12, 2023
 *      Author: Carst
 */

#include <cstring>
#include <stm32g0xx_ll_gpio.h>
#include "STM32G071KBT6.hpp"
#include "Display.hpp"

#define pulse_delay() for(uint32_t i = 0UL; i < 1UL; i++);
#define PIN_RS LL_GPIO_PIN_4
#define PIN_RW LL_GPIO_PIN_5
#define PIN_E LL_GPIO_PIN_6
#define PIN_BL LL_GPIO_PIN_7

namespace ElektronischeLast
{
  void Display::init(void)
  {
    LL_GPIO_InitTypeDef init =
    {
      .Pin = LL_GPIO_PIN_0 | LL_GPIO_PIN_1 | LL_GPIO_PIN_2 | LL_GPIO_PIN_3 |
             PIN_RS | PIN_RW | PIN_E | PIN_BL,
      .Mode = LL_GPIO_MODE_OUTPUT,
      .Speed = LL_GPIO_SPEED_FREQ_LOW,
      .OutputType = LL_GPIO_OUTPUT_PUSHPULL,
      .Pull = LL_GPIO_PULL_NO,
    };
    LL_GPIO_Init(GPIOB, &init);

    this->set_backlight(true);
    this->current_state = StateDelay;
    this->next_state = StateInit01;
    this->start_timer(15UL); // 15ms Startup Delay
  }

  void Display::run(void)
  {
    switch (this->current_state)
    {
      case StateIdle:
        if(this->new_data)
        {
          this->new_data = false;
          this->current_state = StateWriteData;
        }
        break;
      case StateDelay:
        if(this->timer_elapsed())
        {
          this->current_state = this->next_state;
          this->run();
        }
        break;
      case StateWaitWhileBusy:
        break;
      case StateInit01:
        write_command_8bit_4pin(0x30u);
        this->start_timer(5UL); // 5ms Delay
        this->next_state = StateInit02;
        break;
      case StateInit02:
        write_command_8bit_4pin(0x30u);
        this->start_timer(1UL); // 1ms Delay
        this->next_state = StateInit03;
        break;
      case StateInit03:
        write_command_8bit_4pin(0x30u);
        this->start_timer(1UL); // 1ms Delay
        this->next_state = StateInit04;
        break;
      case StateInit04:
        // 4-bit mode
        write_command_8bit_4pin(0x20u);
        this->start_timer(1UL);
        this->next_state = StateInit05;
        break;
      case StateInit05:
        // 4-bit mode, 2 lines
        write_command_4bit(0x28u);
        this->start_timer(1UL);
        this->next_state = StateInit06;
        break;
      case StateInit06:
        // Display off
        write_command_4bit(0x08u);
        this->start_timer(1UL);
        this->next_state = StateInit07;
        break;
      case StateInit07:
        // Clear Display
        write_command_4bit(0x01u);
        this->start_timer(1UL);
        this->next_state = StateInit08;
        break;
      case StateInit08:
        // increment DDRAM
        write_command_4bit(0x06u);
        this->start_timer(1UL);
        this->next_state = StateInit09;
        break;
      case StateInit09:
        // return cursor home
        write_command_4bit(0x02u);
        this->start_timer(1UL);
        this->next_state = StateInit10;
        break;
      case StateInit10:
        // Display on, Cursor on, Blink on
        write_command_4bit(0x0Fu);
        this->start_timer(1UL);
        this->next_state = StateIdle;
        break;
      case StateWriteData:
        write_data_4bit((std::uint8_t)this->string[this->pointer++]);
        if(this->string[this->pointer] == '\0')
        {
          this->next_state = StateIdle;
        }
        else
        {
          this->next_state = StateWriteData;
        }
        break;
      default:
        break;
    }
  }

  void Display::set_backlight(bool on)
  {
    if(on)
    {
      LL_GPIO_SetOutputPin(GPIOB, PIN_BL);
    }
    else
    {
      LL_GPIO_ResetOutputPin(GPIOB, PIN_BL);
    }
  }

  void Display::set_cursor(Line_t line, std::uint32_t position)
  {
    lcd_cmd(0x80u | line | (position & 0x7F));
  }

  void Display::print(const char* const string)
  {
    strncpy(this->string, string, sizeof(this->string));
    this->pointer = 0U;
    this->new_data = true;
  }

  bool Display::ready_for_data(void)
  {
    return (this->current_state == StateIdle);
  }

  bool Display::timer_elapsed(void)
  {
    return (this->timer < systick);
  }

  void Display::start_timer(std::uint32_t timeout)
  {
    this->timer = systick + 1U + timeout; // +1 damit auch mindestens 1ms vorbei geht.
    this->current_state = StateDelay;
  }

  void Display::write_command_8bit_4pin(std::uint32_t data)
  {
    // RS=low, RW=low
    LL_GPIO_ResetOutputPin(GPIOB, PIN_RS | PIN_RW | PIN_E);
    std::uint32_t tmp = data >> 4U;
    tmp |= ((~(data >> 4U) & 0xFU) << GPIO_BSRR_BR0_Pos);
    LL_GPIO_SetOutputPin(GPIOB, tmp);
    __NOP();
    LL_GPIO_SetOutputPin(GPIOB, PIN_E);
    pulse_delay();
    LL_GPIO_ResetOutputPin(GPIOB, PIN_E);
  }

  void Display::write_command_4bit(std::uint32_t command)
  {
    // RS=low, RW=low
    LL_GPIO_ResetOutputPin(GPIOB, PIN_RS | PIN_RW | PIN_E);
    std::uint32_t tmp = command >> 4U;
    tmp |= ((~(command >> 4U) & 0xFU) << GPIO_BSRR_BR0_Pos);
    LL_GPIO_SetOutputPin(GPIOB, tmp);
    __NOP();
    LL_GPIO_SetOutputPin(GPIOB, PIN_E);
    pulse_delay();
    LL_GPIO_ResetOutputPin(GPIOB, PIN_E);
    tmp = command & 0x0FU;
    tmp |= ((~command & 0xFU) << GPIO_BSRR_BR0_Pos);
    LL_GPIO_SetOutputPin(GPIOB, tmp);
    __NOP();
    LL_GPIO_SetOutputPin(GPIOB, PIN_E);
    pulse_delay();
    LL_GPIO_ResetOutputPin(GPIOB, PIN_E);
  }

  /// \brief write command to lcd and wait until busy is gone
  /// \param[in] cmd command to transfer
  void Display::lcd_cmd(uint8_t cmd)
  {
    write_command_4bit(cmd);
    this->start_timer(1UL);
  }

  /// \brief write data to display
  /// \param[in] data data which to write to display
  void Display::lcd_data(uint8_t data)
  {
    write_data_4bit(data);
    this->start_timer(1UL);
  }

  /// \brief write data to lcd in 4 bit mode
  /// \details write data given by \p data to lcd.
  /// First write upper nibble and in second step lower nibble
  /// \param[in] data data to transfer
  void Display::write_data_4bit(uint8_t data)
  {
    // RS=high, RW=low
    LL_GPIO_SetOutputPin(GPIOB, PIN_RS | PIN_RW  << GPIO_BSRR_BR0_Pos | PIN_E << GPIO_BSRR_BR0_Pos);
    std::uint32_t tmp = data >> 4U;
    tmp |= ((~(data >> 4U) & 0xFU) << GPIO_BSRR_BR0_Pos);
    LL_GPIO_SetOutputPin(GPIOB, tmp);
    __NOP();
    LL_GPIO_SetOutputPin(GPIOB, PIN_E);
    pulse_delay();
    LL_GPIO_ResetOutputPin(GPIOB, PIN_E);
    tmp = data & 0x0FU;
    tmp |= ((~data & 0xFU) << GPIO_BSRR_BR0_Pos);
    LL_GPIO_SetOutputPin(GPIOB, tmp);
    __NOP();
    LL_GPIO_SetOutputPin(GPIOB, PIN_E);
    pulse_delay();
    LL_GPIO_ResetOutputPin(GPIOB, PIN_E);
    this->start_timer(1UL);
  }
}