/*
 * Copyright (C) 2020-2021 Aloysius Indrayanto
 *
 * This file is part of the eCxx library, see LICENSE file for the license details.
 */


#ifndef INCLUDE_ECXX_PLATFORM_CHARACTER_LCD_TPP
#error "this file cannot be included directly"
#endif
#undef INCLUDE_ECXX_PLATFORM_CHARACTER_LCD_TPP


#ifndef ECXX_PLATFORM_CHARACTER_LCD_TPP
#define ECXX_PLATFORM_CHARACTER_LCD_TPP


//
// A simple character LCD driver base class
//
template <typename OutputMethodT>
inline CharacterLCD<OutputMethodT>::CharacterLCD()
: _numCol(0)
, _numRow(0)
{}


template <typename OutputMethodT>
inline CharacterLCD<OutputMethodT>::~CharacterLCD()
{ end(); }


template <typename OutputMethodT>
inline void CharacterLCD<OutputMethodT>::begin(uint8_t numCol, uint8_t numRow)
{
    // http://web.alfredstate.edu/faculty/weimandn/lcd/lcd_initialization/lcd_initialization_index.html
    // https://mil.ufl.edu/3744/docs/lcdmanual/commands.html
    // https://exploreembedded.com/wiki/Interfacing_LCD_in_4-bit_mode_with_8051

    // Initialize the output method
    OutputMethodT& om = *static_cast<OutputMethodT*>(this);

    om.init();
    if( !om.initialized() ) return;

    // Save the number of column and rows
    _numCol = numCol;
    _numRow = numRow;

    // Determine if the LCD will actually consist of two stacked LCDs
    _twoUnit = (_numCol * _numRow) > 80;

    // Initialize the LCD
    bool twice = _twoUnit;
    for(;;) {
        // Initialize the LCD
        om.sendLNibble(0b00000011); /* Reset               */ Application::delayMS(5);
        om.sendLNibble(0b00000011); /* Reset               */ Application::delayMS(1);
        om.sendLNibble(0b00000011); /* Reset               */ Application::delayMS(1);
        om.sendLNibble(0b00000010); /* Set 4-bit interface */ Application::delayMS(1);
        //               001DNF**
        om.sendCommand(0b00101000); // Function set    : 4-bit interface, 2 lines, 5x7 dots
        Application::delayMS(1);
        //               000001IS
        om.sendCommand(0b00000110); // Entry mode set  : increment cursor position, no display shift
        Application::delayMS(1);
        //               00001DCB
        om.sendCommand(0b00001100); // Display control : display on, cursor off, cursor blink off
        Application::delayMS(1);
        // Clear screen and move cursor to home
        clear();
        cursorHome();
        // Check if it will need to initialize the second half of the LCD
        if(twice) {
            twice = false;
            om.useRWAsE2(true);
            continue;
        }
        // Done!
        if(_twoUnit) om.useRWAsE2(false);
        break;
    }
}


template <typename OutputMethodT>
inline void CharacterLCD<OutputMethodT>::end()
{
    if(!_numCol || !_numRow) return;

    _numCol = 0;
    _numRow = 0;

    static_cast<OutputMethodT*>(this)->uninit();
}


template <typename OutputMethodT>
inline void CharacterLCD<OutputMethodT>::selectTopHalf() noexcept
{ if(_twoUnit) static_cast<OutputMethodT*>(this)->useRWAsE2(false); }


template <typename OutputMethodT>
inline void CharacterLCD<OutputMethodT>::selectBotHalf() noexcept
{ if(_twoUnit) static_cast<OutputMethodT*>(this)->useRWAsE2(true); }


template <typename OutputMethodT>
inline void CharacterLCD<OutputMethodT>::clear() const noexcept
{
    //                                                     00000001
    static_cast<const OutputMethodT*>(this)->sendCommand(0b00000001); // Clear display
    Application::delayMS(2);
}


template <typename OutputMethodT>
inline void CharacterLCD<OutputMethodT>::cursorHome() const noexcept
{
    //                                                     0000001*
    static_cast<const OutputMethodT*>(this)->sendCommand(0b00000010); // Cursor home
    Application::delayMS(2);
}


