/*
 * Copyright (c) 2014-2015 ARM Limited. All rights reserved.
 * SPDX-License-Identifier: Apache-2.0
 * Licensed under the Apache License, Version 2.0 (the License); you may
 * not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an AS IS BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include "ns_types.h"
#include "ns_list.h"
#include "ns_timer.h"
#include "eventOS_callback_timer.h"
#include "platform/arm_hal_interrupt.h"
#include "platform/arm_hal_timer.h"
#include "nsdynmemLIB.h"

#ifndef NS_EXCLUDE_HIGHRES_TIMER
typedef enum ns_timer_state_e {
    NS_TIMER_ACTIVE = 0,        // Will run on the next HAL interrupt
    NS_TIMER_HOLD,              // Will run on a later HAL interrupt
    NS_TIMER_RUN_INTERRUPT,     // Running on the interrupt we're currently handling
    NS_TIMER_STOP               // Timer not scheduled ("start" not called since last callback)
} ns_timer_state_e;

typedef struct ns_timer_struct {
    int8_t ns_timer_id;
    ns_timer_state_e timer_state;
    uint16_t slots;
    uint16_t remaining_slots;
    void (*interrupt_handler)(int8_t, uint16_t);
    ns_list_link_t link;
} ns_timer_struct;

static NS_LIST_DEFINE(ns_timer_list, ns_timer_struct, link);


#define NS_TIMER_RUNNING    1
static uint8_t ns_timer_state = 0;

#ifdef ATMEGA256RFR2
#define COMPENSATION 3
#define COMPENSATION_TUNE 1
#else
#define COMPENSATION 0
#define COMPENSATION_TUNE 0
#endif

static void ns_timer_interrupt_handler(void);
static ns_timer_struct *ns_timer_get_pointer_to_timer_struct(int8_t timer_id);
static bool ns_timer_initialized = 0;

int8_t eventOS_callback_timer_register(void (*timer_interrupt_handler)(int8_t, uint16_t))
{
    int8_t retval = -1;

    if (!ns_timer_initialized) {
        /*Set interrupt handler in HAL driver*/
        platform_timer_set_cb(ns_timer_interrupt_handler);
        ns_timer_initialized = 1;
    }

    /*Find first free timer ID in timer list*/
    /*(Note use of uint8_t to avoid overflow if we reach 0x7F)*/
    for (uint8_t i = 0; i <= INT8_MAX; i++) {
        if (!ns_timer_get_pointer_to_timer_struct(i)) {
            retval = i;
            break;
        }
    }

    if (retval == -1) {
        return -1;
    }

    ns_timer_struct *new_timer = ns_dyn_mem_alloc(sizeof(ns_timer_struct));
    if (!new_timer) {
        return -1;
    }

    /*Initialise new timer*/
    new_timer->ns_timer_id = retval;
    new_timer->timer_state = NS_TIMER_STOP;
    new_timer->remaining_slots = 0;
    new_timer->interrupt_handler = timer_interrupt_handler;

    // Critical section sufficient as long as list can't be reordered from
    // interrupt, otherwise will need to cover whole routine
    platform_enter_critical();
    ns_list_add_to_end(&ns_timer_list, new_timer);
    platform_exit_critical();

    /*Return timer ID*/
    return retval;
}

int8_t eventOS_callback_timer_unregister(int8_t ns_timer_id)
{
    ns_timer_struct *current_timer;

    current_timer = ns_timer_get_pointer_to_timer_struct(ns_timer_id);
    if (!current_timer) {
        return -1;
    }

    // Critical section sufficient as long as list can't be reordered from
    // interrupt, otherwise will need to cover whole routine
    platform_enter_critical();
    ns_list_remove(&ns_timer_list, current_timer);
    platform_exit_critical();

    ns_dyn_mem_free(current_timer);
    return 0;
}


static int8_t ns_timer_start_pl_timer(uint16_t pl_timer_start_slots)
{
    /*Don't start timer with 0 slots*/
    if (!pl_timer_start_slots) {
        pl_timer_start_slots = 1;
    }

    /*Start HAL timer*/
    platform_timer_start(pl_timer_start_slots);
    /*Set HAL timer state to running*/
    ns_timer_state |= NS_TIMER_RUNNING;
    return 0;
}

