/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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 <stdint.h>
#include <errno.h>
#include <stdbool.h>
#include "os/mynewt.h"
#include <hal/hal_timer.h>
#include <mcu/plic.h>
#include <mcu/fe310_hal.h>
#include <env/freedom-e300-hifive1/platform.h>

#define FE310_HAL_TIMER_MAX     (3)

struct fe310_hal_tmr {
    void *pwm_regs;         /* Pointer to timer registers */
    uint32_t value;         /* Accumulated timer value, incremented on CMP0 */
    uint16_t pwms;          /* Value of register pwms taken when value was set */
    uint8_t max_scale;      /* Max value for pwmcfg.pwmscale 7 (for PMW0) or 15 (for PWM1/2) */
    uint8_t pwmxcmp0_int;   /* PWMxCMP0 interrupt number */
    TAILQ_HEAD(hal_timer_qhead, hal_timer) sht_timers;
};

#if MYNEWT_VAL(TIMER_0)
struct fe310_hal_tmr fe310_pwm2 = {
    .pwm_regs = (uint32_t *)PWM2_CTRL_ADDR,
    .max_scale = 15,
    .pwmxcmp0_int = INT_PWM2_BASE,
};
#endif
#if MYNEWT_VAL(TIMER_1)
struct fe310_hal_tmr fe310_pwm1 = {
    .pwm_regs = (uint32_t *)PWM1_CTRL_ADDR,
    .max_scale = 15,
    .pwmxcmp0_int = INT_PWM1_BASE,
};
#endif
#if MYNEWT_VAL(TIMER_2)
struct fe310_hal_tmr fe310_pwm0 = {
    .pwm_regs = (uint32_t *)PWM0_CTRL_ADDR,
    .max_scale = 7,
    .pwmxcmp0_int = INT_PWM0_BASE,
};
#endif

static struct fe310_hal_tmr *fe310_tmr_devs[FE310_HAL_TIMER_MAX] = {
#if MYNEWT_VAL(TIMER_0)
    &fe310_pwm2,
#else
    NULL,
#endif
#if MYNEWT_VAL(TIMER_1)
    &fe310_pwm1,
#else
    NULL,
#endif
#if MYNEWT_VAL(TIMER_2)
    &fe310_pwm0,
#else
    NULL,
#endif
};

static uint8_t pwm_to_timer[FE310_HAL_TIMER_MAX];

static uint32_t
hal_timer_cnt(struct fe310_hal_tmr *tmr)
{
    uint32_t cnt;
    int sr;
    uint32_t regs = (uint32_t) tmr->pwm_regs;

    __HAL_DISABLE_INTERRUPTS(sr);
    tmr->pwms = _REG32(regs, PWM_S);
    cnt = tmr->pwms + tmr->value;
    /* Check if just overflowed */
    if (_REG32(regs, PWM_CFG) & PWM_CMP0) {
        cnt += _REG32(regs, PWM_CMP0) + 1;
    }
    __HAL_ENABLE_INTERRUPTS(sr);

    return cnt;
}

static void
fe310_tmr_check_first(struct fe310_hal_tmr *tmr)
{
    struct hal_timer *ht;
    uint32_t cnt;
    int32_t ticks;

    ht = TAILQ_FIRST(&tmr->sht_timers);
    if (ht) {
        cnt = hal_timer_cnt(tmr);
        ticks = (int32_t)(ht->expiry - cnt);
        /*
         * Setup CMP1 only when it would need to fire before CMP0.
         */
        if (tmr->pwms + ticks < _REG32(tmr->pwm_regs, PWM_CMP0)) {
            _REG32(tmr->pwm_regs, PWM_CMP1) = tmr->pwms + ticks;
            plic_enable_interrupt(tmr->pwmxcmp0_int + 1);
            return;
        }
    }
    _REG32(tmr->pwm_regs, PWM_CMP1) = _REG32(tmr->pwm_regs, PWM_CMP0);
     /* Disable PWMxCMP1 interrupt, leaving only CMP0 which is used all the time */
    plic_disable_interrupt(tmr->pwmxcmp0_int + 1);
}

/*
 * Call expired timer callbacks
 */
static void
fe310_tmr_cbs(struct fe310_hal_tmr *tmr)
{
    uint32_t cnt;
    struct hal_timer *ht;

    while ((ht = TAILQ_FIRST(&tmr->sht_timers)) != NULL) {
        cnt = hal_timer_cnt(tmr);
        if (((int32_t)(cnt - ht->expiry)) >= 0) {
            TAILQ_REMOVE(&tmr->sht_timers, ht, link);
            ht->link.tqe_prev = NULL;
            ht->cb_func(ht->cb_arg);
        } else {
            break;
        }
    }
    fe310_tmr_check_first(tmr);
}

