/*
 * 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 slight modifications) from:
 *     spi_flash_defs.h              - SPI flash chip commands and status registers
 *     core_esp8266_spi_utils.cpp    - send a custom SPI command
 *     core_esp8266_flash_quirks.cpp - chip specific flash initialization
 * ----------------------------------------------------------------------------------
 * Copyright (C) 2019 Mike Nix. 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 "CoreSys.h"


//
// Flash chip related constants
//

// Flash chip Status register 3 - vendor XMC output drive levels
#define SPI_FLASH_SR3_XMC_DRV_25   1
#define SPI_FLASH_SR3_XMC_DRV_50   0
#define SPI_FLASH_SR3_XMC_DRV_75   2
#define SPI_FLASH_SR3_XMC_DRV_100  3

#define SPI_FLASH_SR3_XMC_DRV_S    5
#define SPI_FLASH_SR3_XMC_DRV_MASK 0x03

// Flash chip commands
#define SPI_FLASH_CMD_RSR1  0x05  // Read flash status register
#define SPI_FLASH_CMD_RSR2  0x35
#define SPI_FLASH_CMD_RSR3  0x15
#define SPI_FLASH_CMD_WSR1  0x01  // Write flash status register
#define SPI_FLASH_CMD_WSR2  0x31
#define SPI_FLASH_CMD_WSR3  0x11
#define SPI_FLASH_CMD_WEVSR 0x50  // Write enable volatile status registers
#define SPI_FLASH_CMD_WREN  0x06  // Write enable
#define SPI_FLASH_CMD_WRDI  0x04  // Write disable


//
// Function prototypes with "C" linkage
//
extern "C" eCxx::uint32_t Wait_SPI_Idle(SpiFlashChip* fc);


namespace eCxx {

namespace NodeMCU {


//
// SPI operation result
//
enum class SpiOpResult : uint8_t {
    OK,
    Error,
    Timeout
};


//
// Helper functions
//
static uint32_t getFlashMHz()
{
    uint32_t data;
    uint8_t* bytes = reinterpret_cast<uint8_t*>(&data);

    // Read the first 4 bytes (magic byte + flash configuration)
    // See also magicFlashChipSpeed() in "SysFlashHAL.cpp"
    if( spi_flash_read(0x0000, &data, 4) == SPI_FLASH_RESULT_OK ) {
        switch(bytes[3] & 0x0F) {
            case 0x0 : return 40; // 40 MHz
            case 0x1 : return 26; // 26 MHz
            case 0x2 : return 20; // 20 MHz
            case 0xF : return 80; // 80 MHz
        }
    }

    return 0;
}


static SpiOpResult PRECACHE_ATTR _SPICommand(
    volatile uint32_t  spiIfNum,
             uint32_t  spic,
             uint32_t  spiu,
             uint32_t  spiu1,
             uint32_t  spiu2,
             uint32_t* data,
             uint32_t  writeWords,
             uint32_t  readWords
)
{
    if(spiIfNum > 1) return SpiOpResult::Error;

    // Force SPI register access via [base + offest] and prevents
    // loading individual address constants from flash
    volatile uint32_t* spiBasRegAddr = ( spiIfNum ? &(SPI1CMD) : &(SPI0CMD) );
    #define SPIREG(REG) ( *( spiBasRegAddr + ( &(REG) - &(SPI0CMD) ) ) )

    // Preload some constants and functions into variables (use volatile to prevent the optimizer
    // from treating them as constants that will result in flash reads)
    using WaitSPIIdle_VPtrT = uint32_t(*volatile)(SpiFlashChip*);

             WaitSPIIdle_VPtrT vptrWaitSPIIdle = &Wait_SPI_Idle;
    volatile SpiFlashChip*     fchip           = flashchip;
    volatile uint32_t          spicmdusr       = SPICMDUSR;

    uint32_t savePS = 0;

    if(!spiIfNum) {
        // Only need to disable interrupts and precache when using SPI0
        savePS = xt_rsil(15);
        PRECACHE_START(_SPICommand);
        // Wait until idle
        vptrWaitSPIIdle( const_cast<SpiFlashChip*>(fchip) );
    }

    // Preserve essential controller state such as incoming/outgoing data lengths and IO mode
    const uint32_t oldSPI0C  = SPIREG(SPI0C );
    const uint32_t oldSPI0U  = SPIREG(SPI0U );
    const uint32_t oldSPI0U1 = SPIREG(SPI0U1);
    const uint32_t oldSPI0U2 = SPIREG(SPI0U2);

    SPIREG(SPI0C ) = spic;
    SPIREG(SPI0U ) = spiu;
    SPIREG(SPI0U1) = spiu1;
    SPIREG(SPI0U2) = spiu2;

    // Copy the outgoing data to the SPI hardware
    if(writeWords > 0) {
                 uint32_t* src = data;
        volatile uint32_t* dst = &SPIREG(SPI0W0);
        for(uint32_t i = 0; i < writeWords; ++i) *dst++ = *src++;
    }

    // Start the transfer
    SPIREG(SPI0CMD) = spicmdusr;

    // Wait for the command to complete (typically only 1-3 iterations)
    uint32_t timeout = 1000;
    while( ( SPIREG(SPI0CMD) & spicmdusr ) && timeout-- );

    // Copy the response back to the buffer
    if( (readWords > 0 ) && (timeout > 0 ) ) {
                 uint32_t* dst = data;
        volatile uint32_t* src = &SPIREG(SPI0W0);
        for(uint32_t i = 0; i < readWords; ++i) *dst++ = *src++;
    }

    // Restore saved registers
    SPIREG(SPI0U2) = oldSPI0U2;
    SPIREG(SPI0U1) = oldSPI0U1;
    SPIREG(SPI0U ) = oldSPI0U;
    SPIREG(SPI0C ) = oldSPI0C;

    // Done
    PRECACHE_END(_SPICommand);
    if(!spiIfNum) xt_wsr_ps(savePS);

    return (timeout > 0) ? SpiOpResult::OK : SpiOpResult::Timeout;
}


static SpiOpResult SPI0Command(uint8_t cmd, uint32_t* data, uint32_t mosiBits, uint32_t misoBits)
{
    if( mosiBits > 64 * 8 ) return SpiOpResult::Error;
    if( misoBits > 64 * 8 ) return SpiOpResult::Error;

    // Calculate the number of data words (registers) that need to be copied to/from the SPI controller
    uint32_t mosiWords = mosiBits / 32;
    uint32_t misoWords = misoBits / 32;

    if(mosiBits % 32 != 0) ++mosiWords;
    if(misoBits % 32 != 0) ++misoWords;

    // Select user defined command mode in the controller
    uint32_t spiu = SPIUCOMMAND;
    uint32_t spic = SPI0C;

    // Set the command byte to send
    uint32_t spiu1 = 0;
    uint32_t spiu2 = ( (7 & SPIMCOMMAND) << SPILCOMMAND ) | cmd;

    // Set the number of outgoing data bits to send
    if(mosiBits > 0) {
        spiu1 |= ( (mosiBits - 1) & SPIMMOSI ) << SPILMOSI;
        spiu  |= SPIUMOSI;
    }

    // Set the number of incoming bits to read
    if(misoBits > 0) {
        spiu1 |= ( (misoBits - 1) & SPIMMISO ) << SPILMISO;
        spiu  |= SPIUMISO;
    }

    // Select the most basic IO mode for maximum compatibility
    // (some flash commands are only available in this mode)
    spic &= ~( SPICQIO | SPICDIO | SPICQOUT | SPICDOUT | SPICAHB | SPICFASTRD );
    spic |=  ( SPICRESANDRES | SPICSHARE | SPICWPR | SPIC2BSE );

    // Execute the command
    SpiOpResult rc = _SPICommand(0, spic, spiu, spiu1, spiu2, data, mosiWords, misoWords);

    if(rc == SpiOpResult::OK) {
        // Clear any bits we did not read in the last word
        if(misoBits % 32) data[misoBits / 32] &= ~( 0xFFFFFFFF << ( misoBits % 32 ) );
    }

    return rc;
}


//
// Perform chip-specific flash initialization to improve performance and reliability
//
extern void initFlashQuirks()
{
    const uint32_t vendor = spi_flash_get_id() & 0x000000FF;

    if(vendor == SPI_FLASH_VENDOR_XMC) {
        // Read SR3
        uint32_t SR3;
        if(SPI0Command(SPI_FLASH_CMD_RSR3, &SR3, 0, 8) != SpiOpResult::OK ) return;
        // Set the output drive to 100% if the flash clock speed is > 26 MHz
        uint32_t newSR3 = SR3;
        if(getFlashMHz() > 26) {
            newSR3 &= ~( SPI_FLASH_SR3_XMC_DRV_MASK << SPI_FLASH_SR3_XMC_DRV_S );
            newSR3 |=  ( SPI_FLASH_SR3_XMC_DRV_100  << SPI_FLASH_SR3_XMC_DRV_S );
        }
        // Only write if changed
        if(newSR3 != SR3) {
            // Write enable volatile SR
            if(SPI0Command(SPI_FLASH_CMD_WEVSR, nullptr, 0, 0) == SpiOpResult::OK) {
                // Write SR3
                SPI0Command(SPI_FLASH_CMD_WSR3, &newSR3, 8, 0);
            }
            // Write disable
            SPI0Command(SPI_FLASH_CMD_WRDI, nullptr, 0, 0);
        }
    }
}


} // namespace NodeMCU

} // namespace eCxx
