// Copyright 2022 The IREE Authors
//
// Licensed under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception

#include "module.h"

#include <cstdio>
#include <thread>

#include "iree/modules/hal/types.h"
#include "iree/vm/native_module_cc.h"

// NOTE: this module is written in C++ using the native module wrapper and uses
// template magic to handle marshaling arguments. For a lot of uses this is a
// much friendlier way of exposing modules to the IREE VM and if performance and
// code size are not a concern is a fine route to take. Here we do it for
// brevity but all of the internal IREE modules are implemented in C.

//===----------------------------------------------------------------------===//
// VM module interface implementation
//===----------------------------------------------------------------------===//

namespace {

using namespace iree;

// Approximation of some external library call that populates a buffer.
// It's assumed that when this is called the |source_buffer| is available to
// read and the |target_buffer| is available to write (no other readers exist).
// This sample assumes that the buffers are mappable so we can do the work here
// but they will not always be. APIs like iree_hal_allocator_import_buffer and
// iree_hal_allocator_export_buffer can be used in some cases to avoid
// potentially expensive operations but real applications that care about
// performance would want to issue async transfer command buffers.
//
// Only use this as a reference for when synchronous behavior is absolutely
// required (old-style blocking file IO/etc).
static Status SyncSimulatedHostOpI32(iree_hal_buffer_t* source_buffer,
                                     iree_hal_buffer_t* target_buffer,
                                     iree_hal_dim_t count) {
  Status status = OkStatus();

  // Map the source and target buffers into host memory. Note that not all
  // devices allow this but in this sample we assume they do.
  iree_hal_buffer_mapping_t source_mapping = {{0}};
  if (status.ok()) {
    status = iree_hal_buffer_map_range(
        source_buffer, IREE_HAL_MAPPING_MODE_SCOPED,
        IREE_HAL_MEMORY_ACCESS_READ, 0, IREE_HAL_WHOLE_BUFFER, &source_mapping);
  }
  iree_hal_buffer_mapping_t target_mapping = {{0}};
  if (status.ok()) {
    status =
        iree_hal_buffer_map_range(target_buffer, IREE_HAL_MAPPING_MODE_SCOPED,
                                  IREE_HAL_MEMORY_ACCESS_DISCARD_WRITE, 0,
                                  IREE_HAL_WHOLE_BUFFER, &target_mapping);
  }

  // Sad slow host work. Whenever possible it's worth it to move these into the
  // program so the IREE compiler can fuse and accelerate these operations.
  if (status.ok()) {
    const int32_t* source_ptr =
        reinterpret_cast<const int32_t*>(source_mapping.contents.data);
    int32_t* target_ptr =
        reinterpret_cast<int32_t*>(target_mapping.contents.data);
    for (iree_host_size_t i = 0; i < count; ++i) {
      target_ptr[i] = source_ptr[i] * 2;
    }
  }

  // We must unmap the buffers before they will be usable.
  // Note that it's possible for these to fail in cases where the buffer
  // required emulated mapping but on basic host-local devices like CPU assumed
  // in this sample that should never happen.
  iree_status_ignore(iree_hal_buffer_unmap_range(&source_mapping));
  iree_status_ignore(iree_hal_buffer_unmap_range(&target_mapping));

  return status;
}

// Represents some kind of stateful async operation.
// Here we spin up a thread to wait on the wait_fence, do some expensive work,
// and then signal the signal_fence.
//
// **This is not actually how this should be done** - spinning up a thread for
// each operation is extremely wasteful and doing so will contend with the
// threads IREE uses for scheduling its compute workloads. This is pretty much
// the worst way to run asynchronous work (but at least it's async!). Instead
// think of this as an example of calling off to some service/system layer where
// the ownership of the work scheduling is not in control of the application
// (like networking or RPC).
//
// Each AsyncOp instance is used for a single operation and deletes itself when
// the operation is complete. In order to prevent hangs it's critical that the
// signal_fence is signaled or marked as failing.
//
// TODO(benvanik): demonstrate getting the iree_task_executor_t for direct use.
class AsyncOp {
 public:
  static void Launch(vm::ref<iree_hal_buffer_view_t> source_view,
                     vm::ref<iree_hal_buffer_view_t> target_view,
                     vm::ref<iree_hal_fence_t> wait_fence,
                     vm::ref<iree_hal_fence_t> signal_fence) {
    new AsyncOp(std::move(source_view), std::move(target_view),
                std::move(wait_fence), std::move(signal_fence));
  }

 private:
  AsyncOp(vm::ref<iree_hal_buffer_view_t> source_view,
          vm::ref<iree_hal_buffer_view_t> target_view,
          vm::ref<iree_hal_fence_t> wait_fence,
          vm::ref<iree_hal_fence_t> signal_fence)
      : source_view_(std::move(source_view)),
        target_view_(std::move(target_view)),
        wait_fence_(std::move(wait_fence)),
        signal_fence_(std::move(signal_fence)),
        thread_([this]() {
          thread_.detach();
          ThreadEntry();
          delete this;  // self cleanup
        }) {}

