/**
 * @file tftp_server.c
 * @brief main application entry point
 *
 * @section License
 *
 * Copyright (C) 2021-2026 Oryx Embedded SARL. All rights reserved.
 * 
 * SPDX-License-Identifier: GPL-2.0-or-later
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This program 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.

 *
 * @author Oryx Embedded SARL (www.oryx-embedded.com)
 * @version 2.6.4
 **/

#include "core/net.h"
#include "drivers/mac/stm32f4xx_eth_driver.h"
#include "drivers/phy/lan8742_driver.h"
#include "dhcp/dhcp_client.h"
#include "ipv6/slaac.h"
#include "tftp/tftp_server.h"
#include "fs_port.h"

#include "date_time.h"
#include "resource_manager.h"
#include "update/update.h"
#include "core/mailbox.h"

#include "stm32f4xx_nucleo_144.h"
#include "drivers/flash/internal/stm32f4xx_flash_driver.h"

#if UPDATE_FALLBACK_SUPPORT == ENABLED
#include "version.h"
#endif

#include "debug.h"

//Ethernet interface configuration
#define APP_IF_NAME "eth0"
#define APP_HOST_NAME "tftp-server-demo"
#define APP_MAC_ADDR "00-AB-CD-EF-04-07"

#define APP_USE_DHCP_CLIENT ENABLED
#define APP_IPV4_HOST_ADDR "192.168.0.20"
#define APP_IPV4_SUBNET_MASK "255.255.255.0"
#define APP_IPV4_DEFAULT_GATEWAY "192.168.0.254"
#define APP_IPV4_PRIMARY_DNS "8.8.8.8"
#define APP_IPV4_SECONDARY_DNS "8.8.4.4"

#define APP_USE_SLAAC ENABLED
#define APP_IPV6_LINK_LOCAL_ADDR "fe80::407"
#define APP_IPV6_PREFIX "2001:db8::"
#define APP_IPV6_PREFIX_LENGTH 64
#define APP_IPV6_GLOBAL_ADDR "2001:db8::407"
#define APP_IPV6_ROUTER "fe80::1"
#define APP_IPV6_PRIMARY_DNS "2001:4860:4860::8888"
#define APP_IPV6_SECONDARY_DNS "2001:4860:4860::8844"

#define APP_START_ADDRESS 0x08020400

//Global variables
DhcpClientSettings dhcpClientSettings;
DhcpClientContext dhcpClientContext;
SlaacSettings slaacSettings;
SlaacContext slaacContext;
TftpServerSettings tftpServerSettings;
TftpServerContext tftpServerContext;
NetContext netContext;
NetInterface netInterfaces[1];

bool_t ledState = FALSE;
systime_t ledTime = 0;
bool_t linkState = FALSE;
bool_t prevLinkState = FALSE;

bool_t rebootRequested;
systime_t rebootTimestamp;

extern Image current_image;
extern Image backup_image;

//Forward declarations
void SystemClock_Config(void);
void ledTask(void);

void *tftpServerOpenFileCallback(const char_t *filename,
                                 const char_t *mode, bool_t writeAccess);

error_t tftpServerWriteFileCallback(void *file,
                                    size_t offset, const uint8_t *data, size_t length);

error_t tftpServerReadFileCallback(void *file,
                                   size_t offset, uint8_t *data, size_t size, size_t *length);

void tftpServerCloseFileCallback(void *file);

// User-defined functions
bool_t bootIsFallbackTriggered(void) {
    TRACE_INFO("Checking if fallback is triggered...\r\n");
    if (BSP_PB_GetState(BUTTON_USER) == SET) {
        HAL_Delay(100);
        TRACE_INFO("Fallback is triggered!\r\n");
        TRACE_INFO("Restoring previous firmware version...\r\n");
        return BSP_PB_GetState(BUTTON_USER) == SET;
    }

    return FALSE;
}