int8_t ns_timer_sleep(void)
{
    int8_t ret_val = -1;
    if (ns_timer_state & NS_TIMER_RUNNING) {
        /*Start HAL timer*/
        platform_timer_disable();
        /*Set HAL timer state to running*/
        ns_timer_state &= ~NS_TIMER_RUNNING;
        ret_val = 0;
    }
    return ret_val;
}

static int8_t ns_timer_get_next_running_to(void)
{
    uint8_t hold_count = 0;
    ns_timer_struct *first_timer = NULL;

    /*Find hold-labelled timer with the least remaining slots*/
    ns_list_foreach(ns_timer_struct, current_timer, &ns_timer_list) {
        if (current_timer->timer_state == NS_TIMER_HOLD) {
            if (!first_timer || current_timer->remaining_slots < first_timer->remaining_slots) {
                first_timer = current_timer;
            }
            /*For optimisation, count the found timers*/
            hold_count++;
        }
    }

    if (!first_timer) {
        return 0;
    }

    /*If hold-labelled timer found, set it active and start the HAL driver*/
    hold_count--;
    first_timer->timer_state = NS_TIMER_ACTIVE;
    /*Compensate time spent in timer function*/
    if (first_timer->remaining_slots > COMPENSATION) {
        first_timer->remaining_slots -= COMPENSATION;
    }
    /*Start HAL timer*/
    ns_timer_start_pl_timer(first_timer->remaining_slots);

    /*Update other hold-labelled timers*/
    ns_list_foreach(ns_timer_struct, current_timer, &ns_timer_list) {
        if (hold_count == 0) { // early termination optimisation
            break;
        }
        if (current_timer->timer_state == NS_TIMER_HOLD) {
            if (current_timer->remaining_slots == first_timer->remaining_slots) {
                current_timer->timer_state = NS_TIMER_ACTIVE;
            } else {
                current_timer->remaining_slots -= first_timer->remaining_slots;
                /*Compensate time spent in timer function*/
                if (current_timer->remaining_slots > COMPENSATION) {
                    current_timer->remaining_slots -= COMPENSATION;
                }
            }
            hold_count--;
        }
    }

    return 0;
}


static ns_timer_struct *ns_timer_get_pointer_to_timer_struct(int8_t timer_id)
{
    /*Find timer with the given ID*/
    ns_list_foreach(ns_timer_struct, current_timer, &ns_timer_list) {
        if (current_timer->ns_timer_id == timer_id) {
            return current_timer;
        }
    }
    return NULL;
}

int8_t eventOS_callback_timer_start(int8_t ns_timer_id, uint16_t slots)
{
    int8_t ret_val = 0;
    uint16_t pl_timer_remaining_slots;
    ns_timer_struct *timer;
    platform_enter_critical();

    /*Find timer to be activated*/
    timer = ns_timer_get_pointer_to_timer_struct(ns_timer_id);
    if (!timer) {
        ret_val = -1;
        goto exit;
    }

    // XXX this assumes the timer currently isn't running?
    // Is event.c relying on this restarting HAL timer after ns_timer_sleep()?

    /*If any timers are active*/
    if (ns_timer_state & NS_TIMER_RUNNING) {
        /*Get remaining slots of the currently activated timeout*/
        pl_timer_remaining_slots = platform_timer_get_remaining_slots();

        /*New timeout is shorter than currently enabled timeout*/
        if (pl_timer_remaining_slots > slots) {
            /*Start HAL timer*/
            ns_timer_start_pl_timer(slots - 0);

            ns_list_foreach(ns_timer_struct, current_timer, &ns_timer_list) {
                /*Switch active timers to hold*/
                if (current_timer->timer_state == NS_TIMER_ACTIVE) {
                    current_timer->timer_state = NS_TIMER_HOLD;
                    current_timer->remaining_slots = 0;
                }
                /*Update hold-labelled timers*/
                if (current_timer->timer_state == NS_TIMER_HOLD) {
                    current_timer->remaining_slots += (pl_timer_remaining_slots - slots);
                    /*Compensate time spent in timer function*/
                    if (current_timer->remaining_slots > (COMPENSATION - COMPENSATION_TUNE)) {
                        current_timer->remaining_slots -= (COMPENSATION - COMPENSATION_TUNE);
                    }
                }
            }
            /*Mark active and start the timer*/
            timer->timer_state = NS_TIMER_ACTIVE;
            timer->slots = slots;
            timer->remaining_slots = slots;
        }

        /*New timeout is longer than currently enabled timeout*/
        else if (pl_timer_remaining_slots < slots) {
            /*Mark hold and update remaining slots*/
            timer->timer_state = NS_TIMER_HOLD;
            timer->slots = slots;
            timer->remaining_slots = (slots - pl_timer_remaining_slots);
        }
        /*New timeout is equal to currently enabled timeout*/
        else {
            /*Mark it active and it will be handled in next interrupt*/
            timer->timer_state = NS_TIMER_ACTIVE;
            timer->slots = slots;
            timer->remaining_slots = slots;
        }
    } else {
        /*No timers running*/
        timer->timer_state = NS_TIMER_HOLD;
        timer->slots = slots;
        timer->remaining_slots = slots;
        /*Start next timeout*/
        ns_timer_get_next_running_to();
    }
exit:
    platform_exit_critical();
    return ret_val;
}