  void ThreadEntry() {
    IREE_TRACE_SET_THREAD_NAME("std-thread-worker");
    IREE_TRACE_SCOPE();

    fprintf(stdout, "ASYNC: BEFORE WAIT\n");
    fflush(stdout);

    // Give a pause to simulate doing something expensive.
    std::this_thread::sleep_for(std::chrono::milliseconds(1000));

    // Wait until the tensor is ready for use. A real application could
    // export the fence to a native wait handle they could use with syscalls
    // or add the fence to a multi-wait operation. Here we just block the
    // thread until ready. Due to the nature of ordering it's possible the
    // fence has already been signaled by the time we get here.
    Status status = iree_hal_fence_wait(
        wait_fence_.get(), iree_infinite_timeout(), IREE_ASYNC_WAIT_FLAG_NONE);

    fprintf(stdout, "ASYNC: AFTER WAIT\n");
    fflush(stdout);

    // Perform the expensive work while the input tensor is known good and
    // the output is ready to accept it.
    if (status.ok()) {
      // Hacky example accessing the source contents and producing the result
      // contents. This emulates what an external library the user is calling
      // that expects host void* buffers does.
      status = SyncSimulatedHostOpI32(
          iree_hal_buffer_view_buffer(source_view_.get()),
          iree_hal_buffer_view_buffer(target_view_.get()),
          iree_hal_buffer_view_element_count(source_view_.get()));
    }

    fprintf(stdout, "ASYNC: BEFORE SIGNAL\n");
    fflush(stdout);

    // Completion is the caller-visible teardown point. Release async-owned
    // references before signaling so device teardown cannot race this thread's
    // final buffer releases after the fence is observed.
    source_view_.reset();
    target_view_.reset();
    wait_fence_.reset();

    // Try to signal completion so that downstream consumers of the result
    // can get scheduled.
    if (status.ok()) {
      status = iree_hal_fence_signal(signal_fence_.get());
    }

    // If we failed then we propagate the failure status. This is likely to
    // result in complete failure of the invocation though when the user is
    // able to observe the failure is hard to determine as they may be
    // pipelined N invocations deep by the time this runs.
    if (!status.ok()) {
      iree_hal_fence_fail(signal_fence_.get(), status.release());
    }

    fprintf(stdout, "ASYNC: AFTER SIGNAL\n");
    fflush(stdout);
  }

  vm::ref<iree_hal_buffer_view_t> source_view_;
  vm::ref<iree_hal_buffer_view_t> target_view_;
  vm::ref<iree_hal_fence_t> wait_fence_;
  vm::ref<iree_hal_fence_t> signal_fence_;
  std::thread thread_;
};

// Per-context module state.
// This can contain "globals" and other arbitrary state.
//
// Thread-compatible; the runtime will not issue multiple calls at the same
// time using the same state. If the implementation uses external threads then
// it must synchronize itself.
class CustomModuleState final {
 public:
  explicit CustomModuleState(vm::ref<iree_hal_device_t> device,
                             iree_allocator_t host_allocator)
      : device_(std::move(device)), host_allocator_(host_allocator) {}
  ~CustomModuleState() = default;