bool_t bootIsUpdateAvailable(void) {
    TRACE_INFO("Checking if update is available...\r\n");
#if 0 // Commented out temporarily to showcase fallback trigger
    if (BSP_PB_GetState(BUTTON_USER) == SET) {
        HAL_Delay(100);
        TRACE_INFO("Update is available!\r\n");
        TRACE_INFO("Activating firmware update mode...\r\n");
        return BSP_PB_GetState(BUTTON_USER) == SET;
    }
#endif
    return FALSE;
}

/**
 * @brief LED task
 **/

void ledTask(void) {
    //Check current time
    if ((int32_t) (systemTicks - ledTime) > 0) {
        //Toggle LED state
        if (ledState == 0) {
            BSP_LED_On(LED1);
            ledState = 1;
            ledTime = systemTicks + 100;
        } else {
            BSP_LED_Off(LED1);
            ledState = 0;
            ledTime = systemTicks + 900;
        }
    }
}

/**
 * @brief System clock configuration
 **/
void SystemClock_Config(void) {
    RCC_OscInitTypeDef RCC_OscInitStruct = {0};
    RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};

    //Enable Power Control clock
    __HAL_RCC_PWR_CLK_ENABLE();

    //The voltage scaling allows optimizing the power consumption when the
    //device is clocked below the maximum system frequency
    __HAL_PWR_VOLTAGESCALING_CONFIG(PWR_REGULATOR_VOLTAGE_SCALE1);

    //Enable HSE Oscillator and activate PLL with HSE as source
    RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
    RCC_OscInitStruct.HSEState = RCC_HSE_BYPASS;
    RCC_OscInitStruct.HSIState = RCC_HSI_OFF;
    RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
    RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;
    RCC_OscInitStruct.PLL.PLLM = 8;
    RCC_OscInitStruct.PLL.PLLN = 360;
    RCC_OscInitStruct.PLL.PLLP = RCC_PLLP_DIV2;
    RCC_OscInitStruct.PLL.PLLQ = 7;
    HAL_RCC_OscConfig(&RCC_OscInitStruct);

    //Enable overdrive mode
    HAL_PWREx_EnableOverDrive();

    //Select PLL as system clock source and configure the HCLK, PCLK1 and PCLK2
    //clocks dividers
    RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_SYSCLK | RCC_CLOCKTYPE_HCLK |
                                  RCC_CLOCKTYPE_PCLK1 | RCC_CLOCKTYPE_PCLK2;
    RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
    RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
    RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV4;
    RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV2;
    HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_5);
}

