// Copyright 2024 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_async_fuchsia/fake_dispatcher.h"

#include "pw_async_fuchsia/util.h"

namespace pw::async::test::backend {

NativeFakeDispatcher::NativeFakeDispatcher(Dispatcher& test_dispatcher)
    : dispatcher_(test_dispatcher) {}

bool NativeFakeDispatcher::DestroyLoop() { return fake_loop_.Shutdown(); }

chrono::SystemClock::time_point NativeFakeDispatcher::now() {
  return fake_loop_.Now();
}

void NativeFakeDispatcher::Post(Task& task) { PostAt(task, now()); }

void NativeFakeDispatcher::PostAfter(Task& task,
                                     chrono::SystemClock::duration delay) {
  PostAt(task, now() + delay);
}

void NativeFakeDispatcher::PostAt(Task& task,
                                  chrono::SystemClock::time_point time) {
  // TODO: https://fxbug.dev/42075952 - Return errors once these methods return
  // a Status.
  if (!fake_loop_.Runnable()) {
    Context ctx{.dispatcher = &dispatcher_, .task = &task};
    task(ctx, Status::Cancelled());
    return;
  }
  ::pw::async::backend::NativeTask& native_task = task.native_type();
  native_task.set_due_time(time);
  native_task.dispatcher_ = &dispatcher_;
  fake_loop_.PostTask(&native_task);
}

bool NativeFakeDispatcher::Cancel(Task& task) {
  return fake_loop_.Runnable() &&
         fake_loop_.CancelTask(&task.native_type()) == ZX_OK;
}

bool NativeFakeDispatcher::RunUntilIdle() {
  if (stop_requested_) {
    return DestroyLoop();
  }
  return fake_loop_.RunUntilIdle();
}

bool NativeFakeDispatcher::RunUntil(chrono::SystemClock::time_point end_time) {
  if (stop_requested_) {
    return DestroyLoop();
  }
  return fake_loop_.Run(pw::async_fuchsia::TimepointToZxTime(end_time).get(),
                        false);
}

bool NativeFakeDispatcher::RunFor(chrono::SystemClock::duration duration) {
  return RunUntil(now() + duration);
}

NativeFakeDispatcher::FakeAsyncLoop::FakeAsyncLoop() {
  list_initialize(&task_list_);
  list_initialize(&due_list_);
}

chrono::SystemClock::time_point NativeFakeDispatcher::FakeAsyncLoop::Now()
    const {
  return pw::async_fuchsia::ZxTimeToTimepoint(zx::time{now_});
}

zx_status_t NativeFakeDispatcher::FakeAsyncLoop::PostTask(async_task_t* task) {
  if (state_ == ASYNC_LOOP_SHUTDOWN) {
    return ZX_ERR_BAD_STATE;
  }

  InsertTask(task);
  if (!dispatching_tasks_ && TaskToNode(task)->prev == &task_list_) {
    // Task inserted at head.  Earliest deadline changed.
    RestartTimer();
  }

  return ZX_OK;
}

zx_status_t NativeFakeDispatcher::FakeAsyncLoop::CancelTask(
    async_task_t* task) {
  // Note: We need to process cancellations even while the loop is being
  // destroyed in case the client is counting on the handler not being
  // invoked again past this point.  Also, the task we're removing here
  // might be present in the dispatcher's |due_list| if it is pending
  // dispatch instead of in the loop's |task_list| as usual.  The same
  // logic works in both cases.

  list_node_t* node = TaskToNode(task);
  if (!list_in_list(node)) {
    return ZX_ERR_NOT_FOUND;
  }

  // Determine whether the head task was canceled and following task has
  // a later deadline.  If so, we will bump the timer along to that deadline.
  bool must_restart = !dispatching_tasks_ && node->prev == &task_list_ &&
                      (node->next == &task_list_ ||
                       NodeToTask(node->next)->deadline > task->deadline);
  list_delete(node);
  if (must_restart) {
    RestartTimer();
  }

  return ZX_OK;
}

bool NativeFakeDispatcher::FakeAsyncLoop::RunUntilIdle() {
  return Run(now_, false);
}

bool NativeFakeDispatcher::FakeAsyncLoop::Run(zx_time_t deadline, bool once) {
  zx_status_t status;
  bool task_invoked = false;
  do {
    status = RunOnce(deadline, &task_invoked);
  } while (status == ZX_OK && !once);
  return task_invoked;
}

void NativeFakeDispatcher::FakeAsyncLoop::InsertTask(async_task_t* task) {
  list_node_t* node;
  for (node = task_list_.prev; node != &task_list_; node = node->prev) {
    if (task->deadline >= NodeToTask(node)->deadline) {
      break;
    }
  }
  list_add_after(node, TaskToNode(task));
}

void NativeFakeDispatcher::FakeAsyncLoop::RestartTimer() {
  zx_time_t deadline = NextDeadline();

  if (deadline == ZX_TIME_INFINITE) {
    // Nothing is left on the queue to fire.
    if (timer_armed_) {
      next_timer_expiration_ =
          ZX_TIME_INFINITE;  // Simulate timer cancellation.
      timer_armed_ = false;
    }
    return;
  }

  next_timer_expiration_ = deadline;

  if (!timer_armed_) {
    timer_armed_ = true;
  }
}

zx_time_t NativeFakeDispatcher::FakeAsyncLoop::NextDeadline() {
  if (list_is_empty(&due_list_)) {
    list_node_t* head = list_peek_head(&task_list_);
    if (!head) {
      return ZX_TIME_INFINITE;
    }
    async_task_t* task = NodeToTask(head);
    return task->deadline;
  }
  // Fire now.
  return 0ULL;
}

zx_status_t NativeFakeDispatcher::FakeAsyncLoop::RunOnce(zx_time_t deadline,
                                                         bool* task_invoked) {
  if (state_ == ASYNC_LOOP_SHUTDOWN) {
    return ZX_ERR_BAD_STATE;
  }
  if (state_ != ASYNC_LOOP_RUNNABLE) {
    return ZX_ERR_CANCELED;
  }

  // Simulate timeout of zx_port_wait() syscall.
  if (deadline < next_timer_expiration_) {
    now_ = deadline;
    return ZX_ERR_TIMED_OUT;
  }
  // Otherwise, a timer would have expired at or before `deadline`.
  now_ = next_timer_expiration_;
  next_timer_expiration_ = ZX_TIME_INFINITE;
  *task_invoked |= DispatchTasks();
  return ZX_OK;
}

bool NativeFakeDispatcher::FakeAsyncLoop::DispatchTasks() {
  bool task_invoked = false;
  // Dequeue and dispatch one task at a time in case an earlier task wants
  // to cancel a later task which has also come due. Timer restarts are
  // suppressed until we run out of tasks to dispatch.
  if (!dispatching_tasks_) {
    dispatching_tasks_ = true;

    // Extract all of the tasks that are due into |due_list| for dispatch
    // unless we already have some waiting from a previous iteration which
    // we would like to process in order.
    list_node_t* node;
    if (list_is_empty(&due_list_)) {
      zx_time_t due_time = now_;
      list_node_t* tail = nullptr;
      list_for_every(&task_list_, node) {
        if (NodeToTask(node)->deadline > due_time) {
          break;
        }
        tail = node;
      }
      if (tail) {
        list_node_t* head = task_list_.next;
        task_list_.next = tail->next;
        tail->next->prev = &task_list_;
        due_list_.next = head;
        head->prev = &due_list_;
        due_list_.prev = tail;
        tail->next = &due_list_;
      }
    }

    // Dispatch all due tasks.
    while ((node = list_remove_head(&due_list_))) {
      // Invoke the handler.  Note that it might destroy itself.
      async_task_t* task = NodeToTask(node);

      task->handler(nullptr, task, ZX_OK);
      task_invoked = true;

      if (state_ != ASYNC_LOOP_RUNNABLE) {
        break;
      }
    }

    dispatching_tasks_ = false;
    timer_armed_ = false;
    RestartTimer();
  }

  return task_invoked;
}

bool NativeFakeDispatcher::FakeAsyncLoop::Shutdown() {
  if (state_ == ASYNC_LOOP_SHUTDOWN) {
    return false;
  }
  state_ = ASYNC_LOOP_SHUTDOWN;

  // Cancel any remaining pending tasks on our queues.
  return CancelAll();
}

bool NativeFakeDispatcher::FakeAsyncLoop::CancelAll() {
  ZX_DEBUG_ASSERT(state_ == ASYNC_LOOP_SHUTDOWN);
  bool task_invoked = false;

  list_node_t* node;

  while ((node = list_remove_head(&due_list_))) {
    async_task_t* task = NodeToTask(node);
    task->handler(nullptr, task, ZX_ERR_CANCELED);
    task_invoked = true;
  }
  while ((node = list_remove_head(&task_list_))) {
    async_task_t* task = NodeToTask(node);
    task->handler(nullptr, task, ZX_ERR_CANCELED);
    task_invoked = true;
  }
  return task_invoked;
}

}  // namespace pw::async::test::backend
