/*
 * 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 {


//
// Analog comparator user callback function
//
static eCxx::VoidCallback acIntFunc = nullptr;


//
// A simple AVR analog comparator wrapper
//
AnalogComparator::AnalogComparator()
{ reset(); }


bool AnalogComparator::begin()
{
    // Simply exit if the analog comparator is already enabled
    if(AC0.CTRLA & AC_ENABLE_bm) return false;

    // A constant to indicate that the pin will be unused
    static constexpr uint8_t PinUnused = AVR::InvalidPin - 1;

    // Get and check the pins
    uint8_t pinp = AVR::InvalidPin;
    switch(inputp) {
        case InputP::P0 : pinp = AVR::AC0_InPos0; break;
        case InputP::P1 : pinp = AVR::AC0_InPos1; break;
        case InputP::P2 : pinp = AVR::AC0_InPos2; break;
        case InputP::P3 : pinp = AVR::AC0_InPos3; break;
    }
    if(pinp == AVR::InvalidPin) return false;

    uint8_t pinn = AVR::InvalidPin;
    switch(inputn) {
        case InputN::N0       : pinn = AVR::AC0_InNeg0; break;
        case InputN::N1       : pinn = AVR::AC0_InNeg1; break;
        case InputN::N2       : pinn = AVR::AC0_InNeg2; break;
        case InputN::Internal : pinn = PinUnused;       break;
    }
    if(pinn == AVR::InvalidPin) return false;

    const uint8_t pino = (output != Output::Disabled) ? AVR::AC0_Out : PinUnused;
    if(pino == AVR::InvalidPin) return false;

    // Configure the positive input pin
    {
        PORT port = pinNumberToPort(pinp);
        REG8 creg = getPinControlRegister( port, pinNumberToBitPosition(pinp) );

        port->DIRCLR  = pinNumberToBitmask(pinp);
        *creg        &= ~PORT_PULLUPEN_bm;
        *creg        |=  PORT_ISC_INPUT_DISABLE_gc;
    }

    // Configure the negative input pin
    if(pinn != PinUnused) {
        PORT port = pinNumberToPort(pinn);
        REG8 creg = getPinControlRegister( port, pinNumberToBitPosition(pinn) );

        port->DIRCLR  = pinNumberToBitmask(pinn);
        *creg        &= ~PORT_PULLUPEN_bm;
        *creg        |=  PORT_ISC_INPUT_DISABLE_gc;
    }

    // Configure the output pin
    if(pino != PinUnused) {
        pinNumberToPort(pino)->DIRSET = pinNumberToBitmask(pino);
    }

    // Configure the voltage reference
    VREF.CTRLA &= ~VREF_AC0REFSEL_gm;
    VREF.CTRLA |= ( static_cast<uint8_t>(acVRef) << VREF_AC0REFSEL_gp );
    AC0.DACREF  = dacRef;

    // Clear the interrupt flag
    AC0.STATUS |= AC_CMP_bm;

    // Configure and enable the analog comparator
    AC0.MUXCTRLA = ( (output == Output::Inverted) ? AC_INVERT_bm : 0x00 )
                 | ( static_cast<uint8_t>(inputp) << AC_MUXPOS_gp       )
                 | ( static_cast<uint8_t>(inputn) << AC_MUXNEG_gp       );

    AC0.CTRLA    = ( (pino != PinUnused) ? AC_OUTEN_bm : 0x00         )
                 | ( static_cast<int8_t>(hysteresis) << AC_HYSMODE_gp )
                 | AC_ENABLE_bm;

    // Done
    return true;
}


bool AnalogComparator::end()
{
    // Simply exit if the analog comparator is currently disabled
    if( !(AC0.CTRLA & AC_ENABLE_bm) ) return false;

    // Disable the analog comparator
    AC0.CTRLA &= ~AC_ENABLE_bm;

    // Restore the voltage reference to maximum AVCC
    VREF.CTRLA &= ~VREF_AC0REFSEL_gm;
    VREF.CTRLA |=  VREF_AC0REFSEL_AVDD_gc;
    AC0.DACREF  =  255;

    // Disable the positive input pin
    uint8_t pinp = AVR::InvalidPin;
    switch(inputp) {
        case InputP::P0 : pinp = AVR::AC0_InPos0; break;
        case InputP::P1 : pinp = AVR::AC0_InPos1; break;
        case InputP::P2 : pinp = AVR::AC0_InPos2; break;
        case InputP::P3 : pinp = AVR::AC0_InPos3; break;
    }
    if(pinp != AVR::InvalidPin) {
        PORT port = pinNumberToPort(pinp);
        REG8 creg = getPinControlRegister( port, pinNumberToBitPosition(pinp) );

        *creg &= ~PORT_ISC_gm;
    }

    // Disable the negative input pin
    uint8_t pinn = AVR::InvalidPin;
    switch(inputn) {
        case InputN::N0 : pinn = AVR::AC0_InNeg0; break;
        case InputN::N1 : pinn = AVR::AC0_InNeg1; break;
        case InputN::N2 : pinn = AVR::AC0_InNeg2; break;
        default         : pinn = AVR::InvalidPin; break;
    }
    if(pinn != AVR::InvalidPin) {
        PORT port = pinNumberToPort(pinn);
        REG8 creg = getPinControlRegister( port, pinNumberToBitPosition(pinn) );

        *creg &= ~PORT_ISC_gm;
    }

    // Disable the output pin
    const uint8_t pino = (output != Output::Disabled) ? AVR::AC0_Out : AVR::InvalidPin;
    if(pino != AVR::InvalidPin) {
        PORT port = pinNumberToPort(pino);
        REG8 creg = getPinControlRegister( port, pinNumberToBitPosition(pino) );

        port->DIRCLR  = pinNumberToBitmask(pino);
        *creg        &= ~PORT_PULLUPEN_bm;
    }

    // Done
    return true;
}


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

    inputp     = InputP::P0;
    inputn     = InputN::N0;
    output     = Output::Disabled;
    acVRef     = ACVRef::ExternalAVCC;
    dacRef     = 0xFF;
    hysteresis = Hysteresis::Disabled;
}


bool AnalogComparator::getOutStatus()
{ return AC0.STATUS & AC_STATE_bm; }


void AnalogComparator::attachInterrupt(eCxx::VoidCallback userFunc, Application::InterruptMode mode)
{
    AC0.INTCTRL &= ~AC_CMP_bm;
    AC0.CTRLA   &= ~AC_INTMODE_gm;

    acIntFunc = userFunc;

    switch(mode) {
        case Application::InterruptMode::Rising  : AC0.CTRLA |= AC_INTMODE_POSEDGE_gc;  break;
        case Application::InterruptMode::Falling : AC0.CTRLA |= AC_INTMODE_NEGEDGE_gc;  break;
        case Application::InterruptMode::Change  : AC0.CTRLA |= AC_INTMODE_BOTHEDGE_gc; break;
    }

    AC0.STATUS  |= AC_CMP_bm;
    AC0.INTCTRL |= AC_CMP_bm;
}


void AnalogComparator::detachInterrupt()
{
    AC0.INTCTRL &= ~AC_CMP_bm;

    acIntFunc = nullptr;
}


//
// Analog comparator interrupt vector
//
ISR(AC0_AC_vect)
{
    // Call the user callback if specified
    if(acIntFunc) (*acIntFunc)();

    // Clear the interrupt flag
    AC0.STATUS |= AC_CMP_bm;
}


} // namespace eCxx
