/**
 * @file
 * @brief Example uBASIC-Plus porting layer for the uBASIC-Plus interpreter
 *  for STM32F4xx NUCLIO board
 * @author Steve Karg
 * @date 2025
 * @copyright SPDX-License-Identifier: MIT
 */
#include <stdio.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <limits.h>
#include <ctype.h>
#include "bacnet/basic/object/ai.h"
#include "bacnet/basic/object/ao.h"
#include "bacnet/basic/object/av.h"
#include "bacnet/basic/object/bi.h"
#include "bacnet/basic/object/bo.h"
#include "bacnet/basic/object/bv.h"
#include "bacnet/basic/object/device.h"
#include "bacnet/basic/object/ms-input.h"
#include "bacnet/basic/object/mso.h"
#include "bacnet/basic/object/msv.h"
#include "bacnet/basic/program/ubasic/ubasic.h"
#include "bacnet/basic/sys/mstimer.h"
#include "bacnet/wp.h"
#include "led.h"

#if defined(UBASIC_SCRIPT_HAVE_PRINT_TO_SERIAL)
/**
 * @brief Write a buffer to the serial port
 * @param msg Pointer to the buffer to write
 * @param n Number of bytes to write
 */
static void serial_write(const char *msg, uint16_t n)
{
    (void)msg;
    (void)n;
}

/**
 * @brief Gather key presses until new-line is recieved or buffer is full
 * @return return the next byte from the input stream, or EOF(-1) if no byte is
 *  available
 */
static int serial_getc(void)
{
    int ch = -1;

    return ch;
}
#endif

#if defined(UBASIC_SCRIPT_HAVE_HARDWARE_EVENTS)
static uint32_t Event_Mask;

/**
 * @brief Hardware event status bit
 * @param bit Event bit
 * @return 1 if the event is set, 0 otherwise
 */
static int8_t hw_event(uint8_t bit)
{
    if (bit < 32) {
        if (Event_Mask & (1UL << bit)) {
            return 1; // Event is set
        }
    }

    return 0; // Event is not set
}

/**
 * @brief Clear a hardware event state bit
 * @param bit Event bit
 */
static void hw_event_clear(uint8_t bit)
{
    if (bit < 32) {
        Event_Mask &= ~(1UL << bit);
    }
}
#endif

#if defined(UBASIC_SCRIPT_HAVE_STORE_VARS_IN_FLASH)
static uint8_t EEPROM_Buffer[256];

/**
 * @brief Read some data from the EEPROM
 * @param start_address EEPROM starting memory address
 * @param buffer data to store
 * @param length number of bytes of data to read
 */
static size_t
eepromRead(uint16_t start_address, uint8_t *buffer, uint16_t length)
{
    size_t bytes_read = 0;
    uint16_t i = 0;

    for (i = 0; i < length; i++) {
        if (start_address + i < sizeof(EEPROM_Buffer)) {
            buffer[i] = EEPROM_Buffer[start_address + i];
            bytes_read++;
        } else {
            break;
        }
    }

    return bytes_read;
}

/**
 * @brief Write some data to the EEPROM
 * @param start_address EEPROM starting memory address
 * @param buffer data to send
 * @param length number of bytes of data
 */
static size_t
eepromWrite(uint16_t start_address, uint8_t *buffer, uint16_t length)
{
    size_t bytes_written = 0;
    uint16_t i = 0;

    for (i = 0; i < length; i++) {
        if (start_address + i < sizeof(EEPROM_Buffer)) {
            EEPROM_Buffer[start_address + i] = buffer[i];
            bytes_written++;
        } else {
            break;
        }
    }

    return bytes_written;
}

#define UBASIC_FLASH_PAGE_SIZE 256

/**
 * @brief Write a variable to the EEPROM
 * @param Name Variable name
 * @param Vartype Variable type
 * @param datalen_bytes Data length in bytes
 * @param dataptr Pointer to the data
 */