int main() {
    error_t error;
    cboot_error_t cerror;
    NetInterface *interface;
    NetSettings netSettings;
    MacAddr macAddr;
#if (APP_USE_DHCP_CLIENT == DISABLED)
    Ipv4Addr ipv4Addr;
#endif
#if (APP_USE_SLAAC == DISABLED)
    Ipv6Addr ipv6Addr;
#endif
    bool_t goToBootloader = FALSE;

    //HAL library initialization
    HAL_Init();
    //Configure the system clock
    SystemClock_Config();

    //Initialize kernel
    osInitKernel();
    //Configure debug UART
    debugInit(115200);

    //Initialize user button
    BSP_PB_Init(BUTTON_KEY, BUTTON_MODE_GPIO);

    //File system initialization
    error = fsInit();
    //Any error to report?
    if (error) {
        //Debug message
        TRACE_ERROR("Failed to initialize file system!\r\n");
    }

#if (STANDALONE_FALLBACK_SUPPORT == ENABLED)
    bool_t fallbackRequested = mailBoxIsFallbackRequested();
    bool_t fallbackAvailable = bootIsFallbackTriggered();

    if (fallbackRequested || fallbackAvailable) {
        // Clear the fallback requested flag
        mailBoxSetFallbackRequested(FALSE);

        // Validate the integrity of the fallback slot beforehand
        if (imageCheckIntegrity(&backup_image) != CBOOT_NO_ERROR) {
            goToBootloader = TRUE;
        }

        // perform fallback here
        cerror = memoryCopySlot(backup_image.activeSlot,
            current_image.activeSlot, current_image.activeSlot->size);
        if (cerror != CBOOT_NO_ERROR) {
            goToBootloader = TRUE;
        } else {
            cerror = memoryEraseSlot(backup_image.activeSlot,0,backup_image.activeSlot->size);
            if (cerror != CBOOT_NO_ERROR) {
                TRACE_ERROR("Failed to erase backup slot!\r\n");
            }
        }

        // Cleanup
        mailBoxSetUpdateRequested(FALSE);
        mailBoxSetFallbackRequested(FALSE);
    }

#endif

    // Step 1: Check if an update is explicitly requested
    bool_t updateRequested = mailBoxIsUpdateRequested();
    bool_t updateAvailable = bootIsUpdateAvailable();

    if (updateRequested || updateAvailable) {
        // Clear the update requested flag
        if (updateRequested)
            mailBoxSetUpdateRequested(FALSE);

        goToBootloader = TRUE;
    } else {
        // Step 2: Validate integrity of the current application image
        if (imageCheckIntegrity(&current_image) != CBOOT_NO_ERROR) {
            goToBootloader = TRUE;
        } else {
            // Step 3: Jump to application
            TRACE_INFO("Jumping to application...\r\n");
            mcuJumpToApplication(APP_START_ADDRESS);
        }
    }

    if (goToBootloader) {

        // Bootloader operation beyond this point

        //Start-up message
        TRACE_INFO("\r\n");
        TRACE_INFO("*****************************************\r\n");
        TRACE_INFO("*** Second Stage TFTP Bootloader Demo ***\r\n");
        TRACE_INFO("*****************************************\r\n");
        TRACE_INFO("Copyright: 2010-2026 Oryx Embedded SARL\r\n");
        TRACE_INFO("Compiled: %s %s\r\n", __DATE__, __TIME__);
        TRACE_INFO("Target: STM32F439\r\n");
        TRACE_INFO("\r\n");

        //LED configuration
        BSP_LED_Init(LED1);
        BSP_LED_Init(LED2);
        BSP_LED_Init(LED3);

        //Clear LEDs
        BSP_LED_Off(LED1);
        BSP_LED_Off(LED2);
        BSP_LED_Off(LED3);

        //Debug message
        TRACE_INFO("\r\nPreparing to receive firmware update image...\r\n");

        //Get default settings
        netGetDefaultSettings(&netSettings);
        //Network interfaces
        netSettings.interfaces = netInterfaces;
        netSettings.numInterfaces = arraysize(netInterfaces);

        //TCP/IP stack initialization
        error = netInit(&netContext, &netSettings);
        //Any error to report?
        if (error) {
            //Debug message
            TRACE_ERROR("Failed to initialize TCP/IP stack!\r\n");
        }

        //Configure the first Ethernet interface
        interface = &netInterfaces[0];

        //Set interface name
        netSetInterfaceName(interface, APP_IF_NAME);
        //Set host name
        netSetHostname(interface, APP_HOST_NAME);
        //Set host MAC address
        macStringToAddr(APP_MAC_ADDR, &macAddr);
        netSetMacAddr(interface, &macAddr);
        //Select the relevant network adapter
        netSetDriver(interface, &stm32f4xxEthDriver);
        netSetPhyDriver(interface, &lan8742PhyDriver);

        //Initialize network interface
        error = netConfigInterface(interface);
        //Any error to report?
        if (error) {
            //Debug message
            TRACE_ERROR("Failed to configure interface %s!\r\n", interface->name);
        }

#if (IPV4_SUPPORT == ENABLED)
#if (APP_USE_DHCP_CLIENT == ENABLED)
        //Get default settings
        dhcpClientGetDefaultSettings(&dhcpClientSettings);
        //Set the network interface to be configured by DHCP
        dhcpClientSettings.interface = interface;
        //Disable rapid commit option
        dhcpClientSettings.rapidCommit = FALSE;

        //DHCP client initialization
        error = dhcpClientInit(&dhcpClientContext, &dhcpClientSettings);
        //Failed to initialize DHCP client?
        if (error) {
            //Debug message
            TRACE_ERROR("Failed to initialize DHCP client!\r\n");
        }

        //Start DHCP client
        error = dhcpClientStart(&dhcpClientContext);
        //Failed to start DHCP client?
        if (error) {
            //Debug message
            TRACE_ERROR("Failed to start DHCP client!\r\n");
        }
#else
        //Set IPv4 host address
        ipv4StringToAddr(APP_IPV4_HOST_ADDR, &ipv4Addr);
        ipv4SetHostAddr(interface, ipv4Addr);

        //Set subnet mask
        ipv4StringToAddr(APP_IPV4_SUBNET_MASK, &ipv4Addr);
        ipv4SetSubnetMask(interface, ipv4Addr);

        //Set default gateway
        ipv4StringToAddr(APP_IPV4_DEFAULT_GATEWAY, &ipv4Addr);
        ipv4SetDefaultGateway(interface, ipv4Addr);

        //Set primary and secondary DNS servers
        ipv4StringToAddr(APP_IPV4_PRIMARY_DNS, &ipv4Addr);
        ipv4SetDnsServer(interface, 0, ipv4Addr);
        ipv4StringToAddr(APP_IPV4_SECONDARY_DNS, &ipv4Addr);
        ipv4SetDnsServer(interface, 1, ipv4Addr);
#endif
#endif

#if (IPV6_SUPPORT == ENABLED)
#if (APP_USE_SLAAC == ENABLED)
        //Get default settings
        slaacGetDefaultSettings(&slaacSettings);
        //Set the network interface to be configured
        slaacSettings.interface = interface;

        //SLAAC initialization
        error = slaacInit(&slaacContext, &slaacSettings);
        //Failed to initialize SLAAC?
        if (error) {
            //Debug message
            TRACE_ERROR("Failed to initialize SLAAC!\r\n");
        }

        //Start IPv6 address autoconfiguration process
        error = slaacStart(&slaacContext);
        //Failed to start SLAAC process?
        if (error) {
            //Debug message
            TRACE_ERROR("Failed to start SLAAC!\r\n");
        }
#else
        //Set link-local address
        ipv6StringToAddr(APP_IPV6_LINK_LOCAL_ADDR, &ipv6Addr);
        ipv6SetLinkLocalAddr(interface, &ipv6Addr);

        //Set IPv6 prefix
        ipv6StringToAddr(APP_IPV6_PREFIX, &ipv6Addr);
        ipv6SetPrefix(interface, 0, &ipv6Addr, APP_IPV6_PREFIX_LENGTH);

        //Set global address
        ipv6StringToAddr(APP_IPV6_GLOBAL_ADDR, &ipv6Addr);
        ipv6SetGlobalAddr(interface, 0, &ipv6Addr);

        //Set default router
        ipv6StringToAddr(APP_IPV6_ROUTER, &ipv6Addr);
        ipv6SetDefaultRouter(interface, 0, &ipv6Addr);

        //Set primary and secondary DNS servers
        ipv6StringToAddr(APP_IPV6_PRIMARY_DNS, &ipv6Addr);
        ipv6SetDnsServer(interface, 0, &ipv6Addr);
        ipv6StringToAddr(APP_IPV6_SECONDARY_DNS, &ipv6Addr);
        ipv6SetDnsServer(interface, 1, &ipv6Addr);
#endif
#endif

        //Get default settings
        tftpServerGetDefaultSettings(&tftpServerSettings);
        //Bind TFTP server to the desired interface
        tftpServerSettings.interface = &netInterfaces[0];
        //Listen to port 69
        tftpServerSettings.port = TFTP_PORT;
        //Open file callback function
        tftpServerSettings.openFileCallback = tftpServerOpenFileCallback;
        //Write file callback function
        tftpServerSettings.writeFileCallback = tftpServerWriteFileCallback;
        //Read file callback function
        tftpServerSettings.readFileCallback = tftpServerReadFileCallback;
        //Close file callback function
        tftpServerSettings.closeFileCallback = tftpServerCloseFileCallback;

        //TFTP server initialization
        error = tftpServerInit(&tftpServerContext, &tftpServerSettings);
        //Failed to initialize TFTP server?
        if (error) {
            //Debug message
            TRACE_ERROR("Failed to initialize TFTP server!\r\n");
        }

        //Start TFTP server
        error = tftpServerStart(&tftpServerContext);
        //Failed to start TFTP server?
        if (error) {
            //Debug message
            TRACE_ERROR("Failed to start TFTP server!\r\n");
        }

        //Configure SysTick
        SysTick_Config(SystemCoreClock / 1000);

        while (1) {
            //Handle TCP/IP events
            netTask(&netContext);
            //Process TCP server events
            tftpServerTask(&tftpServerContext);
            //LED blinking task
            ledTask();

            //Get ethernet interface link state
            linkState = netGetLinkState(&netInterfaces[0]);
            //Does Link state change?
            if (linkState != prevLinkState) {
                //Save new interface link state
                prevLinkState = linkState;

                //Check link state
                if (linkState == TRUE) {
                    //Debug message
                    TRACE_INFO("USER : Ethernet interface link is UP\r\n");
                } else {
                    //Debug message
                    TRACE_INFO("USER : Ethernet interface link is DOWN!\r\n");
                }
            }

            //Check whether a system reboot is requested
            if (rebootRequested) {
                //Check current time
                if ((systemTicks - rebootTimestamp) >= 1000) {
                    //Perform a system reset
                    NVIC_SystemReset();
                }
            }
        }
    }
}