void
fe310_pwm_cmp0_handler(int num)
{
    int pwm_num = (num - INT_PWM0_BASE) >> 2;
    int timer_num = pwm_to_timer[pwm_num];
    struct fe310_hal_tmr *tmr = fe310_tmr_devs[timer_num];
    /* Clear interrupt flag CMP0IP */
    _REG32(tmr->pwm_regs, PWM_CFG) &= ~PWM_CFG_CMP0IP;
    tmr->value += _REG32(tmr->pwm_regs, PWM_CMP0) + 1;
    fe310_tmr_cbs(tmr);
}

void
fe310_pwm_cmp1_handler(int num)
{
    int pwm_num = (num - INT_PWM0_BASE) >> 2;
    int timer_num = pwm_to_timer[pwm_num];
    struct fe310_hal_tmr *tmr = fe310_tmr_devs[timer_num];
    /* Clear interrupt flag CMP1IP */
    _REG32(tmr->pwm_regs, PWM_CMP1) = _REG32(tmr->pwm_regs, PWM_CMP0);
    _REG32(tmr->pwm_regs, PWM_CFG) &= ~PWM_CFG_CMP1IP;
    fe310_tmr_cbs(tmr);
}

/**
 * hal timer init
 *
 * Initialize platform specific timer items
 *
 * @param timer_num     Timer number to initialize
 * @param cfg           Pointer to platform specific configuration
 *
 * @return int          0: success; error code otherwise
 */
int
hal_timer_init(int timer_num, void *cfg)
{
    (void)cfg;

    if (timer_num >= FE310_HAL_TIMER_MAX || (fe310_tmr_devs[timer_num] == NULL)) {
        return -1;
    }

    return 0;
}

/**
 * hal timer config
 *
 * Configure a timer to run at the desired frequency. This starts the timer.
 *
 * @param timer_num
 * @param freq_hz
 *
 * @return int
 */
int
hal_timer_config(int timer_num, uint32_t freq_hz)
{
    struct fe310_hal_tmr *tmr;
    uint32_t cpu_freq;
    uint32_t div;
    int scale = -1;

    if (timer_num >= FE310_HAL_TIMER_MAX || !(tmr = fe310_tmr_devs[timer_num])) {
        return -1;
    }

    cpu_freq = get_cpu_freq();
    div = cpu_freq / freq_hz;

    if (div > (1 << tmr->max_scale) || div == 0) {
        return -1;
    }

    div += div >> 1;
    while (div) {
        div >>= 1;
        scale++;
    }

    _REG32(tmr->pwm_regs, PWM_CFG) = 0;
    _REG32(tmr->pwm_regs, PWM_COUNT) = 0;
    /* 15 -> 0xFFFF, 7 -> 0xFF*/
    _REG32(tmr->pwm_regs, PWM_CMP0) = (1 << (1 + tmr->max_scale)) - 1;
    _REG32(tmr->pwm_regs, PWM_CMP1) = _REG32(tmr->pwm_regs, PWM_CMP0);
    pwm_to_timer[(tmr->pwmxcmp0_int - INT_PWM0_BASE) >> 2] = (uint8_t) timer_num;
    plic_set_handler(tmr->pwmxcmp0_int, fe310_pwm_cmp0_handler, 3);
    plic_set_handler(tmr->pwmxcmp0_int + 1, fe310_pwm_cmp1_handler, 3);
    _REG32(tmr->pwm_regs, PWM_CFG) = PWM_CFG_ZEROCMP |
                                     PWM_CFG_ENALWAYS | scale;
    plic_enable_interrupt(tmr->pwmxcmp0_int);

    return 0;
}

/**
 * hal timer deinit
 *
 * De-initialize a HW timer.
 *
 * @param timer_num
 *
 * @return int
 */
int
hal_timer_deinit(int timer_num)
{
    struct fe310_hal_tmr *tmr;
    int sr;

    if (timer_num >= FE310_HAL_TIMER_MAX || !(tmr = fe310_tmr_devs[timer_num])) {
        return -1;
    }

    __HAL_DISABLE_INTERRUPTS(sr);
    _REG32(tmr->pwm_regs, PWM_CFG) = 0;
    plic_disable_interrupt(tmr->pwmxcmp0_int);
    plic_disable_interrupt(tmr->pwmxcmp0_int + 1);
    __HAL_ENABLE_INTERRUPTS(sr);

    return 0;
}

/**
 * hal timer get resolution
 *
 * Get the resolution of the timer. This is the timer period, in nanoseconds
 *
 * @param timer_num
 *
 * @return uint32_t The
 */
