/*
 * 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:
 *     SPI.cpp - SPI 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
 * ----------------------------------------------------------------------------------
 */


#include "CoreLib.h"
#include "CoreUtil.h"


namespace eCxx {

namespace NodeMCU {


//
// Helper data types
//
union SPIClock {
    uint32_t regValue;
    struct {
        unsigned regL   :  6;
        unsigned regH   :  6;
        unsigned regN   :  6;
        unsigned regPre : 13;
        unsigned regEQU :  1;
    };
};


//
// Helper functions
//
static inline uint32_t clkRegToFreq(const SPIClock* reg)
{ return ESP8266_CLOCK / ( (reg->regPre + 1) * (reg->regN + 1) ); }


static inline void setDataBits(uint16_t bits)
{
    const uint32_t mask = ~( (SPIMMOSI << SPILMOSI) | (SPIMMISO << SPILMISO) );

    --bits;
    SPI1U1 = ( (SPI1U1 & mask) | ( (bits << SPILMOSI) | (bits << SPILMISO) ) );
}


static inline void setBitOrder(uint8_t bitOrder)
{
    if(bitOrder == MSBFIRST) SPI1C &= ~(SPICWBO | SPICRBO);
    else                     SPI1C |=  (SPICWBO | SPICRBO);
}


static inline void setDataMode(uint8_t dataMode)
{
    // SPI_MODE0 (0x00) : CPOL = 0 ; CPHA = 0
    // SPI_MODE1 (0x01) : CPOL = 0 ; CPHA = 1
    // SPI_MODE2 (0x10) : CPOL = 1 ; CPHA = 0
    // SPI_MODE3 (0x11) : CPOL = 1 ; CPHA = 1
    bool CPOL = (dataMode & 0x10); // Clock polarity
    bool CPHA = (dataMode & 0x01); // Clock phase

    // Ensure same behavior as SAM, AVR and Intel Boards
    // https://github.com/esp8266/Arduino/issues/2416
    // https://github.com/esp8266/Arduino/pull/2418
    if(CPOL) CPHA ^= 0x1;

    if(CPHA) SPI1U |=  (SPIUSME);
    else     SPI1U &= ~(SPIUSME);

    if(CPOL) SPI1P |=  _BV(29);
    else     SPI1P &= ~_BV(29);
}


static inline void setClockDivider(uint32_t clockDiv)
{
    // Set bit 9 if system clock is required
    if(clockDiv == 0x80000000) GPMUX |=  _BV(9);
    else                       GPMUX &= ~_BV(9);

    SPI1CLK = clockDiv;
}


static inline void setFrequency(uint32_t freq)
{
    // Check if it has been set to the same frequency before
    static uint32_t lastSetFrequency = 0;

    if(lastSetFrequency == freq) return;

    // Check for maximum possible frequency
    if(freq >= ESP8266_CLOCK) {
        setClockDivider(0x80000000);
        return;
    }

    // Check for minimum possible frequency
    const SPIClock minFreqReg = { 0x7FFFF020 };
    const uint32_t minFreq    = clkRegToFreq(&minFreqReg);

    if(freq < minFreq) {
        setClockDivider(minFreqReg.regValue);
        lastSetFrequency = freq;
        return;
    }

    // Find the best match for the requested frequency
    SPIClock bestReg  = { 0 };
    int32_t  bestFreq = 0;
    uint8_t  calN     = 1;

    while(calN <= 0x3F) { // 63 (max for regN:6)
        SPIClock reg        = { 0 };
        int8_t   calPreVari = -2;
        int32_t  calPre;
        int32_t  calFreq;

        reg.regN = calN;

        // Test different variants for calPre
        while(calPreVari++ <= 1) {
            calPre = ( ( (ESP8266_CLOCK / (reg.regN + 1) ) / freq) - 1 ) + calPreVari;

                 if(calPre >  0x1FFF) reg.regPre = 0x1FFF; // 8191 (max for regPre:13)
            else if(calPre <=      0) reg.regPre = 0;
            else                      reg.regPre = calPre;

            reg.regL = (reg.regN + 1) / 2;

            // Test the frequency
            calFreq = clkRegToFreq(&reg);

            // If an accurate match is found, use it ...
            if( calFreq == static_cast<int32_t>(freq) ) {
                memcpy( &bestReg, &reg, sizeof(bestReg) );
                break;
            }
            // ... otherwise, do not go over the requested frequency
            else if( calFreq < static_cast<int32_t>(freq) ) {
                const int32_t cal  = abs(static_cast<int32_t>(freq) - calFreq );
                const int32_t best = abs(static_cast<int32_t>(freq) - bestFreq);
                if(cal < best) {
                    bestFreq = calFreq;
                    memcpy( &bestReg, &reg, sizeof(bestReg) );
                }
            }
        }
        // Found an accurate match, use it
        if( calFreq == static_cast<int32_t>(freq) ) {
            // accurate match use it!
            break;
        }
        ++calN;
    }

    // Set the clock divider
    setClockDivider(bestReg.regValue);
    lastSetFrequency = freq;
}


//
// HSPI master low-level functions
//
extern void hspiMaster_begin()
{
    // NOTE : The SS pin is handled by the higher layer

    // Set pin mode
    NodeMCU::pinMode(NodeMCU::SPI_MOSI, SPECIAL); // GPIO13
    NodeMCU::pinMode(NodeMCU::SPI_MISO, SPECIAL); // GPIO12
    NodeMCU::pinMode(NodeMCU::SPI_SCK,  SPECIAL); // GPIO14

    // Enable SPI in master mode using the safest setting
    SPI1C  = 0;
    SPI1U  = SPIUMOSI | SPIUDUPLEX | SPIUSSE;
    SPI1U1 = (7 << SPILMOSI) | (7 << SPILMISO);
    SPI1C1 = 0;

    setFrequency(eCxx::SPI::DefaultSPIFrequency);
    setBitOrder (MSBFIRST);
    setDataMode (0x00);
}


extern void hspiMaster_end()
{
    // NOTE : The SS pin is handled by the higher layer

    // Set pin mode
    NodeMCU::pinMode(NodeMCU::SPI_MOSI, INPUT); // GPIO13
    NodeMCU::pinMode(NodeMCU::SPI_MISO, INPUT); // GPIO12
    NodeMCU::pinMode(NodeMCU::SPI_SCK,  INPUT); // GPIO14
}


extern void hspiMaster_setConfig(uint32_t freq, uint8_t bitOrder, uint8_t dataMode)
{
    while(SPI1CMD & SPIBUSY);

    setFrequency(freq);
    setBitOrder (bitOrder);
    setDataMode (dataMode);
}


extern uint8_t hspiMaster_transfer(uint8_t data)
{
    while(SPI1CMD & SPIBUSY);

    setDataBits(8);

    SPI1W0   = data;
    SPI1CMD |= SPIBUSY;

    while(SPI1CMD & SPIBUSY);

    return SPI1W0 & 0xFF;
}


} // namespace NodeMCU

} // namespace eCxx