static void ns_timer_interrupt_handler(void)
{
    uint8_t i = 0;

    platform_enter_critical();
    /*Clear timer running state*/
    ns_timer_state &= ~NS_TIMER_RUNNING;
    /*Mark active timers as NS_TIMER_RUN_INTERRUPT, interrupt functions are called at the end of this function*/
    ns_list_foreach(ns_timer_struct, current_timer, &ns_timer_list) {
        if (current_timer->timer_state == NS_TIMER_ACTIVE) {
            current_timer->timer_state = NS_TIMER_RUN_INTERRUPT;
            /*For optimisation, count the found timers*/
            i++;
        }
    }

    /*Start next timeout*/
    ns_timer_get_next_running_to();

    /*Call interrupt functions*/
    ns_list_foreach(ns_timer_struct, current_timer, &ns_timer_list) {
        if (i == 0) {
            break;
        }
        if (current_timer->timer_state == NS_TIMER_RUN_INTERRUPT) {
            current_timer->timer_state = NS_TIMER_STOP;
            current_timer->interrupt_handler(current_timer->ns_timer_id, current_timer->slots);
            i--;
        }
    }

    platform_exit_critical();
}

int8_t eventOS_callback_timer_stop(int8_t ns_timer_id)
{
    uint16_t pl_timer_remaining_slots;
    bool active_timer_found = false;
    ns_timer_struct *current_timer;
    ns_timer_struct *first_timer = NULL;
    int8_t retval = -1;

    platform_enter_critical();
    /*Find timer with given timer ID*/
    current_timer = ns_timer_get_pointer_to_timer_struct(ns_timer_id);
    if (!current_timer) {
        goto exit;
    }

    retval = 0;

    /*Check if already stopped*/
    if (current_timer->timer_state == NS_TIMER_STOP) {
        goto exit;
    }

    current_timer->timer_state = NS_TIMER_STOP;
    current_timer->remaining_slots = 0;

    /*Check if some timer is already active*/
    ns_list_foreach(ns_timer_struct, current_timer, &ns_timer_list) {
        if (current_timer->timer_state == NS_TIMER_ACTIVE) {
            active_timer_found = true;
            break;
        }
    }
    /*If no active timers found, start one*/
    if (!active_timer_found) {
        pl_timer_remaining_slots = platform_timer_get_remaining_slots();
        /*Find hold-labelled timer with the least remaining slots*/
        ns_list_foreach(ns_timer_struct, current_timer, &ns_timer_list) {
            if (current_timer->timer_state == NS_TIMER_HOLD) {
                current_timer->remaining_slots += pl_timer_remaining_slots;

                if (!first_timer || current_timer->remaining_slots < first_timer->remaining_slots) {
                    first_timer = current_timer;
                }
            }
        }
        /*If hold-labelled timer found, set it active and start the HAL driver*/
        if (first_timer) {
            first_timer->timer_state = NS_TIMER_ACTIVE;
            /*Start HAL timer*/
            ns_timer_start_pl_timer(first_timer->remaining_slots);
            /*If some of the other hold-labelled timers have the same remaining slots as the timer_tmp, mark them active*/
            ns_list_foreach(ns_timer_struct, current_timer, &ns_timer_list) {
                if (current_timer->timer_state == NS_TIMER_HOLD) {
                    if (current_timer->remaining_slots == first_timer->remaining_slots) {
                        current_timer->timer_state = NS_TIMER_ACTIVE;
                    } else {
                        current_timer->remaining_slots -= first_timer->remaining_slots;
                    }
                }
            }
        }
    }

exit:
    platform_exit_critical();

    return retval;
}
#endif // NS_EXCLUDE_HIGHRES_TIMER
