/*!\file
 *
 * SUMMARY
 *  \brief This file contains the main function of the application framework,
 *  which the application programmer uses to configure and run applications.
 *
 *
 * This is the main entrance class for most of the neural models. The following
 * Figure shows how all of the c code interacts with each other and what classes
 * are used to represent over arching logic
 * (such as plasticity, spike processing, utilities, synapse types, models)
 *
 * @image html spynnaker_c_code_flow.png
 *
 */

#include <common/in_spikes.h>
#include "neuron.h"
#include "synapses.h"
#include <neuron/spike_processing.h>
#include <neuron/population_table/population_table.h>
#include <neuron/plasticity/synapse_dynamics.h>

#include <data_specification.h>
#include <simulation.h>
#include <debug.h>

#include "fp_math.h"

/* validates that the model being compiled does indeed contain a application
   magic number*/
#ifndef APPLICATION_NAME_HASH
#error APPLICATION_NAME_HASH was undefined.  Make sure you define this\
       constant
#endif

//! human readable definitions of each region in SDRAM
typedef enum regions_e{
    SYSTEM_REGION,
    NEURON_PARAMS_REGION,
    SYNAPSE_PARAMS_REGION,
    POPULATION_TABLE_REGION,
    SYNAPTIC_MATRIX_REGION,
    SYNAPSE_DYNAMICS_REGION,
    BUFFERING_OUT_SPIKE_RECORDING_REGION,
    BUFFERING_OUT_POTENTIAL_RECORDING_REGION,
    BUFFERING_OUT_GSYN_RECORDING_REGION,
    BUFFERING_OUT_CONTROL_REGION,
    PROVENANCE_DATA_REGION,
    BUFFER_REGION
} regions_e;

typedef enum extra_provenance_data_region_entries{
    NUMBER_OF_PRE_SYNAPTIC_EVENT_COUNT = 0,
    SYNAPTIC_WEIGHT_SATURATION_COUNT = 1,
    INPUT_BUFFER_OVERFLOW_COUNT = 2,
    CURRENT_TIMER_TICK = 3,
} extra_provenance_data_region_entries;

//! values for the priority for each callback
typedef enum callback_priorities{
    MC = -1, SDP_AND_DMA_AND_USER = 0, TIMER_AND_BUFFERING = 2
} callback_priorities;

//! The number of regions that are to be used for recording
#define NUMBER_OF_REGIONS_TO_RECORD 3

// Globals

//! the current timer tick value TODO this might be able to be removed with
//! the timer tick callback returning the same value.
uint32_t time;

//! The number of timer ticks to run for before being expected to exit
static uint32_t simulation_ticks = 0;

//! Determines if this model should run for infinite time
static uint32_t infinite_run;

//! The recording flags
static uint32_t recording_flags = 0;

//! The EIEIO prefix types
typedef enum eieio_prefix_types {
    PREFIX_TYPE_LOWER_HALF_WORD, PREFIX_TYPE_UPPER_HALF_WORD
} eieio_prefix_types;

typedef struct {
    uint16_t eieio_header_command;
    uint16_t chip_id;
    uint8_t processor;
    uint8_t pad1;
    uint8_t region;
    uint8_t sequence;
    uint32_t space_available;
} req_packet_sdp_t;

typedef enum eieio_data_message_types {
    KEY_16_BIT, KEY_PAYLOAD_16_BIT, KEY_32_BIT, KEY_PAYLOAD_32_bIT
} eieio_data_message_types;

#define MIN_BUFFER_SPACE 10
#define MAX_PACKET_SIZE 280
#define TICKS_BETWEEN_REQUESTS 25
#define SPIKE_HISTORY_CHANNEL 0

static bool apply_prefix;
static bool check;
static uint32_t prefix;
static bool has_key;
static uint32_t key_space;
static uint32_t mask;
static uint32_t incorrect_keys;
static uint32_t incorrect_packets;
static uint32_t late_packets;
static uint32_t last_stop_notification_request;
static eieio_prefix_types prefix_type;
static uint32_t buffer_region_size;
static uint32_t space_before_data_request;

static uint8_t *buffer_region;
static uint8_t *end_of_buffer_region;
static uint8_t *write_pointer;
static uint8_t *read_pointer;

sdp_msg_t req;
req_packet_sdp_t *req_ptr;
static eieio_msg_t msg_from_sdram;
static bool msg_from_sdram_in_use;
static int msg_from_sdram_length;
static uint32_t next_buffer_time;
static uint8_t pkt_last_sequence_seen;
static bool send_packet_reqs;
static bool last_buffer_operation;
static uint8_t return_tag_id;
static uint32_t last_space;
static uint32_t last_request_tick;