static void variable_write(
    uint8_t Name, uint8_t vartype, uint8_t datalen_bytes, uint8_t *dataptr)
{
    // Calculate the starting address based on variable name
    uint16_t start_address = Name * UBASIC_FLASH_PAGE_SIZE;
    uint8_t buffer[UBASIC_FLASH_PAGE_SIZE];

    // Prepare the buffer with the variable type and data length
    buffer[0] = vartype; // First byte is the variable type
    buffer[1] = datalen_bytes; // Second byte is the data length
    for (uint8_t i = 0; i < datalen_bytes; i++) {
        buffer[i + 2] = dataptr[i]; // Copy the actual data into the buffer
    }

    // Write the buffer to EEPROM
    eepromWrite(start_address, buffer, datalen_bytes + 2);
}

/**
 * @brief Read a variable from the EEPROM
 * @param Name Variable name
 * @param Vartype Variable type
 * @param dataptr Pointer to store the data
 * @param datalen Pointer to store the data length
 */
static void
variable_read(uint8_t Name, uint8_t vartype, uint8_t *dataptr, uint8_t *datalen)
{
    // Calculate the starting address based on variable name
    uint8_t buffer[UBASIC_FLASH_PAGE_SIZE] = { 0 };
    uint16_t start_address = Name * UBASIC_FLASH_PAGE_SIZE;

    // Read the data from EEPROM
    eepromRead(start_address, buffer, UBASIC_FLASH_PAGE_SIZE);
    // Check if the variable type matches
    if (buffer[0] == vartype) {
        *datalen = buffer[1]; // Get the data length
        for (uint8_t i = 0; i < *datalen; i++) {
            dataptr[i] =
                buffer[i + 2]; // Copy the actual data into the provided pointer
        }
    } else {
        *datalen = 0; // If type does not match, set length to 0
    }
}
#endif

#if (                                              \
    defined(UBASIC_SCRIPT_HAVE_TICTOC_CHANNELS) || \
    defined(UBASIC_SCRIPT_HAVE_SLEEP) ||           \
    defined(UBASIC_SCRIPT_HAVE_INPUT_FROM_SERIAL))
/* defined in mstimer.h */
#endif

#if defined(UBASIC_SCRIPT_HAVE_RANDOM_NUMBER_GENERATOR)
/**
 * @brief Generate a random number
 * @param size Size of the random number in bits
 * @return Random number size-bits wide
 */
static uint32_t random_uint32(uint8_t size)
{
    uint32_t value = 0, grains = 0;
    uint8_t k, i;
    static bool initialized = false;

    if (!initialized) {
        srand(0);
        initialized = true;
    }
    for (k = 0; k < 4; k++) {
        grains = 0;
        for (i = 0; i < (size >> 1); i++) {
            /* Two LS bits are most likely most random */
            grains |= (rand() & 0x00000003) << (2 * i);
        }
        value ^= grains;
    }

    return value;
}
#endif

#if defined(UBASIC_SCRIPT_HAVE_PWM_CHANNELS)
static int32_t dutycycle_pwm_ch[UBASIC_SCRIPT_HAVE_PWM_CHANNELS];

/**
 * @brief Configure the PWM
 * @param psc Prescaler
 * @param per Period
 */
static void pwm_config(uint16_t psc, uint16_t per)
{
    (void)psc;
    (void)per;
}

/**
 * @brief Write a value to the PWM
 * @param ch Channel
 * @param dutycycle Duty cycle
 */
static void pwm_write(uint8_t ch, int32_t dutycycle)
{
    if (ch < UBASIC_SCRIPT_HAVE_PWM_CHANNELS) {
        dutycycle_pwm_ch[ch] = dutycycle;
    }
}

/**
 * @brief Read a value from the PWM
 * @param ch Channel
 * @return Duty cycle
 */
static int32_t pwm_read(uint8_t ch)
{
    if (ch < UBASIC_SCRIPT_HAVE_PWM_CHANNELS) {
        return dutycycle_pwm_ch[ch];
    }
    return 0;
}
#endif

#if defined(UBASIC_SCRIPT_HAVE_ANALOG_READ)
/**
 * @brief Configure the ADC
 * @param sampletime Sample time
 * @param nreads Number of reads
 */
