#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <err.h>
#include <errno.h>
#include <string.h>
#include <math.h>
#include <time.h>

#include <sys/ioctl.h>
#include <sys/gpio.h>
#include <dev/spi/spi_io.h>

#define GPIO "/dev/gpio0"
#define PIN_RESET   12
#define PIN_SELECT  25

#define SPI "/dev/spi0"
#define MODE 3
#define ADDR 0

#define _X(a,b) ((a)?(b):0)

/* table 0 */
#define CLEAR_DISPLAY 1
#define RETURN_HOME 2
#define ENTRY_MODE_SET(id,s) (0x04 | ((id)?2:0) | ((s)?1:0))
#define DISPLAY_ONOFF(d,c,b) (0x08 | _X(d,4) | _X(c,2) | _X(b,1))
#define CURSOR_OR_DISPLAY_SHIFT(sc,rl) (0x10 | _X(sc,8) | _X(rl,4))
#define FUNCTION_SET(dl,n,dh,is) (0x20 | _X(dl,16) | _X(n,8) | _X(dh,4) | ((is) & 0x3))
#define SET_CGRAM(ac) (0x40 | ((ac) & 0x3f))
#define SET_DDRAM(ac) (0x80 | ((ac) & 0x7f))

/* table 1 */
#define BIAS_SET(bs,fx) (0x10 | _X(bs,8) | _X(fx,1))
#define SET_IRAM(ac) (0x40 | ((ac) & 0xf))
#define POWER_SET(ion,bon,c) (0x50 | _X(ion,8) | _X(bon,4) | (((c)>>4)&0x3))
#define FOLLOWER_CONTROL(fon,rab) (0x60 | _X(fon,8) | ((rab) & 0x7))
#define CONTRAST_SET(c) (0x70 | ((c) & 0xf))

/* table 2 */
#define DOUBLE_HEIGHT(ud) (0x10 | _X(ud,8))

typedef struct {
       int gfd;
       int sfd;

       int select_pin;
       int reset_pin;
       int rows;
       int columns;
       int addr;

       int enabled;
       int cursor;
       int blink;
       int dbl;
} lcd_t;

void
delay(double seconds)
{
       unsigned hz = 100;
       unsigned micros = 1000000u / hz;
       useconds_t ticks = ceil(seconds * hz);

       usleep(ticks * micros);
}

int
gpio_set(int fd, int pin, int val)
{
       struct gpio_req gp;

       gp.gp_name[0] = '\0';
       gp.gp_pin = pin;
       gp.gp_value = val;

       if (ioctl(fd, GPIOWRITE, &gp) == -1)
               return errno;
       return 0;
}

int
spi_configure(int fd, int addr, uint32_t mode, uint32_t speed)
{
       spi_ioctl_configure_t sic;

       sic.sic_addr = addr;
       sic.sic_mode = mode;
       sic.sic_speed = speed;

       if (ioctl(fd, SPI_IOCTL_CONFIGURE, &sic) == -1)
               return errno;
       return 0;
}

int
spi_transfer(int fd, int addr, void *send, size_t slen, void *recv, size_t rlen)
{
       spi_ioctl_transfer_t sit;

       sit.sit_addr = addr;
       sit.sit_send = send;
       sit.sit_sendlen = slen;
       sit.sit_recv = recv;
       sit.sit_recvlen = rlen;

       if (ioctl(fd, SPI_IOCTL_TRANSFER, &sit) == -1)
               return errno;
       return 0;
}

int
read_busy(lcd_t *lcd)
{
       uint8_t buf;

       spi_transfer(lcd->sfd, lcd->addr, NULL, 0, &buf, 1);

       return (int)buf;
}

void
write_char(lcd_t *lcd, uint8_t c)
{
       gpio_set(lcd->gfd, lcd->select_pin, GPIO_PIN_HIGH);
       spi_transfer(lcd->sfd, lcd->addr, &c, 1, NULL, 0);
       delay(50e-6);
}

void
write_string(lcd_t *lcd, uint8_t *s)
{
       gpio_set(lcd->gfd, lcd->select_pin, GPIO_PIN_HIGH);
       while (*s) {
               spi_transfer(lcd->sfd, lcd->addr, s++, 1, NULL, 0);
               delay(30e-6);
       }
}

void
write_instruction_set(lcd_t *lcd, int set)
{
       uint8_t cmd = FUNCTION_SET(1, 1, lcd->dbl, set);

       gpio_set(lcd->gfd, lcd->select_pin, GPIO_PIN_LOW);
       spi_transfer(lcd->sfd, lcd->addr, &cmd, 1, NULL, 0);
       delay(30e-6);
}