// spindle data
REAL sp_len;
REAL sp_dlen;
uint8_t data_has_come;


//! \brief Initialises the recording parts of the model
//! \return True if recording initialisation is successful, false otherwise
static bool initialise_recording(){
    address_t address = data_specification_get_data_address();
    address_t system_region = data_specification_get_region(
        SYSTEM_REGION, address);
    regions_e regions_to_record[] = {
        BUFFERING_OUT_SPIKE_RECORDING_REGION,
        BUFFERING_OUT_POTENTIAL_RECORDING_REGION,
        BUFFERING_OUT_GSYN_RECORDING_REGION
    };
    uint8_t n_regions_to_record = NUMBER_OF_REGIONS_TO_RECORD;
    uint32_t *recording_flags_from_system_conf =
        &system_region[SIMULATION_N_TIMING_DETAIL_WORDS];
    regions_e state_region = BUFFERING_OUT_CONTROL_REGION;

    bool success = recording_initialize(
        n_regions_to_record, regions_to_record,
        recording_flags_from_system_conf, state_region,
        TIMER_AND_BUFFERING, &recording_flags);
    log_info("Recording flags = 0x%08x", recording_flags);
    return success;
}

bool read_net_parameters(address_t region_address) {

    // Get the configuration data
    apply_prefix = 1;//region_address[APPLY_PREFIX];
    prefix = 128;//region_address[PREFIX];
    prefix_type = (eieio_prefix_types) 1;//region_address[PREFIX_TYPE];
    check = 0;//region_address[CHECK_KEYS];
    has_key = 1;//region_address[HAS_KEY];
    key_space = 128;//region_address[KEY_SPACE];
    mask = 4294967168;//region_address[MASK];
    buffer_region_size = 0;// region_address[BUFFER_REGION_SIZE];
    space_before_data_request = 0;// region_address[SPACE_BEFORE_DATA_REQUEST];
    return_tag_id = 0;// region_address[RETURN_TAG_ID];

    // There is no point in sending requests until there is space for
    // at least one packet
    if (space_before_data_request < MIN_BUFFER_SPACE) {
        space_before_data_request = MIN_BUFFER_SPACE;
    }

    // Set the initial values
    incorrect_keys = 0;
    incorrect_packets = 0;
    msg_from_sdram_in_use = false;
    next_buffer_time = 0;
    pkt_last_sequence_seen = MAX_SEQUENCE_NO;
    send_packet_reqs = true;
    last_request_tick = 0;

    if (buffer_region_size != 0) {
        last_buffer_operation = BUFFER_OPERATION_WRITE;
    } else {
        last_buffer_operation = BUFFER_OPERATION_READ;
    }

    // allocate a buffer size of the maximum SDP payload size
    msg_from_sdram = (eieio_msg_t) spin1_malloc(MAX_PACKET_SIZE);

    req.length = 8 + sizeof(req_packet_sdp_t);
    req.flags = 0x7;
    req.tag = return_tag_id;
    req.dest_port = 0xFF;
    req.srce_port = (1 << 5) | spin1_get_core_id();
    req.dest_addr = 0;
    req.srce_addr = spin1_get_chip_id();
    req_ptr = (req_packet_sdp_t*) &(req.cmd_rc);
    req_ptr->eieio_header_command = 1 << 14 | SPINNAKER_REQUEST_BUFFERS;
    req_ptr->chip_id = spin1_get_chip_id();
    req_ptr->processor = (spin1_get_core_id() << 3);
    req_ptr->pad1 = 0;
    req_ptr->region = BUFFER_REGION & 0x0F;

//    log_info("apply_prefix: %d", apply_prefix);
//    log_info("prefix: %d", prefix);
//    log_info("prefix_type: %d", prefix_type);
//    log_info("check: %d", check);
//    log_info("key_space: 0x%08x", key_space);
//    log_info("mask: 0x%08x", mask);
//    log_info("space_before_read_request: %d", space_before_data_request);
//    log_info("return_tag_id: %d", return_tag_id);

    buffer_region = (uint8_t *) region_address;
    read_pointer = buffer_region;
    write_pointer = buffer_region;
    end_of_buffer_region = buffer_region + buffer_region_size;

//    log_info("buffer_region: 0x%.8x", buffer_region);
//    log_info("buffer_region_size: %d", buffer_region_size);
//    log_info("end_of_buffer_region: 0x%.8x", end_of_buffer_region);

    return true;
}


