#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);
}