void
write_command(lcd_t *lcd, uint8_t cmd, int set)
{

       gpio_set(lcd->gfd, lcd->select_pin, GPIO_PIN_LOW);
       write_instruction_set(lcd, set);
       spi_transfer(lcd->sfd, lcd->addr, &cmd, 1, NULL, 0);
       delay(30e-6);
}

void
update_display_mode(lcd_t *lcd)
{
       write_command(lcd,
               DISPLAY_ONOFF(lcd->enabled,lcd->cursor,lcd->blink),
               0);
       write_command(lcd,
               DOUBLE_HEIGHT(lcd->dbl >> 1),
               2);
}

void
set_bias(lcd_t *lcd, int bias)
{
       write_command(lcd, BIAS_SET(bias, 1), 1);
}

void
set_contrast(lcd_t *lcd, int contrast)
{
       write_command(lcd, POWER_SET(1,1,contrast), 1);
       write_command(lcd, FOLLOWER_CONTROL(1,3), 1);
       delay(200e-3);
       write_command(lcd, CONTRAST_SET(contrast), 1);
}

void
set_display_mode(lcd_t *lcd, int enabled, int cursor, int blink)
{
       lcd->enabled = enabled;
       lcd->cursor = cursor;
       lcd->blink = blink;
       update_display_mode(lcd);
}

void
enable_cursor(lcd_t *lcd, int set)
{
       lcd->cursor = set;
       update_display_mode(lcd);
}

void
enable_blink(lcd_t *lcd, int set)
{
       lcd->blink = set;
       update_display_mode(lcd);
}

void
set_cursor_offset(lcd_t *lcd, int offset)
{
       write_command(lcd, SET_DDRAM(offset), 0);
       delay(50e-6);
}

void
set_cursor_position(lcd_t *lcd, int row, int column)
{
       int stride, n;

       switch (lcd->rows) {
       case 1: stride = 0; break;
       case 2: stride = 64; break;
       case 3: stride = 16; break;
       default:
               return;
       }

       n = row * stride + column;
       if (n >= 0 && n < 128)
               set_cursor_offset(lcd, n);
}

void
home(lcd_t *lcd)
{
       set_cursor_position(lcd, 0, 0);
}

void
clear(lcd_t *lcd)
{
       write_command(lcd, CLEAR_DISPLAY, 0);
       delay(2e-3);
}

void
reset(lcd_t *lcd)
{
       gpio_set(lcd->gfd, lcd->reset_pin, GPIO_PIN_LOW);
       delay(0.001);
       gpio_set(lcd->gfd, lcd->reset_pin, GPIO_PIN_HIGH);
}

void
setup(lcd_t *lcd)
{

       lcd->select_pin = PIN_SELECT;
       lcd->reset_pin = PIN_RESET;
       lcd->rows = 3;
       lcd->columns = 16;
       lcd->addr = ADDR;

       lcd->enabled = 1;
       lcd->cursor = 0;
       lcd->blink = 0;
       lcd->dbl = 1;

       spi_configure(lcd->sfd, lcd->addr, MODE, 0);
       spi_configure(lcd->sfd, lcd->addr, MODE, 1000000);

       gpio_set(lcd->gfd, lcd->select_pin, GPIO_PIN_HIGH);
       reset(lcd);
       delay(40e-3);
}


int main()
{
       lcd_t L;
       int i;

       L.gfd = open(GPIO, O_RDWR);
       if (L.gfd == -1)
               err(1,"open %s",GPIO);
       L.sfd = open(SPI, O_RDWR);
       if (L.sfd == -1)
               err(1,"open %s",SPI);

       setup(&L);

       update_display_mode(&L);
       write_command(&L, ENTRY_MODE_SET(1,0), 0);

       set_bias(&L, 1);

       set_display_mode(&L, 1, 0, 0);
       set_contrast(&L, 40);
       clear(&L);

for (;;) {
       time_t now;
       struct tm *tm;
       static int mday;
       char *tz;
       char buf[26];

       time(&now);
       tm = localtime(&now);
       asctime_r(tm, buf);
       tz = tm->tm_zone;

       buf[10] = '\0';
       buf[19] = '\0';
       buf[24] = '\0';

       if (tm->tm_mday != mday) {
               set_cursor_position(&L, 0, 0);
               write_string(&L, &buf[0]);
               set_cursor_position(&L, 0, 12);
               write_string(&L, &buf[20]);
               mday = tm->tm_mday;
       }

       set_cursor_position(&L, 1, 0);
       write_string(&L, &buf[11]);
       set_cursor_position(&L, 1, 9);
       write_string(&L, tz);

       delay(1.0);
}

       //printf("%d\n", read_busy(&L));

       close(L.sfd);
       close(L.gfd);
}