/**
* @brief Open file callback function
* @param[in] filename NULL-terminated string specifying the filename
* @param[in] mode NULL-terminated string specifying the transfer mode
* @param[in] writeAccess Type of access permitted (read or write access)
* @return File handle
**/

void *tftpServerOpenFileCallback(const char_t *filename,
                                 const char_t *mode, bool_t writeAccess) {
    void *file;

    //Check access mode
    if (writeAccess) {
        //Open the specified file for writing
        file = fsOpenFile(filename, FS_FILE_MODE_WRITE |
                                    FS_FILE_MODE_CREATE | FS_FILE_MODE_TRUNC);
    } else {
        //Open the specified file for reading
        file = fsOpenFile(filename, FS_FILE_MODE_READ);
    }

    //Return file handle
    return file;
}


/**
 * @brief Write file callback function
 * @param[in] file Handle that identifies the file to be written
 * @param[in] offset Offset relative to the beginning of the file
 * @param[in] data Pointer to a buffer containing the data to be written
 * @param[in] length Number of data bytes to write
 * @return Error code
 **/

error_t tftpServerWriteFileCallback(void *file,
                                    size_t offset, const uint8_t *data, size_t length) {
    //Write data to the specified file
    return fsWriteFile(file, (void *) data, length);
}


/**
 * @brief Read file callback function
 * @param[in] file Handle that identifies the file to be read
 * @param[in] data Pointer to the buffer where to copy the data
 * @param[in] size Size of the buffer, in bytes
 * @param[out] length Number of data bytes that have been read
 * @return Error code
 **/

error_t tftpServerReadFileCallback(void *file,
                                   size_t offset, uint8_t *data, size_t size, size_t *length) {
    //Read data from the specified file
    return fsReadFile(file, data, size, length);
}


/**
 * @brief Close file callback function
 * @param[in] file Handle that identifies the file to be closed
 **/

void tftpServerCloseFileCallback(void *file) {
    //Close file
    fsCloseFile(file);
}
