// Copyright 2021 The Pigweed Authors
//
// 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
//
//     https://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 "pw_chrono/system_timer.h"

#include <algorithm>
#include <mutex>

#include "FreeRTOS.h"
#include "pw_assert/check.h"
#include "pw_chrono_freertos/system_clock_constants.h"
#include "task.h"
#include "timers.h"

namespace pw::chrono {
namespace {

using State = backend::NativeSystemTimer::State;

// Instead of adding targeted locks to each instance, simply use the global
// scheduler critical section lock.
class SchedulerLock {
 public:
  static void lock() { vTaskSuspendAll(); }
  static void unlock() { xTaskResumeAll(); }
};
SchedulerLock global_timer_lock;

void HandleTimerCallback(TimerHandle_t timer_handle) {
  // The FreeRTOS timer service is always handled by a thread, ergo to ensure
  // this API is threadsafe we simply disable task switching.

  // Because the timer control block, AKA what the timer handle points at, is
  // the first member of the NativeSystemTimer struct we play a trick to
  // cheaply get the native handle reference.
  backend::NativeSystemTimer* native_type =
      reinterpret_cast<backend::NativeSystemTimer*>(timer_handle);

  // We're only using this control structure so that we can be sure we never
  // run the callbacks while the lock_guard is in scope. We need to be sure
  // that all paths through the loop that require a manual unlock() break out of
  // the loop. Currently, that is only for callbacks, so we don't require any
  // control structures, but if more functionality is required, we may need a
  // switch statement following the loop to dispatch to the correct behavior.
  do {
    std::lock_guard lock(global_timer_lock);

    if (native_type->state == State::kCancelled) {
      // Do nothing, we were invoked while the stop command was in the queue.
      //
      // Note that xTimerIsTimerActive cannot be used here. If a timer is
      // started after it expired, it is executed immediately from the command
      // queue. Older versions of FreeRTOS failed to mark expired timers as
      // inactive before executing them in this way. So, if the timer is
      // executed in the command queue before the stop command is processed,
      // this callback will be invoked while xTimerIsTimerActive returns true.
      // This was fixed in https://github.com/FreeRTOS/FreeRTOS-Kernel/pull/305.
      return;
    }

    const SystemClock::duration time_until_deadline =
        native_type->expiry_deadline - SystemClock::now();
    if (time_until_deadline <= SystemClock::duration::zero()) {
      // We have met the deadline, cancel the current state and execute the
      // user's callback. Note we cannot update the state later as the user's
      // callback may alter the desired state through the Invoke*() API.
      native_type->state = State::kCancelled;

      // Release the scheduler lock once we won't modify native_state any
      // further.
      break;
    }

    // We haven't met the deadline yet, reschedule as far out as possible.
    // Note that this must be > SystemClock::duration::zero() based on the
    // conditional above.
    const SystemClock::duration period =
        std::min(pw::chrono::freertos::kMaxTimeout, time_until_deadline);
    PW_CHECK_UINT_EQ(
        xTimerChangePeriod(reinterpret_cast<TimerHandle_t>(&native_type->tcb),
                           static_cast<TickType_t>(period.count()),
                           0),
        pdPASS,
        "Timer command queue overflowed");
    PW_CHECK_UINT_EQ(
        xTimerStart(reinterpret_cast<TimerHandle_t>(&native_type->tcb), 0),
        pdPASS,
        "Timer command queue overflowed");
    return;
  } while (false);

  // Invoke user_callback once the lock is out of scope.
  native_type->user_callback(native_type->expiry_deadline);
}

// FreeRTOS requires a timer to have a non-zero period.
constexpr SystemClock::duration kMinTimerPeriod = SystemClock::duration(1);
constexpr TickType_t kInvalidPeriod = kMinTimerPeriod.count();
constexpr UBaseType_t kOneShotMode = pdFALSE;  // Do not use auto reload.

}  // namespace

#if configUSE_TIMERS != 1
#error \
    "Backend requires your FreeRTOS configuration to have configUSE_TIMERS == 1"
#endif

#if configSUPPORT_STATIC_ALLOCATION != 1
#error \
    "Backend requires your FreeRTOS configuration to have configSUPPORT_STATIC_ALLOCATION == 1"
#endif

SystemTimer::SystemTimer(ExpiryCallback&& callback)
    : native_type_{.tcb{},
                   .state = State::kCancelled,
                   .expiry_deadline = SystemClock::time_point(),
                   .user_callback = std::move(callback)} {
  // Note that timer "creation" is not enqueued through the command queue and
  // is ergo safe to do before the scheduler is running.
  const TimerHandle_t handle =
      xTimerCreateStatic("",  // "pw::chrono::SystemTimer",
                         kInvalidPeriod,
                         kOneShotMode,
                         this,
                         HandleTimerCallback,
                         &native_type_.tcb);

  // This should never fail since the pointer provided was not null and it
  // should return a pointer to the StaticTimer_t.
  PW_DCHECK_PTR_EQ(handle, reinterpret_cast<TimerHandle_t>(&native_type_.tcb));
}

SystemTimer::~SystemTimer() {
  Cancel();

  // WARNING: This enqueues the request to delete the timer through a queue, it
  // does not synchronously delete and disable the timer here! This means that
  // if the timer is about to expire and the timer service thread is a lower
  // priority that it may use the native_type_ after it is free'd.
  PW_CHECK_UINT_EQ(
      pdPASS,
      xTimerDelete(reinterpret_cast<TimerHandle_t>(&native_type_.tcb), 0),
      "Timer command queue overflowed");

  // In case the timer is still active as warned above, busy yield loop until it
  // has been removed. The active flag is cleared in the StaticTimer_t when the
  // delete command is processed.
  //
  // Note that this is safe before the scheduler has been started because the
  // timer cannot have been added to the queue yet and ergo it shouldn't attempt
  // to yield.
  while (
      xTimerIsTimerActive(reinterpret_cast<TimerHandle_t>(&native_type_.tcb))) {
    taskYIELD();
  }
}

void SystemTimer::InvokeAt(SystemClock::time_point timestamp) {
  // The FreeRTOS timer service is always handled by a thread, ergo to ensure
  // this API is threadsafe we simply disable task switching.
  std::lock_guard lock(global_timer_lock);

  // We don't want to call Cancel which would enqueue a stop command instead of
  // synchronously updating the state. Instead we update the expiry deadline
  // and update the state where the one shot only fires if the expiry deadline
  // is exceeded and the callback is executed once.
  native_type_.expiry_deadline = timestamp;

  // Schedule the timer as far out as possible. Note that the timeout might be
  // clamped and it may be rescheduled internally.
  const SystemClock::duration time_until_deadline =
      timestamp - SystemClock::now();
  const SystemClock::duration period = std::clamp(
      kMinTimerPeriod, time_until_deadline, pw::chrono::freertos::kMaxTimeout);

  PW_CHECK_UINT_EQ(
      xTimerChangePeriod(reinterpret_cast<TimerHandle_t>(&native_type_.tcb),
                         static_cast<TickType_t>(period.count()),
                         0),
      pdPASS,
      "Timer command queue overflowed");

  // Don't enqueue the start multiple times, schedule it once and let the
  // callback cancel.
  if (native_type_.state == State::kCancelled) {
    PW_CHECK_UINT_EQ(
        xTimerStart(reinterpret_cast<TimerHandle_t>(&native_type_.tcb), 0),
        pdPASS,
        "Timer command queue overflowed");
    native_type_.state = State::kScheduled;
  }
}

void SystemTimer::Cancel() {
  // The FreeRTOS timer service is always handled by a thread, ergo to ensure
  // this API is threadsafe we simply disable task switching.
  std::lock_guard lock(global_timer_lock);

  // The stop command may not be executed until later in case we're in a
  // critical section. For this reason update the internal state in case the
  // callback gets invoked.
  //
  // Note that xTimerIsTimerActive cannot be used here as the timer service
  // daemon may be a lower priority and ergo may still execute the callback
  // after Cancel() was invoked. This is because a single expired timer may be
  // processed before the entire command queue is emptied.
  native_type_.state = State::kCancelled;

  PW_CHECK_UINT_EQ(
      xTimerStop(reinterpret_cast<TimerHandle_t>(&native_type_.tcb), 0),
      pdPASS,
      "Timer command queue overflowed");
}

}  // namespace pw::chrono