template <typename OutputMethodT>
inline void CharacterLCD<OutputMethodT>::setCursorPosition(uint8_t col, uint8_t row) const noexcept
{
    /*
     * http://web.alfredstate.edu/faculty/weimandn/lcd/lcd_addressing/lcd_addressing_index.html
     *
     * 16x1 => Row 1 => 00 - 0F
     *                  or
     *                  00 - 07 and 40 - 47
     *
     * 16x2 => Row 1 => 00 - 0F
     *         Row 2 => 40 - 4F
     *
     * 16x4 => Row 1 => 00 - 0F
     *         Row 2 => 40 - 4F
     *         Row 3 => 10 - 1F
     *         Row 4 => 50 - 5F
     *
     * 20x2 => Row 1 => 00 - 13
     *         Row 2 => 40 - 53
     *
     * 20x4 => Row 1 => 00 - 13
     *         Row 2 => 40 - 53
     *         Row 3 => 14 - 27
     *         Row 4 => 54 - 67
     *
     * 40x2 => Row 1 => 00 - 27
     *         Row 2 => 40 - 67
     *
     * 40x4 => Row 1 => 00 - 27 [40x2 part 1]   *** Not directly supported by this function!
     *         Row 2 => 40 - 67                 ***
     *         Row 3 => 00 - 27 [40x2 part 2]   *** Use selectTopHalf() and selectBotHalf() and
     *         Row 4 => 40 - 67                 *** assume each half as a 40x2 LCD.
     */
    if(_numCol == 16 && _numRow == 4) {
        constexpr uint8_t rowOffset[4] = { 0x00, 0x40, 0x14, 0x54 };
        //                                   Set DDRAM address 1AAAAAAA
        static_cast<const OutputMethodT*>(this)->sendCommand(0b10000000 | (rowOffset[row] + col) );
    }
    else {
        constexpr uint8_t rowOffset[4] = { 0x00, 0x40, 0x14, 0x54 };
        //                                   Set DDRAM address 1AAAAAAA
        static_cast<const OutputMethodT*>(this)->sendCommand(0b10000000 | (rowOffset[row] + col) );
    }
}


template <typename OutputMethodT>
inline void CharacterLCD<OutputMethodT>::moveCursorLeft() const noexcept
{
    // Move cursor left                                    0001SR**
    static_cast<const OutputMethodT*>(this)->sendCommand(0b00010000);
}


template <typename OutputMethodT>
inline void CharacterLCD<OutputMethodT>::moveCursorRight() const noexcept
{
    // Move cursor right                                   0001SR**
    static_cast<const OutputMethodT*>(this)->sendCommand(0b00010100);
}


template <typename OutputMethodT>
inline void CharacterLCD<OutputMethodT>::scrollDisplayLeft() const noexcept
{
    // Scroll display left                                 0001SR**
    static_cast<const OutputMethodT*>(this)->sendCommand(0b00011000);
}


template <typename OutputMethodT>
inline void CharacterLCD<OutputMethodT>::scrollDisplayRight() const noexcept
{
    // Scroll display right                                0001SR**
    static_cast<const OutputMethodT*>(this)->sendCommand(0b00011100);
}


template <typename OutputMethodT>
inline void CharacterLCD<OutputMethodT>::setDisplay(bool displayOn, CursorMode cursorMode) const noexcept
{
    // Display control            00001DCB     00001DCB
    uint8_t value = displayOn ? 0b00001100 : 0b00001000;

    // Cursor
    switch(cursorMode) {
        // Cursor                             00001DCB
        case CursorMode::Blink   : value |= 0b00000011; break;
        case CursorMode::NoBlink : value |= 0b00000010; break;
    }

    // Send the command
    static_cast<const OutputMethodT*>(this)->sendCommand(value);
    Application::delayMS(1);
}


template <typename OutputMethodT>
inline void CharacterLCD<OutputMethodT>::setEntryMode(EntryMode entryMode) const noexcept
{
    const OutputMethodT& om = *static_cast<const OutputMethodT*>(this);

    switch(entryMode) {
        // Entry mode set                                    000001IS
        case EntryMode::CursorMoveRight   : om.sendCommand(0b00000110); break;
        case EntryMode::CursorMoveLeft    : om.sendCommand(0b00000100); break;
        case EntryMode::DisplayShiftLeft  : om.sendCommand(0b00000111); break;
        case EntryMode::DisplayShiftRight : om.sendCommand(0b00000101); break;
    };
    Application::delayMS(1);
}


template <typename OutputMethodT> template <typename PGMPointerT, typename PGMReadByteFunctorT>
inline void CharacterLCD<OutputMethodT>::_setUserDefinedCharacter(uint8_t pos, PGMPointerT data8) const noexcept
{
    // http://www.handsonembedded.com/lcd16x2-hd44780-tutorial-5

    // For convenience
    const OutputMethodT& om = *static_cast<const OutputMethodT*>(this);

    //                01AAAAAA
    om.sendCommand( 0b01000000 | ( (pos & 0x07) << 3 ) ); // Set CGRAM address

    // Write the character definition
    const PGMReadByteFunctorT functor;

    for(size_t i = 0; i < 8; ++i) {
        const uint8_t ch = functor(data8.p++);
        om.sendData(ch);
    }
}


template <typename OutputMethodT> template <typename PGMReadByteFunctorT>
inline void CharacterLCD<OutputMethodT>::setUserDefinedCharacter(uint8_t pos, PGMCharPointer data8) const noexcept
{ _setUserDefinedCharacter<PGMCharPointer, PGMReadByteFunctorT>(pos, data8); }


template <typename OutputMethodT> template <typename PGMReadByteFunctorT>
inline void CharacterLCD<OutputMethodT>::setUserDefinedCharacter(uint8_t pos, PGMCharPointerFar data8) const noexcept
{ _setUserDefinedCharacter<PGMCharPointerFar, PGMReadByteFunctorT>(pos, data8); }


template <typename OutputMethodT>
inline bool CharacterLCD<OutputMethodT>::write(uint8_t value) const noexcept
{
    static_cast<const OutputMethodT*>(this)->sendData(value);
    return true;
}


#endif