uint32_t
hal_timer_get_resolution(int timer_num)
{
    struct fe310_hal_tmr *tmr;
    uint32_t cpu_freq;

    if (timer_num >= FE310_HAL_TIMER_MAX || !(tmr = fe310_tmr_devs[timer_num])) {
        return -1;
    }

    cpu_freq = get_cpu_freq();

    return 1000000000 / (cpu_freq >> (_REG32(tmr->pwm_regs, PWM_CFG) & PWM_CFG_SCALE));
}

/**
 * hal timer read
 *
 * Returns the timer counter.
 *
 * @return uint32_t The timer counter register.
 */
uint32_t
hal_timer_read(int timer_num)
{
    struct fe310_hal_tmr *tmr;

    if (timer_num >= FE310_HAL_TIMER_MAX || !(tmr = fe310_tmr_devs[timer_num])) {
        return -1;
    }

    return hal_timer_cnt(tmr);
}

/**
 * hal timer delay
 *
 * Blocking delay for n ticks
 *
 * @param timer_num
 * @param ticks
 *
 * @return int 0 on success; error code otherwise.
 */
int
hal_timer_delay(int timer_num, uint32_t ticks)
{
    struct fe310_hal_tmr *tmr;
    uint32_t until;

    if (timer_num >= FE310_HAL_TIMER_MAX || !(tmr = fe310_tmr_devs[timer_num])) {
        return -1;
    }

    until = hal_timer_cnt(tmr) + ticks;
    while ((int32_t)(hal_timer_cnt(tmr) - until) <= 0) {
        /* Loop here till finished */
    }

    return 0;
}

/**
 *
 * Initialize the HAL timer structure with the callback and the callback
 * argument. Also initializes the HW specific timer pointer.
 *
 * @param cb_func
 *
 * @return int
 */
int
hal_timer_set_cb(int timer_num, struct hal_timer *timer, hal_timer_cb cb_func,
                 void *arg)
{
    struct fe310_hal_tmr *tmr;

    if (timer_num >= FE310_HAL_TIMER_MAX || !(tmr = fe310_tmr_devs[timer_num])) {
        return -1;
    }

    timer->cb_func = cb_func;
    timer->cb_arg = arg;
    timer->link.tqe_prev = NULL;
    timer->bsp_timer = tmr;

    return 0;
}

int
hal_timer_start(struct hal_timer *timer, uint32_t ticks)
{
    struct fe310_hal_tmr *tmr;
    uint32_t tick;

    tmr = (struct fe310_hal_tmr *)timer->bsp_timer;

    tick = ticks + hal_timer_cnt(tmr);
    return hal_timer_start_at(timer, tick);
}

int
hal_timer_start_at(struct hal_timer *timer, uint32_t tick)
{
    struct fe310_hal_tmr *tmr;
    struct hal_timer *ht;
    int sr;
    bool first = true;

    tmr = (struct fe310_hal_tmr *)timer->bsp_timer;

    timer->expiry = tick;

    __HAL_DISABLE_INTERRUPTS(sr);

    if (TAILQ_EMPTY(&tmr->sht_timers)) {
        TAILQ_INSERT_HEAD(&tmr->sht_timers, timer, link);
    } else {
        TAILQ_FOREACH(ht, &tmr->sht_timers, link) {
            if ((int32_t)(timer->expiry - ht->expiry) < 0) {
                TAILQ_INSERT_BEFORE(ht, timer, link);
                break;
            }
            first = false;
        }
        if (!ht) {
            TAILQ_INSERT_TAIL(&tmr->sht_timers, timer, link);
        }
    }

    if (first) {
        fe310_tmr_check_first(tmr);
    }

    __HAL_ENABLE_INTERRUPTS(sr);

    return 0;
}

/**
 * hal timer stop
 *
 * Stop a timer.
 *
 * @param timer
 *
 * @return int
 */
int
hal_timer_stop(struct hal_timer *timer)
{
    struct fe310_hal_tmr *tmr;
    int sr;
    bool recalc = false;

    __HAL_DISABLE_INTERRUPTS(sr);

    tmr = (struct fe310_hal_tmr *)timer->bsp_timer;
    /* Is it still in the list ? */
    if (timer->link.tqe_prev != NULL) {
        /* Recalculation only if removing first timer */
        recalc = (timer == TAILQ_FIRST(&tmr->sht_timers));
        TAILQ_REMOVE(&tmr->sht_timers, timer, link);
        timer->link.tqe_prev = NULL;

        if (recalc) {
            fe310_tmr_check_first(tmr);
        }
    }
    __HAL_ENABLE_INTERRUPTS(sr);

    return 0;
}
