/*
 * 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_KEYPAD_SCANNER_TPP
#error "this file cannot be included directly"
#endif
#undef INCLUDE_ECXX_PLATFORM_KEYPAD_SCANNER_TPP


#ifndef ECXX_PLATFORM_KEYPAD_SCANNER_TPP
#define ECXX_PLATFORM_KEYPAD_SCANNER_TPP


//
// A simple keypad scanner
//
template <uint8_t RowN, uint8_t ColN, uint8_t MultiKeyN>
inline KeypadScanner<RowN, ColN, MultiKeyN>::KeypadScanner() noexcept
: _keypadMode(KeypadMode::Uninitialized)
{}


template <uint8_t RowN, uint8_t ColN, uint8_t MultiKeyN>
inline void KeypadScanner<RowN, ColN, MultiKeyN>::begin(
    ActiveState   keypadState,
    const uint8_t (&rowPin)[NumberOfRows   ], // Output
    const uint8_t (&colPin)[NumberOfColumns], // Input
    bool          pullUpColPins               // Only meaningful if the keypad is active-low
) noexcept
{
    static_assert(1 <= RowN && RowN <= 8, "template argument substituting RowN is not between 1 and 8");
    static_assert(1 <= ColN && ColN <= 8, "template argument substituting ColN is not between 1 and 8");

    // Set the keypad mode
    _keypadMode = (keypadState == ActiveState::Low)
                ? KeypadMode::SimpleActiveLow
                : KeypadMode::SimpleActiveHigh;

    // Copy pin specifications
    for(uint8_t i = 0; i < NumberOfRows;    ++i) _pinSpec.simple.rowPin[i] = rowPin[i];
    for(uint8_t i = 0; i < NumberOfColumns; ++i) _pinSpec.simple.colPin[i] = colPin[i];

    // Set pin modes
    const Application::PinMode colPinMode = (pullUpColPins && keypadState == ActiveState::Low)
                                          ? Application::PinMode::InputPullUp
                                          : Application::PinMode::Input;

    for(uint8_t i = 0; i < NumberOfRows;    ++i) Application::setPinMode(_pinSpec.simple.rowPin[i], Application::PinMode::Output);
    for(uint8_t i = 0; i < NumberOfColumns; ++i) Application::setPinMode(_pinSpec.simple.colPin[i], colPinMode                  );
}


template <uint8_t RowN, uint8_t ColN, uint8_t MultiKeyN>
inline void KeypadScanner<RowN, ColN, MultiKeyN>::begin(
    ActiveState   keypadState,
    const uint8_t (&rowPin)[NumberOfRows   ], // Output
    const uint8_t (&colPin)[NumberOfColumns], // Output
    uint8_t       muxPin,                     // Input
    ActiveState   decoderAddressState,
    ActiveState   multiplexerAddressState
) noexcept
{
    static_assert(1 <= RowN && RowN <= 3, "template argument substituting RowN is not between 1 and 3");
    static_assert(1 <= ColN && ColN <= 3, "template argument substituting ColN is not between 1 and 3");

    // Set the keypad mode
    _keypadMode = (keypadState == ActiveState::Low)
                ? KeypadMode::DecMuxActiveLow
                : KeypadMode::DecMuxActiveHigh;

    // Copy pin specifications
    for(uint8_t i = 0; i < NumberOfRows;    ++i) _pinSpec.decmux.rowPin[i] = rowPin[i];
    for(uint8_t i = 0; i < NumberOfColumns; ++i) _pinSpec.decmux.colPin[i] = colPin[i];

    _pinSpec.decmux.muxPin    = muxPin;

    _pinSpec.decmux.decAddrAS = decoderAddressState;
    _pinSpec.decmux.muxAddrAS = multiplexerAddressState;

    // Set pin modes
    for(uint8_t i = 0; i < NumberOfRows;    ++i) Application::setPinMode(_pinSpec.decmux.rowPin[i], Application::PinMode::Output);
    for(uint8_t i = 0; i < NumberOfColumns; ++i) Application::setPinMode(_pinSpec.decmux.colPin[i], Application::PinMode::Output);

    Application::setPinMode(_pinSpec.decmux.muxPin, Application::PinMode::Input);
}


template <uint8_t RowN, uint8_t ColN, uint8_t MultiKeyN>
inline void KeypadScanner<RowN, ColN, MultiKeyN>::begin(
    ActiveState keypadState,
    uint8_t     dsPin,   // Output
    uint8_t     shcpPin, // Output
    uint8_t     stcpPin, // Output
    uint8_t     muxPin,  // Input
    ActiveState decoderAddressState,
    ActiveState multiplexerAddressState
) noexcept
{
    static_assert(1 <= RowN && RowN <= 3, "template argument substituting RowN is not between 1 and 3");
    static_assert(1 <= ColN && ColN <= 3, "template argument substituting ColN is not between 1 and 3");

    // Set the keypad mode
    _keypadMode = (keypadState == ActiveState::Low)
                ? KeypadMode::SIPOActiveLow
                : KeypadMode::SIPOActiveHigh;

    // Copy pin specifications
    _pinSpec.sipo.dsPin     = dsPin;
    _pinSpec.sipo.shcpPin   = shcpPin;
    _pinSpec.sipo.stcpPin   = stcpPin;

    _pinSpec.sipo.muxPin    = muxPin;

    _pinSpec.sipo.decAddrAS = decoderAddressState;
    _pinSpec.sipo.muxAddrAS = multiplexerAddressState;

    // Set pin modes
    Application::setPinMode(_pinSpec.sipo.dsPin,   Application::PinMode::Output);
    Application::setPinMode(_pinSpec.sipo.shcpPin, Application::PinMode::Output);
    Application::setPinMode(_pinSpec.sipo.stcpPin, Application::PinMode::Output);
    Application::setPinMode(_pinSpec.sipo.muxPin,  Application::PinMode::Input );

    // Ensure all clock pins are not active
    Application::digitalWrite(_pinSpec.sipo.shcpPin, false);
    Application::digitalWrite(_pinSpec.sipo.stcpPin, false);
}


template <uint8_t RowN, uint8_t ColN, uint8_t MultiKeyN>
inline void KeypadScanner<RowN, ColN, MultiKeyN>::end() noexcept
{
    switch(_keypadMode) {
        case KeypadMode::SimpleActiveLow  : /* FALLTHROUGH */
        case KeypadMode::SimpleActiveHigh :
            for(uint8_t i = 0; i < NumberOfRows;    ++i) Application::setPinMode(_pinSpec.simple.rowPin[i], Application::PinMode::Input);
            for(uint8_t i = 0; i < NumberOfColumns; ++i) Application::setPinMode(_pinSpec.simple.colPin[i], Application::PinMode::Input);
            break;

        case KeypadMode::DecMuxActiveLow  : /* FALLTHROUGH */
        case KeypadMode::DecMuxActiveHigh :
            for(uint8_t i = 0; i < NumberOfRows;    ++i) Application::setPinMode(_pinSpec.decmux.rowPin[i], Application::PinMode::Input);
            for(uint8_t i = 0; i < NumberOfColumns; ++i) Application::setPinMode(_pinSpec.decmux.colPin[i], Application::PinMode::Input);
                                                         Application::setPinMode(_pinSpec.decmux.muxPin,    Application::PinMode::Input);
            break;

        case KeypadMode::SIPOActiveLow    : /* FALLTHROUGH */
        case KeypadMode::SIPOActiveHigh   :
            Application::setPinMode(_pinSpec.sipo.dsPin,   Application::PinMode::Input);
            Application::setPinMode(_pinSpec.sipo.shcpPin, Application::PinMode::Input);
            Application::setPinMode(_pinSpec.sipo.stcpPin, Application::PinMode::Input);
            Application::setPinMode(_pinSpec.sipo.muxPin,  Application::PinMode::Input);
            break;
    }

    _keypadMode = KeypadMode::Uninitialized;
}