static void adc_config(uint8_t sampletime, uint8_t nreads)
{
    (void)sampletime;
    (void)nreads;
}

/**
 * @brief Read a value from the ADC
 * @param channel Channel
 * @return ADC value
 */
static int32_t adc_read(uint8_t channel)
{
    return (int32_t)random_uint32(12);
}
#endif

#if defined(UBASIC_SCRIPT_HAVE_GPIO_CHANNELS)
/**
 * @brief Configure the GPIO
 * @param ch Channel
 * @param mode Mode
 * @param freq Frequency
 */
static void gpio_config(uint8_t ch, int8_t mode, uint8_t freq)
{
    (void)ch;
    (void)mode;
    (void)freq;
}

/**
 * @brief Write a value to the GPIO
 * @param ch Channel
 * @param pin_state Pin state
 */
static void gpio_write(uint8_t ch, uint8_t pin_state)
{
    unsigned int led_index;

    if (ch == 0) {
        return;
    }
    /* adjust for zero based index */
    led_index = ch - 1;
    if (pin_state) {
        led_on(led_index);
    } else {
        led_off(led_index);
    }
}

/**
 * @brief Read a value from the GPIO
 * @param ch Channel
 * @return GPIO value
 */
static int32_t gpio_read(uint8_t ch)
{
    return 0;
}
#endif

#if defined(UBASIC_SCRIPT_HAVE_BACNET)
/**
 * @brief Create a BACnet object
 * @param object_type Object type
 * @param instance Object instance
 * @param object_name Object name
 */
static void
bacnet_create_object(uint16_t object_type, uint32_t instance, char *object_name)
{
    uint32_t r;

    switch (object_type) {
        case OBJECT_ANALOG_INPUT:
            if (!Analog_Input_Valid_Instance(instance)) {
                r = Analog_Input_Create(instance);
                if (r == instance) {
                    Analog_Input_Name_Set(instance, strdup(object_name));
                }
            }
            break;
        case OBJECT_ANALOG_OUTPUT:
            if (!Analog_Output_Valid_Instance(instance)) {
                r = Analog_Output_Create(instance);
                if (r == instance) {
                    Analog_Output_Name_Set(instance, strdup(object_name));
                }
            }
            break;
        case OBJECT_ANALOG_VALUE:
            if (!Analog_Value_Valid_Instance(instance)) {
                r = Analog_Value_Create(instance);
                if (r == instance) {
                    Analog_Value_Name_Set(instance, strdup(object_name));
                }
            }
            break;
        case OBJECT_BINARY_INPUT:
            if (!Binary_Input_Valid_Instance(instance)) {
                r = Binary_Input_Create(instance);
                if (r == instance) {
                    Binary_Input_Name_Set(instance, strdup(object_name));
                }
            }
            break;
        case OBJECT_BINARY_OUTPUT:
            if (!Binary_Output_Valid_Instance(instance)) {
                r = Binary_Output_Create(instance);
                if (r == instance) {
                    Binary_Output_Name_Set(instance, strdup(object_name));
                }
            }
            break;
        case OBJECT_BINARY_VALUE:
            if (!Binary_Value_Valid_Instance(instance)) {
                r = Binary_Value_Create(instance);
                if (r == instance) {
                    Binary_Value_Name_Set(instance, strdup(object_name));
                }
            }
            break;
        case OBJECT_MULTI_STATE_INPUT:
            if (!Multistate_Input_Valid_Instance(instance)) {
                r = Multistate_Input_Create(instance);
                if (r == instance) {
                    Multistate_Input_Name_Set(instance, strdup(object_name));
                }
            }
            break;
        case OBJECT_MULTI_STATE_OUTPUT:
            if (!Multistate_Output_Valid_Instance(instance)) {
                r = Multistate_Output_Create(instance);
                if (r == instance) {
                    Multistate_Output_Name_Set(instance, strdup(object_name));
                }
            }
            break;
        case OBJECT_MULTI_STATE_VALUE:
            if (!Multistate_Value_Valid_Instance(instance)) {
                r = Multistate_Value_Create(instance);
                if (r == instance) {
                    Multistate_Value_Name_Set(instance, strdup(object_name));
                }
            }
            break;
        default:
            break;
    }
}