//! \brief Initialises the model by reading in the regions and checking
//!        recording data.
//! \param[in] timer_period a pointer for the memory address where the timer
//!            period should be stored during the function.
//! \return True if it successfully initialised, false otherwise
static bool initialise(uint32_t *timer_period) {
    log_info("Initialise: started");

    // Get the address this core's DTCM data starts at from SRAM
    address_t address = data_specification_get_data_address();

    // Read the header
    if (!data_specification_read_header(address)) {
        return false;
    }

    // Get the timing details
    address_t system_region = data_specification_get_region(
        SYSTEM_REGION, address);
    if (!simulation_read_timing_details(
            system_region, APPLICATION_NAME_HASH, timer_period)) {
        return false;
    }

    // setup recording region
    if (!initialise_recording()){
        return false;
    }

    // Set up the neurons
    uint32_t n_neurons;
    uint32_t incoming_spike_buffer_size;
    if (!neuron_initialise(
            data_specification_get_region(NEURON_PARAMS_REGION, address),
            recording_flags, &n_neurons, &incoming_spike_buffer_size)) {
        return false;
    }

    // Set up the synapses
    input_t *input_buffers;
    uint32_t *ring_buffer_to_input_buffer_left_shifts;
    if (!synapses_initialise(
            data_specification_get_region(SYNAPSE_PARAMS_REGION, address),
            n_neurons, &input_buffers,
            &ring_buffer_to_input_buffer_left_shifts)) {
        return false;
    }
    neuron_set_input_buffers(input_buffers);

    // Set up the population table
    uint32_t row_max_n_words;
    if (!population_table_initialise(
            data_specification_get_region(POPULATION_TABLE_REGION, address),
            data_specification_get_region(SYNAPTIC_MATRIX_REGION, address),
            &row_max_n_words)) {
        return false;
    }

    // Set up the synapse dynamics
    if (!synapse_dynamics_initialise(
            data_specification_get_region(SYNAPSE_DYNAMICS_REGION, address),
            n_neurons, ring_buffer_to_input_buffer_left_shifts)) {
        return false;
    }

    if (!spike_processing_initialise(
            row_max_n_words, MC, SDP_AND_DMA_AND_USER, SDP_AND_DMA_AND_USER,
            incoming_spike_buffer_size)) {
        return false;
    }

    if (!read_net_parameters(data_specification_get_region(
                                 BUFFER_REGION, address))) {
        return false;
    }

    log_info("Initialise: finished");
    return true;
}

void c_main_store_provenance_data(address_t provenance_region){
    //log_debug("writing other provenance data");

    // store the data into the provenance data region
    provenance_region[NUMBER_OF_PRE_SYNAPTIC_EVENT_COUNT] =
        synapses_get_pre_synaptic_events();
    provenance_region[SYNAPTIC_WEIGHT_SATURATION_COUNT] =
        synapses_get_saturation_count();
    provenance_region[INPUT_BUFFER_OVERFLOW_COUNT] =
        spike_processing_get_buffer_overflows();
    provenance_region[CURRENT_TIMER_TICK] = time;
    //log_debug("finished other provenance data");
}

void resume_callback() {
    // restart the recording status
    if (!initialise_recording()) {
        log_error("Error setting up recording");
        rt_error(RTE_SWERR);
    }
}







static inline void process_16_bit_packets_custom(
        void* event_pointer, bool pkt_prefix_upper, uint32_t pkt_count,
        uint32_t pkt_key_prefix, uint32_t pkt_payload_prefix,
        bool pkt_has_payload, bool pkt_payload_is_timestamp) {

//    log_info("process_16_bit_packets");
//    log_info("event_pointer: %08x", (uint32_t) event_pointer);
//    log_info("count: %d", pkt_count);
//    log_info("pkt_prefix: %08x", pkt_key_prefix);
//    log_info("pkt_payload_prefix: %08x", pkt_payload_prefix);
//    log_info("payload on: %d", pkt_has_payload);
//    log_info("pkt_format: %d", pkt_prefix_upper);

    // read length and speed
    uint16_t *next_event = (uint16_t *) event_pointer;
    sp_len = (REAL) next_event[0];
    next_event += 1;
    sp_len += fp_div((REAL) next_event[0], REAL_CONST(1000.0));
    next_event += 2;
    sp_dlen = fp_div((REAL) next_event[0], REAL_CONST(1000.0));
    next_event -= 1;
    if (next_event[0] < 32767) {
        sp_dlen += (REAL) next_event[0];
    } else {
        sp_dlen += (REAL)(next_event[0] - 32767);
        sp_dlen = -sp_dlen;
    }
//    sp_dlen = (REAL) next_event[0];
//    if (sp_dlen > 32767) {
//        sp_dlen = -(sp_dlen - 32767);
//    }

    data_has_come = 1;

//    log_info("received (%d.%03d), (%d.%03d) length: %k, dlength: %k",
//             ((uint16_t *)event_pointer)[0],
//             ((uint16_t *)event_pointer)[1],
//             ((uint16_t *)event_pointer)[2],
//             ((uint16_t *)event_pointer)[3],
//             sp_len, sp_dlen);

}