template <uint8_t RowN, uint8_t ColN, uint8_t MultiKeyN>
inline uint8_t KeypadScanner<RowN, ColN, MultiKeyN>::scan(uint8_t (&keyState)[NumberOfMultiKeys]) noexcept
{
    // Perform scanning
    uint8_t numKeys = 0;

    switch(_keypadMode) {
        case KeypadMode::SimpleActiveLow  : /* FALLTHROUGH */
        case KeypadMode::SimpleActiveHigh :
            numKeys = scanSimple();
            break;

        case KeypadMode::DecMuxActiveLow  : /* FALLTHROUGH */
        case KeypadMode::DecMuxActiveHigh :
            numKeys = scanDecMux();
            break;

        case KeypadMode::SIPOActiveLow    : /* FALLTHROUGH */
        case KeypadMode::SIPOActiveHigh   :
            numKeys = scanSIPO();
            break;

        default:
            return 0;
    }

    // Copy the result and return the number of scanned key(s)
    for(uint8_t i = 0; i < NumberOfMultiKeys; ++i) keyState[i] = _keyState[i];

    return numKeys;
}


template <uint8_t RowN, uint8_t ColN, uint8_t MultiKeyN>
inline uint8_t KeypadScanner<RowN, ColN, MultiKeyN>::scanSimple() noexcept
{
    // Determine the logical low (not active) and logical high (active) values
    const bool logicL = (_keypadMode == KeypadMode::SimpleActiveHigh) ? false : true;
    const bool logicH = (_keypadMode == KeypadMode::SimpleActiveHigh) ? true  : false;

    // Ensure all row pins are not active
    for(uint8_t i = 0; i < NumberOfRows; ++i) {
        Application::digitalWrite(_pinSpec.simple.rowPin[i], logicL);
    }

    // Walk through the rows to scan the keypad
    uint8_t keyIdx = 0;
    for(uint8_t r = 0; r < NumberOfRows; ++r) {
        // Activate the row
        Application::digitalWrite(_pinSpec.simple.rowPin[r], logicH);
        // Walk through the columns to scan the keypad
        for(uint8_t c = 0; c < NumberOfColumns; ++c) {
            // Read the column's value and skip if it is not active, ...
            if( Application::digitalRead(_pinSpec.simple.colPin[c]) != logicH ) continue;
            // ... otherwise, create and store the key code
            _keyState[keyIdx++] = 0b10001000 | (r << 4) | c;
            // Done if the number of captured keys already exceed the limit
            if( keyIdx >= NumberOfMultiKeys ) return keyIdx;
        }
        // Deactivate the row
        Application::digitalWrite(_pinSpec.simple.rowPin[r], logicL);
    }

    // Scan process completed
    return keyIdx;
}


