/* mbed Microcontroller Library
 * Copyright (c) 2018 ARM Limited
 *
 * 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 "hal/LowPowerTickerWrapper.h"
#include "platform/Callback.h"

LowPowerTickerWrapper::LowPowerTickerWrapper(const ticker_data_t *data, const ticker_interface_t *interface, uint32_t min_cycles_between_writes, uint32_t min_cycles_until_match)
    : _intf(data->interface), _min_count_between_writes(min_cycles_between_writes + 1), _min_count_until_match(min_cycles_until_match + 1), _suspended(false)
{
    core_util_critical_section_enter();

    this->data.interface = interface;
    this->data.queue = data->queue;
    _reset();

    core_util_critical_section_exit();
}

void LowPowerTickerWrapper::irq_handler(ticker_irq_handler_type handler)
{
    core_util_critical_section_enter();

    if (_pending_fire_now || _match_check(_intf->read()) || _suspended) {
        _timeout.detach();
        _pending_timeout = false;
        _pending_match = false;
        _pending_fire_now = false;
        if (handler) {
            handler(&data);
        }
    } else {
        // Spurious interrupt
        _intf->clear_interrupt();
    }

    core_util_critical_section_exit();
}

void LowPowerTickerWrapper::suspend()
{
    core_util_critical_section_enter();

    // Wait until rescheduling is allowed
    while (!_set_interrupt_allowed) {
        timestamp_t current = _intf->read();
        if (((current - _last_actual_set_interrupt) & _mask) >= _min_count_between_writes) {
            _set_interrupt_allowed  = true;
        }
    }

    _reset();
    _suspended = true;

    core_util_critical_section_exit();
}

void LowPowerTickerWrapper::resume()
{
    core_util_critical_section_enter();

    // Wait until rescheduling is allowed
    while (!_set_interrupt_allowed) {
        timestamp_t current = _intf->read();
        if (((current - _last_actual_set_interrupt) & _mask) >= _min_count_between_writes) {
            _set_interrupt_allowed  = true;
        }
    }

    _suspended = false;

    core_util_critical_section_exit();
}

bool LowPowerTickerWrapper::timeout_pending()
{
    core_util_critical_section_enter();

    bool pending = _pending_timeout;

    core_util_critical_section_exit();
    return pending;
}

void LowPowerTickerWrapper::init()
{
    core_util_critical_section_enter();

    _reset();
    _intf->init();

    core_util_critical_section_exit();
}

void LowPowerTickerWrapper::free()
{
    core_util_critical_section_enter();

    _reset();
    _intf->free();

    core_util_critical_section_exit();
}

uint32_t LowPowerTickerWrapper::read()
{
    core_util_critical_section_enter();

    timestamp_t current = _intf->read();
    if (!_suspended && _match_check(current)) {
        _intf->fire_interrupt();
    }

    core_util_critical_section_exit();
    return current;
}

void LowPowerTickerWrapper::set_interrupt(timestamp_t timestamp)
{
    core_util_critical_section_enter();

    _last_set_interrupt = _intf->read();
    _cur_match_time = timestamp;
    _pending_match = true;
    if (!_suspended) {
        _schedule_match(_last_set_interrupt);
    } else {
        _intf->set_interrupt(timestamp);
        _last_actual_set_interrupt = _last_set_interrupt;
        _set_interrupt_allowed = false;
    }

    core_util_critical_section_exit();
}

void LowPowerTickerWrapper::disable_interrupt()
{
    core_util_critical_section_enter();

    _intf->disable_interrupt();

    core_util_critical_section_exit();
}

void LowPowerTickerWrapper::clear_interrupt()
{
    core_util_critical_section_enter();

    _intf->clear_interrupt();

    core_util_critical_section_exit();
}

void LowPowerTickerWrapper::fire_interrupt()
{
    core_util_critical_section_enter();

    _pending_fire_now = 1;
    _intf->fire_interrupt();

    core_util_critical_section_exit();
}

const ticker_info_t *LowPowerTickerWrapper::get_info()
{

    core_util_critical_section_enter();

    const ticker_info_t *info = _intf->get_info();

    core_util_critical_section_exit();
    return info;
}

void LowPowerTickerWrapper::_reset()
{
    MBED_ASSERT(core_util_in_critical_section());

    _timeout.detach();
    _pending_timeout = false;
    _pending_match = false;
    _pending_fire_now = false;
    _set_interrupt_allowed = true;
    _cur_match_time = 0;
    _last_set_interrupt = 0;
    _last_actual_set_interrupt = 0;

    const ticker_info_t *info = _intf->get_info();
    if (info->bits >= 32) {
        _mask = 0xffffffff;
    } else {
        _mask = ((uint64_t)1 << info->bits) - 1;
    }

    // Round us_per_tick up
    _us_per_tick = (1000000 + info->frequency - 1) / info->frequency;
}

void LowPowerTickerWrapper::_timeout_handler()
{
    core_util_critical_section_enter();
    _pending_timeout = false;

    timestamp_t current = _intf->read();
    /* Add extra check for '_last_set_interrupt == _cur_match_time'
     * 
     * When '_last_set_interrupt == _cur_match_time', _ticker_match_interval_passed sees it as
     * one-round interval rather than just-pass, so add extra check for it. In rare cases, we
     * may trap in _timeout_handler/_schedule_match loop. This check can break it.
     */
    if ((_last_set_interrupt == _cur_match_time) ||
        _ticker_match_interval_passed(_last_set_interrupt, current, _cur_match_time)) {
        _intf->fire_interrupt();
    } else {
        _schedule_match(current);
    }

    core_util_critical_section_exit();
}