static inline bool eieio_data_parse_packet(
        eieio_msg_t eieio_msg_ptr, uint32_t length) {
    //log_debug("eieio_data_process_data_packet");
//    print_packet_bytes(eieio_msg_ptr, length);

    uint16_t data_hdr_value = eieio_msg_ptr[0];
    void *event_pointer = (void *) &eieio_msg_ptr[1];

    if (data_hdr_value == 0) {

        // Count is 0, so no data
        return true;
    }

    //log_debug("====================================");
    //log_debug("eieio_msg_ptr: %08x", (uint32_t) eieio_msg_ptr);
    //log_debug("event_pointer: %08x", (uint32_t) event_pointer);
//    print_packet(eieio_msg_ptr);

    bool pkt_apply_prefix = (bool) ((data_hdr_value >> 15) & 0x1);
    bool pkt_prefix_upper = (bool) ((data_hdr_value >> 14) & 0x1);
    bool pkt_payload_apply_prefix = (bool) ((data_hdr_value >> 13) & 0x1);
    uint8_t pkt_type = (uint8_t) ((data_hdr_value >> 10) & 0x3);
    uint8_t pkt_count = (uint8_t) (data_hdr_value & 0xFF);
    bool pkt_has_payload = (bool) (pkt_type & 0x1);

    uint32_t pkt_key_prefix = 0;
    uint32_t pkt_payload_prefix = 0;
    bool pkt_payload_is_timestamp = (bool)((data_hdr_value >> 12) & 0x1);

    //log_debug("data_hdr_value: %04x", data_hdr_value);
    //log_debug("pkt_apply_prefix: %d", pkt_apply_prefix);
    //log_debug("pkt_format: %d", pkt_prefix_upper);
    //log_debug("pkt_payload_prefix: %d", pkt_payload_apply_prefix);
    //log_debug("pkt_timestamp: %d", pkt_payload_is_timestamp);
    //log_debug("pkt_type: %d", pkt_type);
    //log_debug("pkt_count: %d", pkt_count);
    //log_debug("payload_on: %d", pkt_has_payload);

    uint16_t *hdr_pointer = (uint16_t *) event_pointer;

    if (pkt_apply_prefix) {

        // Key prefix in the packet
        pkt_key_prefix = (uint32_t) hdr_pointer[0];
        hdr_pointer += 1;

        // If the prefix is in the upper part, shift the prefix
        if (pkt_prefix_upper) {
            pkt_key_prefix <<= 16;
        }
    } else if (!pkt_apply_prefix && apply_prefix) {

        // If there isn't a key prefix, but the config applies a prefix,
        // apply the prefix depending on the key_left_shift
        pkt_key_prefix = prefix;
        if (prefix_type == PREFIX_TYPE_UPPER_HALF_WORD) {
            pkt_prefix_upper = true;
        } else {
            pkt_prefix_upper = false;
        }
    }

//    if (pkt_payload_apply_prefix) {

//        log_info("ciaone");

//        if (!(pkt_type & 0x2)) {

//            // If there is a payload prefix and the payload is 16-bit
//            pkt_payload_prefix = (uint32_t) hdr_pointer[0];
//            hdr_pointer += 1;
//        } else {

//            // If there is a payload prefix and the payload is 32-bit
//            pkt_payload_prefix =
//                (((uint32_t) hdr_pointer[1] << 16) |
//                 (uint32_t) hdr_pointer[0]);
//            hdr_pointer += 2;
//        }
//    }

    // Take the event pointer to start at the header pointer
    event_pointer = (void *) hdr_pointer;

    // If the packet has a payload that is a timestamp, but the timestamp
    // is not the current time, buffer it
//    if (pkt_has_payload && pkt_payload_is_timestamp &&
//            pkt_payload_prefix != time) {
//        log_info("packet has payload");
//        if (pkt_payload_prefix > time) {
//            log_info("adding packet to sdram");
//            add_eieio_packet_to_sdram(eieio_msg_ptr, length);
//            return true;
//        }
//        late_packets += 1;
//        return false;
//    }

    if (pkt_type <= 1) {
//        log_info("received 16bit packet");
        process_16_bit_packets_custom(
            event_pointer, pkt_prefix_upper, pkt_count, pkt_key_prefix,
            pkt_payload_prefix, pkt_has_payload, pkt_payload_is_timestamp);
//        if (recording_flags > 0) {
//            //log_debug("recording a eieio message with length %u", length);
//            recording_record(SPIKE_HISTORY_CHANNEL, eieio_msg_ptr, length);
//        }
        return true;
    }/* else {
        log_info("received 32bit packet");
        process_32_bit_packets(
            event_pointer, pkt_count, pkt_key_prefix,
            pkt_payload_prefix, pkt_has_payload, pkt_payload_is_timestamp);
        if (recording_flags > 0) {
            //log_debug("recording a eieio message with length %u", length);
            recording_record(SPIKE_HISTORY_CHANNEL, eieio_msg_ptr, length);
        }
        return false;
    }*/
}