template <uint8_t RowN, uint8_t ColN, uint8_t MultiKeyN>
inline uint8_t KeypadScanner<RowN, ColN, MultiKeyN>::scanDecMux() noexcept
{
    // Determine the and logical high (active) value
    const bool logicH = (_keypadMode == KeypadMode::DecMuxActiveHigh) ? true : false;

    // Calculate the actual number of rows and columns
    const uint8_t actualNumRows = eCxx::pow( 2, NumberOfRows    );
    const uint8_t actualnumCols = eCxx::pow( 2, NumberOfColumns );

    // Walk through the rows to scan the keypad
    uint8_t keyIdx = 0;
    for(uint8_t r = 0; r < actualNumRows; ++r) {
        // Send the row address to the decoder
        sendRowAddress(r);
        // Walk through the columns to scan the keypad
        for(uint8_t c = 0; c < actualnumCols; ++c) {
            // Send the column address to the multiplexer
            sendColAddress(c);
            // Read the column's value and skip if it is not active, ...
            if( Application::digitalRead(_pinSpec.decmux.muxPin) != logicH ) continue;
            // ... otherwise, create and store the key code
            _keyState[keyIdx++] = 0b10001000 | (r << 4) | c;
            // Done if the number of captured keys already exceed the limit
            if( keyIdx >= NumberOfMultiKeys ) return keyIdx;
        }
    }

    // Scan process completed
    return keyIdx;
}


