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


#include "../AVRCommon/Internal.h"

#include <eCxx/Platform/AVRX/PinMapping.h>


namespace eCxx {


//
// CCL data
//
struct LUTMap {
    PORT    portIn[3];
    uint8_t portInBitmask[3];

    uint8_t portOutMuxBitmask;

    PORT    portOutDef;
    uint8_t portOutDefBitmask;

    PORT    portOutAlt;
    uint8_t portOutAltBitmask;

    REG8    ctrla;
    REG8    ctrlb;
    REG8    ctrlc;
    REG8    truth;
};


static const LUTMap PROGMEM lutMap_PGM[Logic::NumberOfLUT] = {
    {
        { LUT0_IN_PORT, LUT0_IN_PORT, LUT0_IN_PORT },
        { LUT0_IN0_BM,  LUT0_IN1_BM,  LUT0_IN2_BM  },
        PORTMUX_LUT0_bm,
        LUT0_OUT_DEF_PORT,
        LUT0_OUT_DEF_BM,
        LUT0_OUT_ALT_PORT,
        LUT0_OUT_ALT_BM,
        &CCL.LUT0CTRLA,
        &CCL.LUT0CTRLB,
        &CCL.LUT0CTRLC,
        &CCL.TRUTH0
    },
    {
        { LUT1_IN_PORT, LUT1_IN_PORT, LUT1_IN_PORT },
        { LUT1_IN0_BM,  LUT1_IN1_BM,  LUT1_IN2_BM  },
        PORTMUX_LUT1_bm,
        LUT1_OUT_DEF_PORT,
        LUT1_OUT_DEF_BM,
        LUT1_OUT_ALT_PORT,
        LUT1_OUT_ALT_BM,
        &CCL.LUT1CTRLA,
        &CCL.LUT1CTRLB,
        &CCL.LUT1CTRLC,
        &CCL.TRUTH1
    },
    {
        { LUT2_IN_PORT, LUT2_IN_PORT, LUT2_IN_PORT },
        { LUT2_IN0_BM,  LUT2_IN1_BM,  LUT2_IN2_BM  },
        PORTMUX_LUT2_bm,
        LUT2_OUT_DEF_PORT,
        LUT2_OUT_DEF_BM,
        LUT2_OUT_ALT_PORT,
        LUT2_OUT_ALT_BM,
        &CCL.LUT2CTRLA,
        &CCL.LUT2CTRLB,
        &CCL.LUT2CTRLC,
        &CCL.TRUTH2
    },
    {
        { LUT3_IN_PORT, LUT3_IN_PORT, LUT3_IN_PORT },
        { LUT3_IN0_BM,  LUT3_IN1_BM,  LUT3_IN2_BM  },
        PORTMUX_LUT3_bm,
        LUT3_OUT_DEF_PORT,
        LUT3_OUT_DEF_BM,
        LUT3_OUT_ALT_PORT,
        LUT3_OUT_ALT_BM,
        &CCL.LUT3CTRLA,
        &CCL.LUT3CTRLB,
        &CCL.LUT3CTRLC,
        &CCL.TRUTH3
    }
};


static eCxx::VoidCallback lutIntFunc[Logic::NumberOfLUT] = { nullptr };


//
// Helper functions
//
static inline REG8 PINCTRL(PORT port, uint8_t bitMask)
{
    if(bitMask == PIN0_bm) return &port->PIN0CTRL;
    if(bitMask == PIN1_bm) return &port->PIN1CTRL;
    if(bitMask == PIN2_bm) return &port->PIN2CTRL;
    if(bitMask == PIN3_bm) return &port->PIN3CTRL;
    if(bitMask == PIN4_bm) return &port->PIN4CTRL;
    if(bitMask == PIN5_bm) return &port->PIN5CTRL;
    if(bitMask == PIN6_bm) return &port->PIN6CTRL;
                           return &port->PIN7CTRL;
}


//
// Logic::LUT
//
Logic::LUT::LUT(uint8_t lutNumber)
: _lutNumber(lutNumber)
{
    static_assert(Logic::LUT::NumberOfInput == 3, "Logic::LUT::NumberOfInput value is not 3");

    reset_impl();
}


void Logic::LUT::reset()
{
    // Get the LUT map
    LUTMap _lutMap;
    PGMMemCpy()( &_lutMap, &lutMap_PGM[_lutNumber], sizeof(_lutMap) );

    // Simply exit if the LUT is currently enabled
    if( *_lutMap.ctrla & CCL_ENABLE_bm ) return;

    // Reset the cofigurations
    reset_impl();
}


void Logic::LUT::reset_impl()
{
    enabled      = false;
    input[0]     = Input::Disabled;
    input[1]     = Input::Disabled;
    input[2]     = Input::Disabled;
    output       = Output::Disabled;
    clockSource  = ClockSource::Peripheral;
    filter       = Filter::Disabled;
    edgeDetector = EdgeDetector::Disabled;
    truthTable   = 0x00;
}


//
// A simple AVR CCL wrapper
//
Logic::Logic()
: lut      { 0, 1, 2, 3 }
, sequencer{ Sequencer::Disabled, Sequencer::Disabled }
{
    static_assert(Logic::NumberOfLUT == 4, "Logic::NumberOfLUT value is not 4");
    static_assert(Logic::NumberOfSequencer == 2, "Logic::NumberOfSequencer value is not 2");
}


bool Logic::begin()
{
    // Simply exit if the CCL is already enabled
    if(CCL.CTRLA & CCL_ENABLE_bm) return false;

    // Initialize sequencers
    CCL.SEQCTRL0 = static_cast<uint8_t>(sequencer[0]) << CCL_SEQSEL0_gp;
    CCL.SEQCTRL1 = static_cast<uint8_t>(sequencer[1]) << CCL_SEQSEL1_gp;

    // Used to copy LUT map from program memory to RAM
    const PGMMemCpy functor;
          LUTMap    _lutMap;

    // Initialize LUT(s) and pin(s)
    for(uint8_t l = 0; l < NumberOfLUT; ++l) {

        // Get the LUT and skip if it is disabled
        const Logic::LUT& _lut = lut[l];
        if(!_lut.enabled) continue;

        // Get the LUT map
        functor( &_lutMap, &lutMap_PGM[l], sizeof(_lutMap) );

        // Configure output - disable output pin
        if(_lut.output == Output::Disabled) {
            // Configure the LUT
            *_lutMap.ctrla &= ~CCL_OUTEN_bm;
        }

        // Configure output - use the default output pin
        else if(_lut.output == Output::Default) {
            // Setup port multiplexer
            PORTMUX_CCLROUTEA &= ~_lutMap.portOutMuxBitmask;
            // Configure the LUT
            *_lutMap.ctrla |= CCL_OUTEN_bm;
            // Configure the pin
            _lutMap.portOutDef->DIRSET = _lutMap.portOutDefBitmask;
        }

        // Configure output - use the alternative output pin
        else if(_lut.output == Output::Alternate && _lutMap.portOutAltBitmask) {
            // Setup port multiplexer
            PORTMUX_CCLROUTEA |= _lutMap.portOutMuxBitmask;
            // Configure the LUT
            *_lutMap.ctrla |= CCL_OUTEN_bm;
            // Configure the pin
            _lutMap.portOutAlt->DIRSET = _lutMap.portOutAltBitmask;
        }

        // Configure output - unsupported configuration
        else {
            return false;
        }

        // Configure input - LUT
        *_lutMap.ctrlb = ( ( static_cast<uint8_t>(_lut.input[0]) & 0x0F ) << CCL_INSEL0_gp )
                       | ( ( static_cast<uint8_t>(_lut.input[1]) & 0x0F ) << CCL_INSEL1_gp );
        *_lutMap.ctrlc = ( ( static_cast<uint8_t>(_lut.input[2]) & 0x0F ) << CCL_INSEL2_gp );

        // Configure input - pin
        for(uint8_t i = 0; i < 3; ++i) {

            // Input without pull-up
            if(_lut.input[i] == Input::Pin) {
                // Configure the pin as input
                _lutMap.portIn[i]->DIRCLR = _lutMap.portInBitmask[i];
                // Disable pull-up
                *PINCTRL( _lutMap.portIn[i], _lutMap.portInBitmask[i] ) &= ~PORT_PULLUPEN_bm;
            }

            // Input with pull-up
            else if(_lut.input[i] == Input::PinPullUp) {
                // Configure the pin as input
                _lutMap.portIn[i]->DIRCLR = _lutMap.portInBitmask[i];
                // Enable pull-up
                *PINCTRL( _lutMap.portIn[i], _lutMap.portInBitmask[i] ) |=  PORT_PULLUPEN_bm;
            }

        } // for i

        // Set the truth table
        *_lutMap.truth = _lut.truthTable;

        // Configure the remaining settins and enable the LUT
        *_lutMap.ctrla |= ( (static_cast<uint8_t>(_lut.edgeDetector) << CCL_EDGEDET_bp) |
                            (static_cast<uint8_t>(_lut.filter      ) << CCL_FILTSEL_gp) |
                            (static_cast<uint8_t>(_lut.clockSource ) << CCL_CLKSRC_gp ) |
                            CCL_ENABLE_bm
                          );

    } // for l

    // Enable the CCL
    CCL.CTRLA |= CCL_ENABLE_bm;

    // Done
    return true;
}


bool Logic::end()
{
    // Simply exit if the CCL is currently disabled
    if( !(CCL.CTRLA & CCL_ENABLE_bm) ) return false;

    // Disable the CCL
    CCL.CTRLA &= ~CCL_ENABLE_bm;

    // Used to copy LUT map from program memory to RAM
    const PGMMemCpy functor;
          LUTMap    _lutMap;

    // Disable LUT(s) and pin(s)
    for(uint8_t l = 0; l < NumberOfLUT; ++l) {

        // Get the LUT and skip if it is disabled
        const Logic::LUT& _lut = lut[l];
        if(!_lut.enabled) continue;

        // Get the LUT map
        functor( &_lutMap, &lutMap_PGM[l], sizeof(_lutMap) );

        // Disable the LUT
        *_lutMap.ctrla &= ~CCL_ENABLE_bm;

        // Disable output as needed
        if(_lut.output == Output::Default) {
            _lutMap.portOutDef->DIRCLR = _lutMap.portOutDefBitmask;
        }
        else if(_lut.output == Output::Alternate && _lutMap.portOutAltBitmask) {
            _lutMap.portOutAlt->DIRCLR = _lutMap.portOutAltBitmask;
        }

        // Disable pull-up(s) as needed
        for(uint8_t i = 0; i < 3; ++i) {
            if(_lut.input[i] != Input::PinPullUp) continue;
            *PINCTRL( _lutMap.portIn[i], _lutMap.portInBitmask[i] ) &= ~PORT_PULLUPEN_bm;
        }

    } // for l

    // Done
    return true;
}


void Logic::reset()
{
    end();

    for(uint8_t i = 0; i < NumberOfLUT;       ++i) lut      [i].reset_impl();
    for(uint8_t i = 0; i < NumberOfSequencer; ++i) sequencer[i] = Sequencer::Disabled;
}


void Logic::attachInterrupt(uint8_t lut_, eCxx::VoidCallback userFunc, Application::InterruptMode mode)
{
    if(lut_ >= NumberOfLUT) return;

    CCL.INTCTRL0 &= ~( CCL_INTMODE0_gm << (lut_ * 2) );

    lutIntFunc[lut_] = userFunc;

    switch(mode) {
        case Application::InterruptMode::Rising  : CCL.INTCTRL0 |= ( CCL_INTMODE0_RISING_gc  << (lut_ * 2) ); break;
        case Application::InterruptMode::Falling : CCL.INTCTRL0 |= ( CCL_INTMODE0_FALLING_gc << (lut_ * 2) ); break;
        case Application::InterruptMode::Change  : CCL.INTCTRL0 |= ( CCL_INTMODE0_BOTH_gc    << (lut_ * 2) ); break;
    }
}


void Logic::detachInterrupt(uint8_t lut_)
{
    if(lut_ >= NumberOfLUT) return;

    CCL.INTCTRL0 &= ~( CCL_INTMODE0_gm << (lut_ * 2) );

    lutIntFunc[lut_] = nullptr;
}


//
// CCL interrupt vector
//
ISR(CCL_CCL_vect)
{
    // LUT0
    if(CCL.INTFLAGS & CCL_INT0_bm) {
        if( lutIntFunc[0] ) (*lutIntFunc[0])(); // Call the user callback if specified
        CCL.INTFLAGS |= CCL_INT0_bm;            // Clear the interrupt flag
    }

    // LUT1
    if(CCL.INTFLAGS & CCL_INT1_bm) {
        if( lutIntFunc[1] ) (*lutIntFunc[1])(); // Call the user callback if specified
        CCL.INTFLAGS |= CCL_INT1_bm;            // Clear the interrupt flag
    }

    // LUT2
    if(CCL.INTFLAGS & CCL_INT2_bm) {
        if( lutIntFunc[2] ) (*lutIntFunc[2])(); // Call the user callback if specified
        CCL.INTFLAGS |= CCL_INT2_bm;            // Clear the interrupt flag
    }

    // LUT3
    if(CCL.INTFLAGS & CCL_INT3_bm) {
        if( lutIntFunc[3] ) (*lutIntFunc[3])(); // Call the user callback if specified
        CCL.INTFLAGS |= CCL_INT3_bm;            // Clear the interrupt flag
    }
}


} // namespace eCxx