//! \brief Timer interrupt callback
//! \param[in] timer_count the number of times this call back has been
//!            executed since start of simulation
//! \param[in] unused unused parameter kept for API consistency
//! \return None
void timer_callback(uint timer_count, uint unused) {
    use(timer_count);
    use(unused);

    time++;

    //log_debug("Timer tick %u \n", time);

    /* if a fixed number of simulation ticks that were specified at startup
       then do reporting for finishing */
    if (infinite_run != TRUE && time >= simulation_ticks) {

        // Enter pause and resume state to avoid another tick
        simulation_handle_pause_resume(resume_callback);

        // Finalise any recordings that are in progress, writing back the final
        // amounts of samples recorded to SDRAM
        if (recording_flags > 0) {
            log_info("updating recording regions");
            recording_finalise();
        }

        // Subtract 1 from the time so this tick gets done again on the next
        // run
        time -= 1;
        return;
    }

//    if (send_packet_reqs &&
//            ((time - last_request_tick) >= TICKS_BETWEEN_REQUESTS)) {
//        send_buffer_request_pkt();
//        last_request_tick = time;
//    }

//    if (!msg_from_sdram_in_use) {
//        fetch_and_process_packet();
//    } else if (next_buffer_time < time) {
//        late_packets += 1;
//        fetch_and_process_packet();
//    } else if (next_buffer_time == time) {
//        eieio_data_parse_packet(msg_from_sdram, msg_from_sdram_length);
//        fetch_and_process_packet();
//    }


    // otherwise do synapse and neuron time step updates
    synapses_do_timestep_update(time, get_f_dyn(), get_f_st());
    neuron_do_timestep_update(time, sp_len, sp_dlen, data_has_come);

    // trigger buffering_out_mechanism
    if (recording_flags > 0) {
        recording_do_timestep_update(time);
    }
}

void sdp_packet_callback(uint mailbox, uint port) {

//    log_info("packet callback on port %d", port);

    use(port);
    sdp_msg_t *msg = (sdp_msg_t *) mailbox;
    uint16_t length = msg->length;
    eieio_msg_t eieio_msg_ptr = (eieio_msg_t) &(msg->cmd_rc);

//    packet_handler_selector(eieio_msg_ptr, length - 8);
    eieio_data_parse_packet(eieio_msg_ptr, length - 8);

    // free the message to stop overload
    spin1_msg_free(msg);
}

//! \brief The entry point for this model.
void c_main(void) {

    data_has_come = 0;

    // Load DTCM data
    uint32_t timer_period;

    // initialise the model
    if (!initialise(&timer_period)){
        rt_error(RTE_API);
    }

    // Start the time at "-1" so that the first tick will be 0
    time = UINT32_MAX;

    // Set timer tick (in microseconds)
    log_info("setting timer tick callback for %d microseconds",
              timer_period);
    spin1_set_timer_tick(timer_period);

    // Set up the timer tick callback (others are handled elsewhere)
    spin1_callback_on(TIMER_TICK, timer_callback, TIMER_AND_BUFFERING);

    // Set up callback listening to SDP messages
    simulation_register_simulation_sdp_callback(
        &simulation_ticks, &infinite_run, SDP_AND_DMA_AND_USER);

    // set up provenance registration
    simulation_register_provenance_callback(
        c_main_store_provenance_data, PROVENANCE_DATA_REGION);

    spin1_sdp_callback_on(
            BUFFERING_IN_SDP_PORT, sdp_packet_callback, SDP_AND_DMA_AND_USER);

    simulation_run();
}