/*
Copyright 2022

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 2 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

#include "hd44780.h"
#include "gpio.h"
#include "progmem.h"
#include "wait.h"

#ifndef HD44780_DATA_PINS
#    error hd44780: no data pins defined!
#endif

#ifndef HD44780_RS_PIN
#    error hd44780: no RS pin defined!
#endif

#ifndef HD44780_RW_PIN
#    error hd44780: no R/W pin defined!
#endif

#ifndef HD44780_E_PIN
#    error hd44780: no E pin defined!
#endif

static const pin_t data_pins[4] = HD44780_DATA_PINS;

#ifndef HD44780_DISPLAY_COLS
#    define HD44780_DISPLAY_COLS 16
#endif

#ifndef HD44780_DISPLAY_LINES
#    define HD44780_DISPLAY_LINES 2
#endif

#ifndef HD44780_DDRAM_LINE0_ADDR
#    define HD44780_DDRAM_LINE0_ADDR 0x00
#endif
#ifndef HD44780_DDRAM_LINE1_ADDR
#    define HD44780_DDRAM_LINE1_ADDR 0x40
#endif

#define HD44780_INIT_DELAY_MS 16
#define HD44780_ENABLE_DELAY_US 1

static void hd44780_latch(void) {
    writePinHigh(HD44780_E_PIN);
    wait_us(HD44780_ENABLE_DELAY_US);
    writePinLow(HD44780_E_PIN);
}

void hd44780_write(uint8_t data, bool isData) {
    writePin(HD44780_RS_PIN, isData);
    writePinLow(HD44780_RW_PIN);

    for (int i = 0; i < 4; i++) {
        setPinOutput(data_pins[i]);
    }

    // Write high nibble
    for (int i = 0; i < 4; i++) {
        writePin(data_pins[i], (data >> 4) & (1 << i));
    }
    hd44780_latch();

    // Write low nibble
    for (int i = 0; i < 4; i++) {
        writePin(data_pins[i], data & (1 << i));
    }
    hd44780_latch();

    for (int i = 0; i < 4; i++) {
        writePinHigh(data_pins[i]);
    }
}

uint8_t hd44780_read(bool isData) {
    uint8_t data = 0;

    writePin(HD44780_RS_PIN, isData);
    writePinHigh(HD44780_RW_PIN);

    for (int i = 0; i < 4; i++) {
        setPinInput(data_pins[i]);
    }

    writePinHigh(HD44780_E_PIN);
    wait_us(HD44780_ENABLE_DELAY_US);

    // Read high nibble
    for (int i = 0; i < 4; i++) {
        data |= (readPin(data_pins[i]) << i);
    }

    data <<= 4;

    writePinLow(HD44780_E_PIN);
    wait_us(HD44780_ENABLE_DELAY_US);
    writePinHigh(HD44780_E_PIN);
    wait_us(HD44780_ENABLE_DELAY_US);

    // Read low nibble
    for (int i = 0; i < 4; i++) {
        data |= (readPin(data_pins[i]) << i);
    }

    writePinLow(HD44780_E_PIN);

    return data;
}

bool hd44780_busy(void) {
    return hd44780_read(false) & HD44780_BUSY_FLAG;
}

void hd44780_command(uint8_t command) {
    while (hd44780_busy())
        ;
    hd44780_write(command, false);
}

void hd44780_data(uint8_t data) {
    while (hd44780_busy())
        ;
    hd44780_write(data, true);
}

void hd44780_clear(void) {
    hd44780_command(HD44780_CMD_CLEAR_DISPLAY);
}

void hd44780_home(void) {
    hd44780_command(HD44780_CMD_RETURN_HOME);
}

void hd44780_on(bool cursor, bool blink) {
    if (cursor) {
        if (blink) {
            hd44780_command(HD44780_CMD_DISPLAY | HD44780_DISPLAY_ON | HD44780_DISPLAY_CURSOR | HD44780_DISPLAY_BLINK);
        } else {
            hd44780_command(HD44780_CMD_DISPLAY | HD44780_DISPLAY_ON | HD44780_DISPLAY_CURSOR);
        }
    } else {
        hd44780_command(HD44780_CMD_DISPLAY | HD44780_DISPLAY_ON);
    }
}

void hd44780_off() {
    hd44780_command(HD44780_CMD_DISPLAY);
}

void hd44780_set_cgram_address(uint8_t address) {
    hd44780_command(HD44780_CMD_SET_CGRAM_ADDRESS + (address & 0x3F));
}

void hd44780_set_ddram_address(uint8_t address) {
    hd44780_command(HD44780_CMD_SET_DDRAM_ADDRESS + (address & 0x7F));
}

void hd44780_init(bool cursor, bool blink) {
    setPinOutput(HD44780_RS_PIN);
    setPinOutput(HD44780_RW_PIN);
    setPinOutput(HD44780_E_PIN);

    for (int i = 0; i < 4; i++) {
        setPinOutput(data_pins[i]);
    }

    wait_ms(HD44780_INIT_DELAY_MS);

    // Manually configure for 4-bit mode - can't use hd44780_command() yet
    // HD44780U datasheet, Fig. 24 (p46)
    writePinHigh(data_pins[0]); // Function set
    writePinHigh(data_pins[1]); // DL = 1
    hd44780_latch();
    wait_ms(5);
    // Send again
    hd44780_latch();
    wait_us(64);
    // And again (?)
    hd44780_latch();
    wait_us(64);

    writePinLow(data_pins[0]); // DL = 0
    hd44780_latch();
    wait_us(64);

#if HD44780_DISPLAY_LINES == 1
    hd44780_command(HD44780_CMD_FUNCTION); // 4 bit, 1 line, 5x8 dots
#else
    hd44780_command(HD44780_CMD_FUNCTION | HD44780_FUNCTION_2_LINES); // 4 bit, 2 lines, 5x8 dots
#endif
    hd44780_on(cursor, blink);
    hd44780_clear();
    hd44780_home();
    hd44780_command(HD44780_CMD_ENTRY_MODE | HD44780_ENTRY_MODE_INC);
}

void hd44780_set_cursor(uint8_t col, uint8_t line) {
    register uint8_t address = col;

#if HD44780_DISPLAY_LINES == 1
    address += HD44780_DDRAM_LINE0_ADDR;
#elif HD44780_DISPLAY_LINES == 2
    if (line == 0) {
        address += HD44780_DDRAM_LINE0_ADDR;
    } else {
        address += HD44780_DDRAM_LINE1_ADDR;
    }
#endif

    hd44780_set_ddram_address(address);
}

void hd44780_define_char(uint8_t index, uint8_t *data) {
    hd44780_set_cgram_address((index & 0x7) << 3);
    for (uint8_t i = 0; i < 8; i++) {
        hd44780_data(data[i]);
    }
}

void hd44780_putc(char c) {
    while (hd44780_busy())
        ;
    uint8_t current_position = hd44780_read(false);

    if (c == '\n') {
        hd44780_set_cursor(0, current_position < HD44780_DDRAM_LINE1_ADDR ? 1 : 0);
    } else {
#if defined(HD44780_WRAP_LINES)
#    if HD44780_DISPLAY_LINES == 1
        if (current_position == HD44780_DDRAM_LINE0_ADDR + HD44780_DISPLAY_COLS) {
            // Go to start of line
            hd44780_set_cursor(0, 0);
        }
#    elif HD44780_DISPLAY_LINES == 2
        if (current_position == HD44780_DDRAM_LINE0_ADDR + HD44780_DISPLAY_COLS) {
            // Go to start of second line
            hd44780_set_cursor(0, 1);
        } else if (current_position == HD44780_DDRAM_LINE1_ADDR + HD44780_DISPLAY_COLS) {
            // Go to start of first line
            hd44780_set_cursor(0, 0);
        }
#    endif
#endif
        hd44780_data(c);
    }
}

void hd44780_puts(const char *s) {
    register char c;
    while ((c = *s++)) {
        hd44780_putc(c);
    }
}

#if defined(__AVR__)
void hd44780_define_char_P(uint8_t index, const uint8_t *data) {
    hd44780_set_cgram_address(index << 3);
    for (uint8_t i = 0; i < 8; i++) {
        hd44780_data(pgm_read_byte(data++));
    }
}

void hd44780_puts_P(const char *s) {
    register char c;
    while ((c = pgm_read_byte(s++))) {
        hd44780_putc(c);
    }
}
#endif