bool LowPowerTickerWrapper::_match_check(timestamp_t current)
{
    MBED_ASSERT(core_util_in_critical_section());

    if (!_pending_match) {
        return false;
    }
    /* Add extra check for '_last_set_interrupt == _cur_match_time' as above */
    return (_last_set_interrupt == _cur_match_time) ||
        _ticker_match_interval_passed(_last_set_interrupt, current, _cur_match_time);
}

uint32_t LowPowerTickerWrapper::_lp_ticks_to_us(uint32_t ticks)
{
    MBED_ASSERT(core_util_in_critical_section());

    // Add 4 microseconds to round up the micro second ticker time (which has a frequency of at least 250KHz - 4us period)
    return _us_per_tick * ticks + 4;
}

void LowPowerTickerWrapper::_schedule_match(timestamp_t current)
{
    MBED_ASSERT(core_util_in_critical_section());

    // Check if _intf->set_interrupt is allowed
    if (!_set_interrupt_allowed) {
        if (((current - _last_actual_set_interrupt) & _mask) >= _min_count_between_writes) {
            _set_interrupt_allowed  = true;
        }
    }

    uint32_t cycles_until_match = (_cur_match_time - current) & _mask;
    bool too_close = cycles_until_match < _min_count_until_match;

    if (!_set_interrupt_allowed) {

        // Can't use _intf->set_interrupt so use microsecond Timeout instead

        // Speed optimization - if a timer has already been scheduled
        // then don't schedule it again.
        if (!_pending_timeout) {
            uint32_t ticks = cycles_until_match < _min_count_until_match ? cycles_until_match : _min_count_until_match;
            _timeout.attach_us(mbed::callback(this, &LowPowerTickerWrapper::_timeout_handler), _lp_ticks_to_us(ticks));
            _pending_timeout = true;
        }
        return;
    }

    if (!too_close) {

        // Schedule LP ticker
        _intf->set_interrupt(_cur_match_time);
        current = _intf->read();
        _last_actual_set_interrupt = current;
        _set_interrupt_allowed = false;

        // Check for overflow
        uint32_t new_cycles_until_match = (_cur_match_time - current) & _mask;
        if (new_cycles_until_match > cycles_until_match) {
            // Overflow so fire now
            _intf->fire_interrupt();
            return;
        }

        // Update variables with new time
        cycles_until_match = new_cycles_until_match;
        too_close = cycles_until_match < _min_count_until_match;
    }

    if (too_close) {

        // Low power ticker incremented to less than _min_count_until_match
        // so low power ticker may not fire. Use Timeout to ensure it does fire.
        uint32_t ticks = cycles_until_match < _min_count_until_match ? cycles_until_match : _min_count_until_match;
        _timeout.attach_us(mbed::callback(this, &LowPowerTickerWrapper::_timeout_handler), _lp_ticks_to_us(ticks));
        _pending_timeout = true;
        return;
    }
}