/**
 * @brief Write a property to a BACnet object
 * @param object_type Object type
 * @param instance Object instance
 * @param property_id Property ID
 * @param value Property value
 */
static void bacnet_write_property(
    uint16_t object_type,
    uint32_t instance,
    uint32_t property_id,
    UBASIC_VARIABLE_TYPE value)
{
    BACNET_BINARY_PV value_binary = BINARY_INACTIVE;

    switch (object_type) {
        case OBJECT_ANALOG_INPUT:
            if (property_id == PROP_PRESENT_VALUE) {
                Analog_Input_Present_Value_Set(
                    instance, fixedpt_tofloat(value));
            }
            break;
        case OBJECT_ANALOG_OUTPUT:
            if (property_id == PROP_PRESENT_VALUE) {
                Analog_Output_Present_Value_Set(
                    instance, fixedpt_tofloat(value), BACNET_MAX_PRIORITY);
            }
            break;
        case OBJECT_ANALOG_VALUE:
            if (property_id == PROP_PRESENT_VALUE) {
                Analog_Value_Present_Value_Set(
                    instance, fixedpt_tofloat(value), BACNET_MAX_PRIORITY);
            }
            break;
        case OBJECT_BINARY_INPUT:
            if (property_id == PROP_PRESENT_VALUE) {
                if (fixedpt_toint(value) != 0) {
                    value_binary = BINARY_ACTIVE;
                }
                Binary_Input_Present_Value_Set(instance, value_binary);
            }
            break;
        case OBJECT_BINARY_OUTPUT:
            if (property_id == PROP_PRESENT_VALUE) {
                if (fixedpt_toint(value) != 0) {
                    value_binary = BINARY_ACTIVE;
                }
                Binary_Output_Present_Value_Set(
                    instance, value_binary, BACNET_MAX_PRIORITY);
            }
            break;
        case OBJECT_BINARY_VALUE:
            if (property_id == PROP_PRESENT_VALUE) {
                if (fixedpt_toint(value) != 0) {
                    value_binary = BINARY_ACTIVE;
                }
                Binary_Value_Present_Value_Set(instance, value_binary);
            }
            break;
        case OBJECT_MULTI_STATE_INPUT:
            if (property_id == PROP_PRESENT_VALUE) {
                Multistate_Input_Present_Value_Set(
                    instance, fixedpt_toint(value));
            }
            break;
        case OBJECT_MULTI_STATE_OUTPUT:
            if (property_id == PROP_PRESENT_VALUE) {
                Multistate_Output_Present_Value_Set(
                    instance, fixedpt_toint(value), BACNET_MAX_PRIORITY);
            }
            break;
        case OBJECT_MULTI_STATE_VALUE:
            if (property_id == PROP_PRESENT_VALUE) {
                Multistate_Value_Present_Value_Set(
                    instance, fixedpt_toint(value));
            }
            break;
        default:
            break;
    }
}

/**
 * @brief Read a property from a BACnet object
 * @param object_type Object type
 * @param instance Object instance
 * @param property_id Property ID
 * @return Property value
 */
