/*
 * Copyright 2024 Google LLC
 *
 * 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 "services/common/system_task.h"

#include "system/logging.h"

#include "drivers/task_watchdog.h"
#include "kernel/pebble_tasks.h"
#include "kernel/util/task_init.h"
#include "mcu/fpu.h"
#include "os/tick.h"
#include "services/common/regular_timer.h"
#include "system/passert.h"

#include "FreeRTOS.h"
#include "queue.h"
#include "task.h"

#define SYSTEM_TASK_PRIORITY (tskIDLE_PRIORITY + 1)

typedef struct {
  SystemTaskEventCallback cb;
  void *data;
} SystemTaskEvent;

static QueueHandle_t s_system_task_queue;
static QueueHandle_t s_from_app_system_task_queue;

static QueueSetHandle_t s_system_task_queue_set;

static SystemTaskEventCallback s_current_cb;

static bool s_system_task_idle = true;
static bool s_should_block_callbacks = false;

static bool prv_is_accepting_callbacks() {
  return s_system_task_queue != 0 && !s_should_block_callbacks;
}

static void system_task_idle_timer_callback(void* data) {
  if (s_system_task_idle && uxQueueMessagesWaiting(s_system_task_queue_set) == 0) {
    system_task_watchdog_feed();
  }
}

static void system_task_main(void* paramater) {
  task_watchdog_mask_set(PebbleTask_KernelBackground);
  task_init();

  while (true) {
    s_system_task_idle = true;

    SystemTaskEvent event;

    QueueSetMemberHandle_t activated_queue = xQueueSelectFromSet(s_system_task_queue_set, portMAX_DELAY);

    // Get event from the activated queue
    portBASE_TYPE result = xQueueReceive(activated_queue, &event, 0);

    // I believe its possible that we just reset the queue and accidently
    // pended an extra event to the queue set so handle that case gracefully
    if (result) {
      s_system_task_idle = false;
      s_current_cb = event.cb;
      event.cb(event.data);
      mcu_fpu_cleanup();
      s_current_cb = NULL;
    }

    // Refresh the watchdog immediately, just in case that cb() took awhile to run.
    system_task_watchdog_feed();
  }
}

void system_task_init(void) {
  static const int SYSTEM_TASK_QUEUE_LENGTH = 30;
  static const int FROM_APP_SYSTEM_TASK_QUEUE_LENGTH = 8;

  s_system_task_queue = xQueueCreate(SYSTEM_TASK_QUEUE_LENGTH, sizeof(SystemTaskEvent));
  s_from_app_system_task_queue = xQueueCreate(FROM_APP_SYSTEM_TASK_QUEUE_LENGTH, sizeof(SystemTaskEvent));

  s_system_task_queue_set = xQueueCreateSet(SYSTEM_TASK_QUEUE_LENGTH + FROM_APP_SYSTEM_TASK_QUEUE_LENGTH);
  xQueueAddToSet(s_system_task_queue, s_system_task_queue_set);
  xQueueAddToSet(s_from_app_system_task_queue, s_system_task_queue_set);

  extern uint32_t __kernel_bg_stack_start__[];
  extern uint32_t __kernel_bg_stack_size__[];
  extern uint32_t __stack_guard_size__[];
  const uint32_t kernel_bg_stack_words = ( (uint32_t)__kernel_bg_stack_size__
                       - (uint32_t)__stack_guard_size__) / sizeof(portSTACK_TYPE);

  TaskParameters_t task_params = {
    .pvTaskCode = system_task_main,
    .pcName = "KernelBG",
    .usStackDepth = kernel_bg_stack_words,
    .uxPriority = SYSTEM_TASK_PRIORITY | portPRIVILEGE_BIT,
    .puxStackBuffer = (void*)(uintptr_t)((uint32_t)__kernel_bg_stack_start__
                                          + (uint32_t)__stack_guard_size__)
  };

  pebble_task_create(PebbleTask_KernelBackground, &task_params, NULL);
}

void system_task_timer_init(void) {
  // Register a regular timer to kick the watchdog while we're waiting for something
  // to do. The other way to do this is to have the xQueueReceive in system_task_main timeout
  // occasionally, but that isn't necessarily second aligned and will require the watch
  // to wakeup from sleep just to kick the watchdog. This way it's kicked at the same time as
  // all the other regular tasks. Note that the system_task_idle_timer_callback only kicks
  // the watchdog if we're currently waiting for work to do on the system_task. If we're in the
  // middle of something we won't kick it.
  static RegularTimerInfo idle_watchdog_timer = {
    .cb = system_task_idle_timer_callback
  };
  regular_timer_add_seconds_callback(&idle_watchdog_timer);
}

void system_task_watchdog_feed(void) {
  task_watchdog_bit_set(PebbleTask_KernelBackground);
}

static void handle_system_task_send_failure(SystemTaskEventCallback cb) {
  register uintptr_t lr __asm("lr");
  uintptr_t saved_lr = lr;

  PBL_LOG(LOG_LEVEL_ERROR, "System task queue full. Dropped cb: %p, current cb: %p", cb, s_current_cb);

  RebootReason reason = {
    .code = RebootReasonCode_EventQueueFull,
    .event_queue = {
      .destination_task = PebbleTask_KernelBackground,
      .push_lr = (uint32_t) saved_lr,
      .current_event = (uint32_t) s_current_cb,
      .dropped_event = (uint32_t) cb
    }
  };
  reboot_reason_set(&reason);

  reset_due_to_software_failure();
}

bool system_task_add_callback_from_isr(SystemTaskEventCallback cb, void *data, bool* should_context_switch) {
  if (!prv_is_accepting_callbacks()) {
    return false;
  }
  SystemTaskEvent event = {
    .cb = cb,
    .data = data,
  };

  signed portBASE_TYPE tmp;
  bool success = (xQueueSendToBackFromISR(s_system_task_queue, &event, &tmp) == pdTRUE);
  if (!success) {
    handle_system_task_send_failure(cb);
  }

  *should_context_switch = (tmp == pdTRUE);

  return success;
}

bool system_task_add_callback(SystemTaskEventCallback cb, void *data) {
  if (!prv_is_accepting_callbacks()) {
    return false;
  }

  SystemTaskEvent event = {
    .cb = cb,
    .data = data,
  };

  if (pebble_task_get_current() == PebbleTask_App) {
    // If we're the app and we've filled up our system task, the app just gets to wait.
    // FIXME: In the future when we want to bound the amount of time a syscall can take this will have to change.
    xQueueSendToBack(s_from_app_system_task_queue, &event, portMAX_DELAY);
    return true;
  } else {
    // Back ourselves up and wait a reasonable amount of time before failing. If the queue is really backed up
    // we want to fall through to the handle_system_task_send_failure and not just get killed by the watchdog.
    bool success = (xQueueSendToBack(s_system_task_queue, &event, milliseconds_to_ticks(3000)) == pdTRUE);
    if (!success) {
      handle_system_task_send_failure(cb);
    }
  }

  return true;
}

void system_task_block_callbacks(bool block) {
  s_should_block_callbacks = block;
}

uint32_t system_task_get_available_space(void) {
  const bool is_app = pebble_task_get_current() == PebbleTask_App;
  return uxQueueSpacesAvailable(is_app ? s_from_app_system_task_queue : s_system_task_queue);
}

void* system_task_get_current_callback(void) {
  return s_current_cb;
}

void system_task_enable_raised_priority(bool is_raised) {
  const uint32_t raised_priority_level = tskIDLE_PRIORITY + 3; // Same as KernelMain / BT tasks
  vTaskPrioritySet(pebble_task_get_handle_for_task(PebbleTask_KernelBackground),
                   (is_raised ? raised_priority_level : SYSTEM_TASK_PRIORITY) | portPRIVILEGE_BIT);
}

bool system_task_is_ready_to_run(void) {
  const eTaskState bg_task_state =
        eTaskGetState(pebble_task_get_handle_for_task(PebbleTask_KernelBackground));
  // check if system task is ready to go (instead of e.g. waiting for a mutex)
  return (bg_task_state == eReady);
}
