#include <avr/io.h>
#include <util/delay.h>
#include <avr/interrupt.h>
#include <util/twi.h>

#define led_toggle() (PORTB ^= _BV(PB5))

static void blink(void)
{
  DDRB |= _BV(DDB5);

  for (uint8_t i = 0; i < 6; i++) {
    led_toggle();
    _delay_ms(400);
  }
}

/* I2C */

#define I2C_FREQ_HZ 100000U
#define I2C_PRESCALER 4U

#define I2C_WRITE 0x00U
#define I2C_READ  0x01U

void i2c_init(void)
{
  /* Prescaler: 4 */
  TWSR |= _BV(TWPS0);
  /* Bit rate: */
  TWBR = (F_CPU / I2C_FREQ_HZ - 16) / (2 * I2C_PRESCALER);
}

static int8_t i2c_rw(uint8_t addr, uint8_t data, uint8_t rw)
{
  /* Send START condition */
  TWCR = _BV(TWINT) | _BV(TWSTA) | _BV(TWEN);
  loop_until_bit_is_set(TWCR, TWINT);
  if ((TWSR & 0xF8) != TW_START)  {
    return -1;
  }
  /* Start transmission of address */
  TWDR = ((addr << 1) | rw);
  TWCR = _BV(TWINT) | _BV(TWEN);
  loop_until_bit_is_set(TWCR, TWINT);
  if ((TWSR) != TW_MT_SLA_ACK) {
    return -1;
  }
  /* Start transmission of data */
  TWDR = data;
  TWCR = _BV(TWINT) | _BV(TWEN);
  loop_until_bit_is_set(TWCR, TWINT);
  if ((TWSR & 0xF8) != TW_MT_DATA_ACK) {
    return -1;
  }
  /* Transmit STOP condition */
  TWCR = _BV(TWINT) | _BV(TWEN) | _BV(TWSTO);

  return 0;
}

int8_t i2c_read(uint8_t addr, uint8_t data)
{
  return i2c_rw(addr, data, I2C_READ);
}

int8_t i2c_write(uint8_t addr, uint8_t data)
{
  return i2c_rw(addr, data, I2C_WRITE);
}

/* LCD */

/* LCD pins and address definitions */
#define LCD_I2C_ADDR  0x27
#define LCD_BACKLIGHT 0x08
#define LCD_PIN_EN    0x04
#define LCD_PIN_RS    0x01

/* Define LCD display commands */
#define LCD_CMD_CLEAR           0x01 /* Clear the display */
#define LCD_CMD_CURSOR_HOME     0x02 /* Set cursor to the home (0,0) position */
#define LCD_CMD_CURSOR_LSHIFT   0x04 /* Shift cursor left */
#define LCD_CMD_CURSOR_RSHIFT   0x06 /* Shift cursor right */
#define LCD_CMD_DISPLAY_RSHIFT  0x05 /* Shift display right */
#define LCD_CMD_DISPLAY_LSHIFT  0x07 /* Shift display left */
#define LCD_CMD_D_OFF_C_OFF     0x08 /* Display OFF, cursor OFF */
#define LCD_CMD_D_OFF_C_ON      0x0A /* Display OFF, cursor ON */
#define LCD_CMD_D_ON_C_OFF      0x0C /* Display ON, cursor OFF */
#define LCD_CMD_D_ON_C_BLK      0x0E /* Display ON, cursor blinking */
#define LCD_CMD_CURSOR_ROW1     0x80 /* Set the cursor to the first row */
#define LCD_CMD_CURSOR_ROW2     0xC0 /* Set the cursor to the second row */
#define LCD_CMD_4BIT_MODE       0x28 /* Set LCD to operate in 4-bit mode */
#define LCD_CMD_8BIT_MODE       0x38 /* Set LCD to operate in 8-bit mode */

static void lcd_send_cmd(uint8_t cmd)
{
  uint8_t upper_nibble = cmd & 0xF0;
  uint8_t lower_nibble = (cmd << 4) & 0xF0;

  i2c_write(LCD_I2C_ADDR, upper_nibble | 0x0C);
  i2c_write(LCD_I2C_ADDR, upper_nibble | 0x08);
  i2c_write(LCD_I2C_ADDR, lower_nibble | 0x0C);
  i2c_write(LCD_I2C_ADDR, lower_nibble | 0x08);
}

static void lcd_send_data(uint8_t data)
{
  uint8_t upper_nibble = data & 0xF0;
  uint8_t lower_nibble = (data << 4) & 0xF0;

  i2c_write(LCD_I2C_ADDR, upper_nibble | 0x0D);
  i2c_write(LCD_I2C_ADDR, upper_nibble | 0x09);
  i2c_write(LCD_I2C_ADDR, lower_nibble | 0x0D);
  i2c_write(LCD_I2C_ADDR, lower_nibble | 0x09);
}

void lcd_init(void)
{
  _delay_ms(50);
  lcd_send_cmd(0x30);
  _delay_ms(5);
  lcd_send_cmd(0x30);
  _delay_us(101);
  lcd_send_cmd(0x30);
  _delay_ms(10);
  lcd_send_cmd(0x20);
  _delay_ms(10);

  lcd_send_cmd(LCD_CMD_4BIT_MODE);
  _delay_ms(10);
  lcd_send_cmd(LCD_CMD_D_OFF_C_OFF);
  _delay_ms(10);
  lcd_send_cmd(LCD_CMD_CLEAR);
  _delay_ms(10);
  lcd_send_cmd(LCD_CMD_CURSOR_RSHIFT);
  _delay_ms(10);
  lcd_send_cmd(LCD_CMD_D_ON_C_OFF);
  _delay_ms(10);
}

void lcd_set_pos(uint8_t row, uint8_t col)
{
  if (row)
    col |= LCD_CMD_CURSOR_ROW2;
  else
    col |= LCD_CMD_CURSOR_ROW1;

  lcd_send_cmd(col);
}

void lcd_puts(const char *s)
{
  while (*s) {
    lcd_send_data(*s++);
  }
}

void lcd_putchar(char c)
{
  lcd_send_data(c);
}

void lcd_clear()
{
  lcd_send_cmd(LCD_CMD_CLEAR);
  _delay_ms(1);
}

/* MAIN */

int main(void)
{
  blink();

  for (;;) {
		lcd_puts("Hello World");
		_delay_ms(2000);
		lcd_clear();
		_delay_ms(2000);
  }
}