template <uint8_t RowN, uint8_t ColN, uint8_t MultiKeyN>
inline uint8_t KeypadScanner<RowN, ColN, MultiKeyN>::scanSIPO() noexcept
{
    // Determine the and logical high (active) value
    const bool logicH = (_keypadMode == KeypadMode::SIPOActiveHigh) ? true : false;

    // Calculate the actual number of rows and columns
    const uint8_t actualNumRows = eCxx::pow( 2, NumberOfRows    );
    const uint8_t actualnumCols = eCxx::pow( 2, NumberOfColumns );

    // Walk through the rows to scan the keypad
    uint8_t keyIdx = 0;
    for(uint8_t r = 0; r < actualNumRows; ++r) {
        // Walk through the columns to scan the keypad
        for(uint8_t c = 0; c < actualnumCols; ++c) {
            // Send the row address to the decoder and the column address to the multiplexer
            sendRowColAddress(r, c);
            // Read the column's value and skip if it is not active, ...
            if( Application::digitalRead(_pinSpec.sipo.muxPin) != logicH ) continue;
            // ... otherwise, create and store the key code
            _keyState[keyIdx++] = 0b10001000 | (r << 4) | c;
            // Done if the number of captured keys already exceed the limit
            if( keyIdx >= NumberOfMultiKeys ) return keyIdx;
        }
    }

    // Scan process completed
    return keyIdx;
}


template <uint8_t RowN, uint8_t ColN, uint8_t MultiKeyN>
inline void KeypadScanner<RowN, ColN, MultiKeyN>::sendRowAddress(uint8_t addr) const noexcept
{
    // Determine the logical low (not active) and logical high (active) values
    const bool logicL = (_pinSpec.decmux.decAddrAS == ActiveState::Low) ? true  : false;
    const bool logicH = (_pinSpec.decmux.decAddrAS == ActiveState::Low) ? false : true;

    // Output the row address to the decoder
    for(uint8_t i = 0; i < NumberOfRows; ++i) {
        Application::digitalWrite( _pinSpec.decmux.rowPin[i], ( addr & _BV(i) ) ? logicH : logicL );
    }
}


template <uint8_t RowN, uint8_t ColN, uint8_t MultiKeyN>
inline void KeypadScanner<RowN, ColN, MultiKeyN>::sendColAddress(uint8_t addr) const noexcept
{
    // Determine the logical low (not active) and logical high (active) values
    const bool logicL = (_pinSpec.decmux.muxAddrAS == ActiveState::Low) ? true  : false;
    const bool logicH = (_pinSpec.decmux.muxAddrAS == ActiveState::Low) ? false : true;

    // Output the column address to the multiplexer
    for(uint8_t i = 0; i < NumberOfColumns; ++i) {
        Application::digitalWrite( _pinSpec.decmux.colPin[i], ( addr & _BV(i) ) ? logicH : logicL );
    }
}


template <uint8_t RowN, uint8_t ColN, uint8_t MultiKeyN>
inline void KeypadScanner<RowN, ColN, MultiKeyN>::sendRowColAddress(uint8_t raddr, uint8_t caddr) const noexcept
{
    // Determine the logical low (not active) and logical high (active) values
    const bool dlogicL = (_pinSpec.sipo.decAddrAS == ActiveState::Low) ? true  : false;
    const bool dlogicH = (_pinSpec.sipo.decAddrAS == ActiveState::Low) ? false : true;

    // Determine the logical low (not active) and logical high (active) values
    const bool mlogicL = (_pinSpec.sipo.muxAddrAS == ActiveState::Low) ? true  : false;
    const bool mlogicH = (_pinSpec.sipo.muxAddrAS == ActiveState::Low) ? false : true;

    // Output the row address for the decoder
    for(uint8_t i = 0; i < NumberOfRows; ++i) {
        Application::digitalWrite( _pinSpec.sipo.dsPin, ( raddr & _BV(NumberOfRows - i - 1) ) ? dlogicH : dlogicL );
        Application::digitalWrite( _pinSpec.sipo.shcpPin, true  );
        Application::digitalWrite( _pinSpec.sipo.shcpPin, false );
    }

    // Output the column address for the multiplexer
    for(uint8_t i = 0; i < NumberOfColumns; ++i) {
        Application::digitalWrite( _pinSpec.sipo.dsPin, ( caddr & _BV(NumberOfColumns - i - 1) ) ? mlogicH : mlogicL );
        Application::digitalWrite( _pinSpec.sipo.shcpPin, true  );
        Application::digitalWrite( _pinSpec.sipo.shcpPin, false );
    }

    // Latch the addresses
    Application::digitalWrite(_pinSpec.sipo.stcpPin, true );
    Application::digitalWrite(_pinSpec.sipo.stcpPin, false);
}


#endif
