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

/*
 * ----------------------------------------------------------------------------------
 * The code in this file is copied (with a few modifications) from:
 *     core_esp8266_wiring_digital.cpp - wiring digital implementation for esp8266
 * ----------------------------------------------------------------------------------
 * Copyright (C) 2015 Hristo Gochkov. All rights reserved.
 *
 * This file is part of the esp8266 core for Arduino environment.
 *
 * This library is free software; you can redistribute it and/or modify it under the
 * terms of the GNU Lesser General Public License as published by the Free Software
 * Foundation; either version 2.1 of the License, or (at your option) any later
 * version.
 *
 * This library 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 Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License along
 * with this library; if not, write to the Free Software Foundation, Inc., 51
 * Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
 * ----------------------------------------------------------------------------------
 */


#include "CoreLib.h"
#include "CoreSys.h"
#include "Schedule.h"


namespace eCxx {

namespace NodeMCU {


//
// Interrupt handler data and state
//
struct InterruptHandlerData {
    uint8_t        mode;
    VoidFuncPtrArg fn;
    void*          arg;
    bool           functional;
};


static InterruptHandlerData interruptHandlerData[16] = { { 0, nullptr, nullptr, false } };
static uint32_t             interruptRegistered      = 0;


//
// GPIO interrupt vector
//
static void ICACHE_RAM_ATTR gpio_interrupt_isr(void*) __ECXX_ATTR_OPTIMIZE_2;
static void ICACHE_RAM_ATTR gpio_interrupt_isr(void*)
{
    const uint32_t status = GPIE;
    GPIEC = status; // Clear the interrupts

    const uint32_t levels = GPI;
    if(status == 0 || interruptRegistered == 0) return;

    ETS_GPIO_INTR_DISABLE();

    int32_t  i           = 0;
    uint32_t changedbits = status & interruptRegistered;

    while(changedbits) {

        while( !( changedbits & _BV(i) ) ) ++i;
        changedbits &= ~_BV(i);

        InterruptHandlerData* handler = &interruptHandlerData[i];

        if( handler->fn && ( handler->mode == CHANGE || (handler->mode & 1) == !!( levels & _BV(i) ) ) ) {
            // Stop other interrupts before calling the user callback
            AtomicGuard g;

            if(handler->functional) {
                ArgStructure* localArg = static_cast<ArgStructure*>(handler->arg);

                if(localArg && localArg->interruptInfo) {
                    localArg->interruptInfo->pin   = i;
                    localArg->interruptInfo->value = NodeMCU::digitalRead(i);
                    localArg->interruptInfo->micro = micros();
                }
            }

            if(handler->arg) (                              *handler->fn )(handler->arg);
            else             (*reinterpret_cast<VoidFuncPtr>(handler->fn))(            );

        } // if

    } // while

    ETS_GPIO_INTR_ENABLE();
}


void ICACHE_RAM_ATTR interruptFunctional_handler(void* arg)
{
    ArgStructure* localArg = static_cast<ArgStructure*>(arg);

    if(localArg->functionInfo->reqScheduledFunction) {
#if 1
        scheduleFunction( [localArg]() {
            localArg->functionInfo->reqScheduledFunction( InterruptInfo( *(localArg->interruptInfo) ) );
        } );
#else
        scheduleFunction( std::bind(
            localArg->functionInfo->reqScheduledFunction, InterruptInfo( *(localArg->interruptInfo) )
        ) );
#endif
    }

    if(localArg->functionInfo->reqFunction) localArg->functionInfo->reqFunction();
}


//
// Helper functions
//
static void cleanupFunctional(void* arg)
{
    ArgStructure* localArg = static_cast<ArgStructure*>(arg);

    delete static_cast<FunctionInfo* >(localArg->functionInfo );
    delete static_cast<InterruptInfo*>(localArg->interruptInfo);
    delete localArg;
}


static void setInterruptHandlers(uint8_t pin, VoidFuncPtrArg userFunc, void* arg, uint8_t mode, bool functional)
{
    InterruptHandlerData* handler = &interruptHandlerData[pin];

    handler->mode = mode;
    handler->fn   = userFunc;

    // Clean on new attach without detach
    if(handler->functional && handler->arg) cleanupFunctional(handler->arg);

    handler->arg        = arg;
    handler->functional = functional;
}


static bool attachInterruptFunctionalArg(uint8_t pin, VoidFuncPtrArg userFunc, void* arg, uint8_t mode, bool functional)
{
    // Abort if the ISR is not in IRAM
    if( !NodeMCU::addressInIRAM(userFunc) ) {
        ::printf_P( PGM_STR("GPIO ISR is not in IRAM!\r\n").p );
        abort();
    }

    // Check the pin number
    if( !userGPIOPin<true>(pin) ) return false;

    // Attach interrupt
    ETS_GPIO_INTR_DISABLE();

    interruptRegistered |= _BV(pin);
    setInterruptHandlers(pin, userFunc, arg, mode, functional);

    GPC(pin) &= ~(0xF << GPCI);         // INT mode disabled
    GPIEC     = _BV(pin);               // Clear interrupt for this pin
    GPC(pin) |= ((mode & 0xF) << GPCI); // Set INT mode

    ETS_GPIO_INTR_ATTACH(gpio_interrupt_isr, &interruptRegistered);
    ETS_GPIO_INTR_ENABLE();

    return true;
}


//
// Interrupt-related functions
//
extern bool attachInterrupt(uint8_t pin, FunctionalInterruptCallback intRoutine, InterruptMode mode)
{
    InterruptInfo* ii = nullptr;
    FunctionInfo*  fi = new FunctionInfo;
    ArgStructure*  as = new ArgStructure;

    fi->reqFunction   = intRoutine;
    as->interruptInfo = ii;
    as->functionInfo  = fi;

    return attachInterruptFunctionalArg(pin, interruptFunctional_handler, as, static_cast<uint8_t>(mode), true);
}


extern bool attachScheduledInterrupt(uint8_t pin, FunctionalScheduledInterruptCallback scheduledIntRoutine, InterruptMode mode)
{
    InterruptInfo* ii = new InterruptInfo;
    FunctionInfo*  fi = new FunctionInfo;
    ArgStructure*  as = new ArgStructure;

    fi->reqScheduledFunction = scheduledIntRoutine;
    as->interruptInfo        = ii;
    as->functionInfo         = fi;

    return attachInterruptFunctionalArg(pin, interruptFunctional_handler, as, static_cast<uint8_t>(mode), true);
}


extern bool attachInterrupt(uint8_t pin, VoidFuncPtr userFunc, InterruptMode mode)
{ return attachInterruptFunctionalArg(pin, reinterpret_cast<VoidFuncPtrArg>(userFunc), nullptr, static_cast<uint8_t>(mode), false); }


extern bool attachInterruptArg(uint8_t pin, VoidFuncPtrArg userFunc, void* arg, InterruptMode mode)
{ return attachInterruptFunctionalArg(pin, userFunc, arg, static_cast<uint8_t>(mode), false); }


extern bool ICACHE_RAM_ATTR detachInterrupt(uint8_t pin)
{
    // Check the pin number
    if( !userGPIOPin<true>(pin) ) return false;

    // Detach interrupt
    ETS_GPIO_INTR_DISABLE();

    GPC(pin) &= ~(0xF << GPCI); // INT mode disabled
    GPIEC     = _BV(pin);       // Clear interrupt for this pin

    interruptRegistered &= ~_BV(pin);
    setInterruptHandlers(pin, nullptr, nullptr, 0, false);

    if(interruptRegistered) ETS_GPIO_INTR_ENABLE();

    return true;
}


} // namespace NodeMCU

} // namespace eCxx