static UBASIC_VARIABLE_TYPE bacnet_read_property(
    uint16_t object_type, uint32_t instance, uint32_t property_id)
{
    UBASIC_VARIABLE_TYPE value = 0;
    float value_float = 0.0;
    BACNET_BINARY_PV value_binary = BINARY_INACTIVE;

    switch (object_type) {
        case OBJECT_ANALOG_INPUT:
            if (property_id == PROP_PRESENT_VALUE) {
                value_float = Analog_Input_Present_Value(instance);
                value = fixedpt_fromfloat(value_float);
            }
            break;
        case OBJECT_ANALOG_OUTPUT:
            if (property_id == PROP_PRESENT_VALUE) {
                value_float = Analog_Output_Present_Value(instance);
                value = fixedpt_fromfloat(value_float);
            }
            break;
        case OBJECT_ANALOG_VALUE:
            if (property_id == PROP_PRESENT_VALUE) {
                value_float = Analog_Value_Present_Value(instance);
                value = fixedpt_fromfloat(value_float);
            }
            break;
        case OBJECT_BINARY_INPUT:
            if (property_id == PROP_PRESENT_VALUE) {
                value_binary = Binary_Input_Present_Value(instance);
                value =
                    fixedpt_fromint((value_binary == BINARY_ACTIVE) ? 1 : 0);
            }
            break;
        case OBJECT_BINARY_OUTPUT:
            if (property_id == PROP_PRESENT_VALUE) {
                value_binary = Binary_Output_Present_Value(instance);
                value =
                    fixedpt_fromint((value_binary == BINARY_ACTIVE) ? 1 : 0);
            }
            break;
        case OBJECT_BINARY_VALUE:
            if (property_id == PROP_PRESENT_VALUE) {
                value_binary = Binary_Value_Present_Value(instance);
                value =
                    fixedpt_fromint((value_binary == BINARY_ACTIVE) ? 1 : 0);
            }
            break;
        case OBJECT_MULTI_STATE_INPUT:
            if (property_id == PROP_PRESENT_VALUE) {
                value =
                    fixedpt_fromint(Multistate_Input_Present_Value(instance));
            }
            break;
        case OBJECT_MULTI_STATE_OUTPUT:
            if (property_id == PROP_PRESENT_VALUE) {
                value =
                    fixedpt_fromint(Multistate_Output_Present_Value(instance));
            }
            break;
        case OBJECT_MULTI_STATE_VALUE:
            if (property_id == PROP_PRESENT_VALUE) {
                value =
                    fixedpt_fromint(Multistate_Value_Present_Value(instance));
            }
            break;
        default:
            break;
    }

    return value;
}
#endif

/**
 * @brief Initialize the hardware drivers
 * @param data Pointer to the ubasic data structure
 */
void ubasic_port_init(struct ubasic_data *data)
{
#if (                                              \
    defined(UBASIC_SCRIPT_HAVE_TICTOC_CHANNELS) || \
    defined(UBASIC_SCRIPT_HAVE_SLEEP) ||           \
    defined(UBASIC_SCRIPT_HAVE_INPUT_FROM_SERIAL))
    data->mstimer_now = mstimer_now;
#endif
    data->variable_write = variable_write;
    data->variable_read = variable_read;
#if defined(UBASIC_SCRIPT_HAVE_HARDWARE_EVENTS)
    data->hw_event = hw_event;
    data->hw_event_clear = hw_event_clear;
#endif
#if defined(UBASIC_SCRIPT_HAVE_PWM_CHANNELS)
    data->pwm_config = pwm_config;
    data->pwm_write = pwm_write;
    data->pwm_read = pwm_read;
#endif
#if defined(UBASIC_SCRIPT_HAVE_PWM_CHANNELS)
    data->adc_config = adc_config;
    data->adc_read = adc_read;
#endif
#if defined(UBASIC_SCRIPT_HAVE_GPIO_CHANNELS)
    data->gpio_config = gpio_config;
    data->gpio_write = gpio_write;
    data->gpio_read = gpio_read;
#endif
#if defined(UBASIC_SCRIPT_HAVE_RANDOM_NUMBER_GENERATOR)
    data->random_uint32 = random_uint32;
#endif
#if defined(UBASIC_SCRIPT_HAVE_PRINT_TO_SERIAL)
    data->serial_write = serial_write;
#endif
#if defined(UBASIC_SCRIPT_HAVE_INPUT_FROM_SERIAL)
    data->ubasic_getc = serial_getc;
#endif
#if defined(UBASIC_SCRIPT_HAVE_BACNET)
    data->bacnet_create_object = bacnet_create_object;
    data->bacnet_write_property = bacnet_write_property;
    data->bacnet_read_property = bacnet_read_property;
#endif
}
