/*
 * 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_si2c.cpp - software I2C library 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
 * ----------------------------------------------------------------------------------
 */


#ifndef ECXX_PLATFORM_NODEMCU_SOFT_TWI_DRIVER_H
#define ECXX_PLATFORM_NODEMCU_SOFT_TWI_DRIVER_H


namespace eCxx {

namespace NodeMCU {


//
// Inline helper functions
//
static inline void SDA_LOW(uint8_t sda) __ECXX_ATTR_ALWAYS_INLINE;
static inline void SDA_LOW(uint8_t sda)
{ GPES = _BV(sda); }


static inline void SDA_HIGH(uint8_t sda) __ECXX_ATTR_ALWAYS_INLINE;
static inline void SDA_HIGH(uint8_t sda)
{ GPEC = _BV(sda); }


static inline bool SDA_READ(uint8_t sda) __ECXX_ATTR_ALWAYS_INLINE;
static inline bool SDA_READ(uint8_t sda)
{ return GPI & _BV(sda); }


static inline void SCL_LOW(uint8_t scl) __ECXX_ATTR_ALWAYS_INLINE;
static inline void SCL_LOW(uint8_t scl)
{ GPES = _BV(scl); }


static inline void SCL_HIGH(uint8_t scl) __ECXX_ATTR_ALWAYS_INLINE;
static inline void SCL_HIGH(uint8_t scl)
{ GPEC = _BV(scl); }


static inline bool SCL_READ(uint8_t scl) __ECXX_ATTR_ALWAYS_INLINE;
static inline bool SCL_READ(uint8_t scl)
{ return GPI & _BV(scl); }


//
// Software TWI driver
//
class SoftTWIDriver {
    public:
        SoftTWIDriver(const SoftTWIDriver& ) = delete;
        SoftTWIDriver(      SoftTWIDriver&&) = delete;

        SoftTWIDriver& operator=(const SoftTWIDriver& ) = delete;
        SoftTWIDriver& operator=(      SoftTWIDriver&&) = delete;

        SoftTWIDriver() = delete;
        ~SoftTWIDriver() = delete;

    public:
        using OnSlaveTransmit = eCxx::VoidCallback;
        using OnSlaveReceive  = eCxx::VoidCallback_UInt8Ptr_Size;

    public:
        static void setClock(uint32_t freq);
        static void setClockStretchLimit(uint32_t limit);

        static void init(uint8_t sda, uint8_t scl);
        static void setAddress(uint8_t address);
        static bool enableSlave();

        static TWIResultCode writeTo(uint8_t address, uint8_t* buf, uint32_t len, uint8_t sendStop);
        static TWIResultCode readFrom(uint8_t address, uint8_t* buf, uint32_t len, uint8_t sendStop);

        static uint8_t status();
        static TWIResultCode transmit(const uint8_t* data, uint8_t length);

        static void attachSlaveRxEvent(OnSlaveReceive func);
        static void attachSlaveTxEvent(OnSlaveTransmit func);

        static void ICACHE_RAM_ATTR reply(bool ack) __ECXX_ATTR_NOINLINE;
        static void ICACHE_RAM_ATTR releaseBus() __ECXX_ATTR_NOINLINE;

    private:
        static uint32_t preferredTWIClock;
        static uint8_t  sdaPin;
        static uint8_t  sclPin;
        static uint8_t  twiAddr;
        static uint32_t twiClockStretchLimit;
        static uint32_t twiDCount;

        enum TWIPMode {
            TWIPM_UNKNOWN = 0,
            TWIPM_IDLE,
            TWIPM_ADDRESSED,
            TWIPM_WAIT
        };

        enum TWIPState {
            TWIP_UNKNOWN = 0,
            TWIP_IDLE,
            TWIP_START,
            TWIP_SEND_ACK,
            TWIP_WAIT_ACK,
            TWIP_WAIT_STOP,
            TWIP_SLA_W,
            TWIP_SLA_R,
            TWIP_REP_START,
            TWIP_READ,
            TWIP_STOP,
            TWIP_REC_ACK,
            TWIP_READ_ACK,
            TWIP_RWAIT_ACK,
            TWIP_WRITE,
            TWIP_BUS_ERR
        };

        enum TWIState {
            TWI_READY = 0,
            TWI_MRX,
            TWI_MTX,
            TWI_SRX,
            TWI_STX
        };

        static constexpr uint8_t SoftTWIDriverBufferSize = 32;

        static volatile TWIPMode  twipMode;
        static volatile TWIPState twipState;
        static volatile int32_t   twipStatus;
        static volatile int32_t   bitCount;

        static volatile uint8_t   twiData;
        static volatile bool      twiAck;
        static volatile bool      twiAckRec;
        static volatile int32_t   twiTimeoutMS;

        static volatile TWIState  twistate;
        static volatile uint8_t   twiError;

        static          uint8_t txBuffer[SoftTWIDriverBufferSize];
        static volatile int32_t txBufferIndex;
        static volatile int32_t txBufferLength;

        static          uint8_t rxBuffer[SoftTWIDriverBufferSize];
        static volatile int32_t rxBufferIndex;

        // Allow not linking in the slave code if there is no call to enableSlave()
        static bool slaveEnabled;

        static OnSlaveReceive  onSlaveReceive;
        static OnSlaveTransmit onSlaveTransmit;

        // ETS queue and timer interfaces
        enum {
            TWI_SIG_RANGE = 0x00000100,
            TWI_SIG_RX    = 0x00000101,
            TWI_SIG_TX    = 0x00000102
        };

        static ETSEvent eventTaskQueue[SoftTWIQueueSize];
        static ETSTimer timer;

        // Event/IRQ callbacks
        static void ICACHE_RAM_ATTR onTimer(void* unused) __ECXX_ATTR_NOINLINE __ECXX_ATTR_OPTIMIZE_2;

        static void ICACHE_RAM_ATTR onSdaChange() __ECXX_ATTR_OPTIMIZE_2;
        static void ICACHE_RAM_ATTR onSclChange() __ECXX_ATTR_OPTIMIZE_2;

        static void eventTask(ETSEvent* e) __ECXX_ATTR_OPTIMIZE_2;

        // Helper functions
        static void ICACHE_RAM_ATTR busyWait(uint32_t v) __ECXX_ATTR_NOINLINE;
        static void ICACHE_RAM_ATTR onTwipEvent(uint8_t status);

        static bool writeStart();
        static bool writeStop();

        static bool writeBit(bool bit);
        static bool readBit();

        static bool writeByte(uint8_t byte);
        static uint8_t readByte(bool nack);

        // Handle the case where a slave needs to stretch the clock with a time-limited busy wait
        inline static void WAIT_CLOCK_STRETCH() __ECXX_ATTR_ALWAYS_INLINE
        {
            PolledTimeout::oneShotFastUs  timeout(twiClockStretchLimit);
            PolledTimeout::periodicFastUs yieldTimeout(5000);

            // Outer loop is stretch duration up to stretch limit
            while( !timeout && !SCL_READ(sclPin) ) {
                // Inner loop yields every 5 mS
                if(yieldTimeout) yield();
            }
        }

        // Generate a clock "valley" at the end of a segment, just before a repeated start
        static void sclPinGenValley();
};


} // namespace NodeMCU

} // namespace eCxx


#endif