  StatusOr<vm::ref<iree_hal_buffer_view_t>> CallAsync(
      iree_hal_buffer_view_t* arg_view, iree_hal_fence_t* wait_fence,
      iree_hal_fence_t* signal_fence) {
    // TODO(benvanik): better fence helpers when timelines are not needed.
    vm::ref<iree_hal_semaphore_t> semaphore;
    IREE_RETURN_IF_ERROR(iree_hal_semaphore_create(
        device_.get(), IREE_HAL_QUEUE_AFFINITY_ANY, 0ull,
        IREE_HAL_SEMAPHORE_FLAG_DEFAULT, &semaphore));
    vm::ref<iree_hal_fence_t> alloca_fence;
    IREE_RETURN_IF_ERROR(iree_hal_fence_create_at(
        semaphore.get(), 1ull, host_allocator_, &alloca_fence));

    // Asynchronously allocate the output memory for the call result.
    // This chains the allocation such that the wait_fence must be signaled
    // before the memory is allocated and our alloca_fence will be used to
    // sequence our work with the allocation:
    //
    // [wait_fence] -> alloca -> [alloca_fence] -> work -> [signal_fence]
    //
    // TODO(benvanik): extend to allowing result storage to be passed in (when
    // possible to compute sizes). For now all results need to be allocated.
    iree_hal_buffer_params_t buffer_params = {
        /*.usage=*/IREE_HAL_BUFFER_USAGE_DEFAULT |
            IREE_HAL_BUFFER_USAGE_MAPPING,
        /*.access=*/IREE_HAL_MEMORY_ACCESS_ALL,
        /*.type=*/IREE_HAL_MEMORY_TYPE_OPTIMAL_FOR_DEVICE |
            IREE_HAL_MEMORY_TYPE_HOST_VISIBLE,
        /*.queue_affinity=*/IREE_HAL_QUEUE_AFFINITY_ANY,
        /*.min_alignment=*/64,
    };
    vm::ref<iree_hal_buffer_t> result_buffer;
    IREE_RETURN_IF_ERROR(iree_hal_device_queue_alloca(
        device_.get(), IREE_HAL_QUEUE_AFFINITY_ANY,
        iree_hal_fence_semaphore_list(wait_fence),
        iree_hal_fence_semaphore_list(alloca_fence.get()),
        /*pool=*/NULL, buffer_params,
        iree_hal_buffer_view_byte_length(arg_view), IREE_HAL_ALLOCA_FLAG_NONE,
        &result_buffer));

    // Wrap the buffer in a buffer view that provides the metadata for
    // runtime verification.
    vm::ref<iree_hal_buffer_view_t> result_view;
    IREE_RETURN_IF_ERROR(iree_hal_buffer_view_create_like(
        result_buffer.get(), arg_view, host_allocator_, &result_view));

    // Launch the stateful async operation.
    // See the notes above - note that this is _not_ a good way of doing this!
    // Note that we should be using host_allocator_ here to create these objects
    // so that memory is properly tracked as originating from this call.
    // We retain the borrowed pointers as AsyncOp takes ownership for async use.
    AsyncOp::Launch(vm::retain_ref(arg_view), vm::retain_ref(result_view),
                    std::move(alloca_fence), vm::retain_ref(signal_fence));

    // Note that the caller needs the buffer view back but is not allowed to
    // access its contents until we signal the signal_fence.
    return result_view;
  }

 private:
  // HAL device used for scheduling work and allocations.
  vm::ref<iree_hal_device_t> device_;

  // Allocator that the caller requested we use for any allocations we need to
  // perform during operation.
  iree_allocator_t host_allocator_;
};

// Function table mapping imported function names to their implementation.
static const vm::NativeFunction<CustomModuleState> kCustomModuleFunctions[] = {
    vm::MakeNativeFunction("call.async", &CustomModuleState::CallAsync),
};

// The module instance that will be allocated and reused across contexts.
// Any context-specific state must be stored in a state structure such as
// CustomModuleState.
//
// Assumed thread-safe (by construction here, as it's immutable), though if any
// mutable state is stored here it will need to be synchronized by the
// implementation.
class CustomModule final : public vm::NativeModule<CustomModuleState> {
 public:
  using vm::NativeModule<CustomModuleState>::NativeModule;

  void SetDevice(vm::ref<iree_hal_device_t> device) {
    device_ = std::move(device);
  }

  // Creates per-context state when the module is added to a new context.
  // May be called from any thread.
  StatusOr<std::unique_ptr<CustomModuleState>> CreateState(
      iree_allocator_t host_allocator) override {
    auto state = std::make_unique<CustomModuleState>(vm::retain_ref(device_),
                                                     host_allocator);
    return state;
  }

  // Forks a parent state into a child state, preserving any module state
  // by-reference.
  StatusOr<std::unique_ptr<CustomModuleState>> ForkState(
      CustomModuleState* parent_state,
      iree_allocator_t host_allocator) override {
    // No special state to preserve; the device is the same for all states
    // created from this module.
    return CreateState(host_allocator);
  }

 private:
  vm::ref<iree_hal_device_t> device_;
};

}  // namespace

// Note that while we are using C++ bindings internally we still expose the
// module as a C instance. This hides the details of our implementation.
extern "C" iree_status_t iree_custom_module_async_create(
    iree_vm_instance_t* instance, iree_hal_device_t* device,
    iree_allocator_t host_allocator, iree_vm_module_t** out_module) {
  IREE_ASSERT_ARGUMENT(out_module);
  *out_module = NULL;

  // NOTE: this isn't using the allocator here and that's bad as it leaves
  // untracked allocations and pulls in the system allocator that may differ
  // from the one requested by the user.
  // TODO(benvanik): std::allocator wrapper around iree_allocator_t so this can
  // use that instead.
  auto module = std::make_unique<CustomModule>(
      "custom", /*version=*/0, instance, host_allocator,
      iree::span<const vm::NativeFunction<CustomModuleState>>(
          kCustomModuleFunctions));
  module->SetDevice(vm::retain_ref(device));

  *out_module = module.release()->interface();
  return iree_ok_status();
}
