// Copyright 2017 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include <fidl/fuchsia.kernel/cpp/fidl.h>
#include <lib/component/incoming/cpp/protocol.h>
#include <lib/zx/resource.h>
#include <lib/zx/thread.h>
#include <lib/zx/vmar.h>
#include <lib/zx/vmo.h>

#include <optional>

#include "src/base/bits.h"
#include "src/base/macros.h"
#include "src/base/platform/platform-posix-time.h"
#include "src/base/platform/platform-posix.h"
#include "src/base/platform/platform.h"

namespace v8 {
namespace base {

namespace {

static zx_handle_t g_vmex_resource = ZX_HANDLE_INVALID;

static void* g_root_vmar_base = nullptr;

// If VmexResource is unavailable or does not return a valid handle then
// this will be observed as failures in vmo_replace_as_executable() calls.
void SetVmexResource() {
  DCHECK_EQ(g_vmex_resource, ZX_HANDLE_INVALID);

  auto vmex_resource_client =
      component::Connect<fuchsia_kernel::VmexResource>();
  if (vmex_resource_client.is_error()) {
    return;
  }

  fidl::SyncClient sync_vmex_resource_client(
      std::move(vmex_resource_client.value()));
  auto result = sync_vmex_resource_client->Get();
  if (result.is_error()) {
    return;
  }

  g_vmex_resource = result->resource().release();
}

zx_vm_option_t GetProtectionFromMemoryPermission(OS::MemoryPermission access) {
  switch (access) {
    case OS::MemoryPermission::kNoAccess:
    case OS::MemoryPermission::kNoAccessWillJitLater:
      return 0;  // no permissions
    case OS::MemoryPermission::kRead:
      return ZX_VM_PERM_READ;
    case OS::MemoryPermission::kReadWrite:
      return ZX_VM_PERM_READ | ZX_VM_PERM_WRITE;
    case OS::MemoryPermission::kReadWriteExecute:
      return ZX_VM_PERM_READ | ZX_VM_PERM_WRITE | ZX_VM_PERM_EXECUTE;
    case OS::MemoryPermission::kReadExecute:
      return ZX_VM_PERM_READ | ZX_VM_PERM_EXECUTE;
  }
  UNREACHABLE();
}

// Determine ZX_VM_ALIGN_X constant corresponding to the specified alignment.
// Returns 0 if there is none.
zx_vm_option_t GetAlignmentOptionFromAlignment(size_t alignment) {
  // The alignment must be one of the ZX_VM_ALIGN_X constants.
  // See zircon/system/public/zircon/types.h.
  static_assert(
      ZX_VM_ALIGN_1KB == (10 << ZX_VM_ALIGN_BASE),
      "Fuchsia's ZX_VM_ALIGN_1KB constant doesn't match expected value");
  static_assert(
      ZX_VM_ALIGN_4GB == (32 << ZX_VM_ALIGN_BASE),
      "Fuchsia's ZX_VM_ALIGN_4GB constant doesn't match expected value");
  zx_vm_option_t alignment_log2 = 0;
  for (int shift = 10; shift <= 32; shift++) {
    if (alignment == (size_t{1} << shift)) {
      alignment_log2 = shift;
      break;
    }
  }
  return alignment_log2 << ZX_VM_ALIGN_BASE;
}

enum class PlacementMode {
  // Attempt to place the object at the provided address, otherwise elsewhere.
  kUseHint,
  // Place the object anywhere it fits.
  kAnywhere,
  // Place the object at the provided address, otherwise fail.
  kFixed
};

void* MapVmo(const zx::vmar& vmar, void* vmar_base, size_t page_size,
             void* address, const zx::vmo& vmo, uint64_t offset,
             PlacementMode placement, size_t size, size_t alignment,
             OS::MemoryPermission access) {
  DCHECK_EQ(0, size % page_size);
  DCHECK_EQ(0, reinterpret_cast<uintptr_t>(address) % page_size);
  DCHECK_IMPLIES(placement != PlacementMode::kAnywhere, address != nullptr);

  zx_vm_option_t options = GetProtectionFromMemoryPermission(access);

  size_t vmar_offset = 0;
  if (placement == PlacementMode::kAnywhere) {
    zx_vm_option_t alignment_option =
        GetAlignmentOptionFromAlignment(alignment);
    if (alignment_option == 0) {
      // Invalid alignment specified, it is not possible to provide an
      // allocation with correct alignment.
      return nullptr;
    }
    options |= alignment_option;
  } else {
    CHECK_EQ(reinterpret_cast<intptr_t>(address) % alignment, 0);
    // Try placing the mapping at the specified address.
    uintptr_t target_addr = reinterpret_cast<uintptr_t>(address);
    uintptr_t base = reinterpret_cast<uintptr_t>(vmar_base);
    DCHECK_GE(target_addr, base);
    vmar_offset = target_addr - base;
    options |= ZX_VM_SPECIFIC;
  }

  zx_vaddr_t result;
  zx_status_t status = vmar.map(options, vmar_offset, vmo, 0, size, &result);

  if (status == ZX_OK) {
    DCHECK_EQ(result % alignment, 0);
    return reinterpret_cast<void*>(result);
  }

  if (placement != PlacementMode::kUseHint) {
    return nullptr;
  }
  // The hint failed, so we try again without the hint but with alignment
  // options.
  // TODO(404563927): Support alignment > 4GB. CppGC's HeapCage allocates with a
  // 32GB alignment, and the allocation fails on Fuchsia at the moment if the
  // provided placement hint is not available. PartitionAlloc already solved
  // this issue, so maybe that solution could be used here as well.
  return MapVmo(vmar, vmar_base, page_size, nullptr, vmo, offset,
                PlacementMode::kAnywhere, size, alignment, access);
}

void* TrimMapping(const zx::vmar& vmar, void* base_address, size_t base_length,
                  size_t target_length, size_t alignment) {
  uintptr_t base = reinterpret_cast<uintptr_t>(base_address);
  uintptr_t new_base = (base + alignment - 1) & ~(alignment - 1);
  DCHECK_GE(new_base, base);
  size_t pre_slack = new_base - base;
  size_t post_slack = base_length - pre_slack - target_length;
  DCHECK_LE(pre_slack, base_length);
  DCHECK_LE(post_slack, base_length);

  if (pre_slack) {
    vmar.unmap(base, pre_slack);
  }
  if (post_slack) {
    vmar.unmap(new_base + target_length, post_slack);
  }
  return reinterpret_cast<void*>(new_base);
}

void* CreateAndMapVmo(const zx::vmar& vmar, void* vmar_base, size_t page_size,
                      void* address, PlacementMode placement, size_t size,
                      size_t alignment, OS::MemoryPermission access) {
  bool huge_alignment = (GetAlignmentOptionFromAlignment(alignment) == 0);
  size_t mapped_size = size;
  if (huge_alignment) {
    mapped_size = size + alignment - page_size;
  }

  zx::vmo vmo;
  if (zx::vmo::create(mapped_size, 0, &vmo) != ZX_OK) {
    return nullptr;
  }
  static const char kVirtualMemoryName[] = "v8-virtualmem";
  vmo.set_property(ZX_PROP_NAME, kVirtualMemoryName,
                   strlen(kVirtualMemoryName));

  // Always call zx_vmo_replace_as_executable() in case the memory will need
  // to be marked as executable in the future.
  // TOOD(https://crbug.com/v8/8899): Only call this when we know that the
  // region will need to be marked as executable in the future.
  zx::unowned_resource vmex(g_vmex_resource);
  if (vmo.replace_as_executable(*vmex, &vmo) != ZX_OK) {
    return nullptr;
  }

  size_t request_size = size;
  size_t request_align = alignment;
  if (placement == PlacementMode::kAnywhere && huge_alignment) {
    request_size = mapped_size;
    request_align = page_size;
  }

  void* addr = MapVmo(vmar, vmar_base, page_size, address, vmo, 0, placement,
                      request_size, request_align, access);

  if (!addr && placement == PlacementMode::kUseHint && huge_alignment) {
    request_size = mapped_size;
    request_align = page_size;
    addr =
        MapVmo(vmar, vmar_base, page_size, nullptr, vmo, 0,
               PlacementMode::kAnywhere, request_size, request_align, access);
  }

  if (addr && huge_alignment && request_size == mapped_size) {
    return TrimMapping(vmar, addr, mapped_size, size, alignment);
  }
  return addr;
}

bool UnmapVmo(const zx::vmar& vmar, size_t page_size, void* address,
              size_t size) {
  DCHECK_EQ(0, reinterpret_cast<uintptr_t>(address) % page_size);
  DCHECK_EQ(0, size % page_size);
  return vmar.unmap(reinterpret_cast<uintptr_t>(address), size) == ZX_OK;
}

bool SetPermissionsInternal(const zx::vmar& vmar, size_t page_size,
                            void* address, size_t size,
                            OS::MemoryPermission access) {
  DCHECK_EQ(0, reinterpret_cast<uintptr_t>(address) % page_size);
  DCHECK_EQ(0, size % page_size);
  uint32_t prot = GetProtectionFromMemoryPermission(access);
  zx_status_t status =
      vmar.protect(prot, reinterpret_cast<uintptr_t>(address), size);

  // Any failure that's not OOM likely indicates a bug in the caller (e.g.
  // using an invalid mapping) so attempt to catch that here to facilitate
  // debugging of these failures. According to the documentation,
  // zx_vmar_protect cannot return ZX_ERR_NO_MEMORY, so any error here is
  // unexpected.
  CHECK_EQ(status, ZX_OK);
  return status == ZX_OK;
}

bool DiscardSystemPagesInternal(const zx::vmar& vmar, size_t page_size,
                                void* address, size_t size) {
  DCHECK_EQ(0, reinterpret_cast<uintptr_t>(address) % page_size);
  DCHECK_EQ(0, size % page_size);
  uint64_t address_int = reinterpret_cast<uint64_t>(address);
  return vmar.op_range(ZX_VMO_OP_DECOMMIT, address_int, size, nullptr, 0) ==
         ZX_OK;
}

zx_status_t CreateAddressSpaceReservationInternal(
    const zx::vmar& vmar, void* vmar_base, size_t page_size, void* address,
    PlacementMode placement, size_t size, size_t alignment,
    OS::MemoryPermission max_permission, zx::vmar* child,
    zx_vaddr_t* child_addr) {
  DCHECK_EQ(0, size % page_size);
  DCHECK_EQ(0, alignment % page_size);
  DCHECK_EQ(0, reinterpret_cast<uintptr_t>(address) % alignment);
  DCHECK_IMPLIES(placement != PlacementMode::kAnywhere, address != nullptr);

  // TODO(v8) determine these based on max_permission.
  zx_vm_option_t options = ZX_VM_CAN_MAP_READ | ZX_VM_CAN_MAP_WRITE |
                           ZX_VM_CAN_MAP_EXECUTE | ZX_VM_CAN_MAP_SPECIFIC;

  zx_vm_option_t alignment_option = GetAlignmentOptionFromAlignment(alignment);
  CHECK_NE(0, alignment_option);  // Invalid alignment specified
  options |= alignment_option;

  size_t vmar_offset = 0;
  if (placement != PlacementMode::kAnywhere) {
    // Try placing the mapping at the specified address.
    uintptr_t target_addr = reinterpret_cast<uintptr_t>(address);
    uintptr_t base = reinterpret_cast<uintptr_t>(vmar_base);
    DCHECK_GE(target_addr, base);
    vmar_offset = target_addr - base;
    options |= ZX_VM_SPECIFIC;
  }

  zx_status_t status =
      vmar.allocate(options, vmar_offset, size, child, child_addr);
  if (status != ZX_OK && placement == PlacementMode::kUseHint) {
    // If a placement hint was specified but couldn't be used (for example,
    // because the offset overlapped another mapping), then retry again without
    // a vmar_offset to let the kernel pick another location.
    options &= ~(ZX_VM_SPECIFIC);
    status = vmar.allocate(options, 0, size, child, child_addr);
  }

  return status;
}

}  // namespace

TimezoneCache* OS::CreateTimezoneCache() {
  return new PosixDefaultTimezoneCache();
}

// static
void OS::Initialize(const char* const gc_fake_mmap) {
  PosixInitializeCommon(gc_fake_mmap);

  // Determine base address of root VMAR.
  zx_info_vmar_t info;
  zx_status_t status = zx::vmar::root_self()->get_info(
      ZX_INFO_VMAR, &info, sizeof(info), nullptr, nullptr);
  CHECK_EQ(ZX_OK, status);
  g_root_vmar_base = reinterpret_cast<void*>(info.base);

  SetVmexResource();
}

// static
void* OS::Allocate(void* address, size_t size, size_t alignment,
                   MemoryPermission access,
                   std::optional<SharedMemoryHandle> handle) {
  // File handles aren't supported.
  DCHECK(!handle.has_value());
  PlacementMode placement =
      address != nullptr ? PlacementMode::kUseHint : PlacementMode::kAnywhere;
  return CreateAndMapVmo(*zx::vmar::root_self(), g_root_vmar_base,
                         AllocatePageSize(), address, placement, size,
                         alignment, access);
}

// static
void OS::Free(void* address, size_t size) {
  CHECK(UnmapVmo(*zx::vmar::root_self(), AllocatePageSize(), address, size));
}

// static
void* OS::AllocateShared(void* address, size_t size,
                         OS::MemoryPermission access, SharedMemoryHandle handle,
                         uint64_t offset) {
  PlacementMode placement =
      address != nullptr ? PlacementMode::kUseHint : PlacementMode::kAnywhere;
  zx::unowned_vmo vmo(handle.GetPlatformHandle());
  return MapVmo(*zx::vmar::root_self(), g_root_vmar_base, AllocatePageSize(),
                address, *vmo, offset, placement, size, AllocatePageSize(),
                access);
}

// static
void OS::FreeShared(void* address, size_t size) {
  CHECK(UnmapVmo(*zx::vmar::root_self(), AllocatePageSize(), address, size));
}

// static
void OS::Release(void* address, size_t size) { Free(address, size); }

// static
bool OS::SetPermissions(void* address, size_t size, MemoryPermission access) {
  return SetPermissionsInternal(*zx::vmar::root_self(), CommitPageSize(),
                                address, size, access);
}

void OS::SetDataReadOnly(void* address, size_t size) {
  CHECK(OS::SetPermissions(address, size, MemoryPermission::kRead));
}

// static
bool OS::SetMemoryRegionName(const void* address, size_t size,
                             const char* name) {
  // Not currently implemented but should be possible using ZX_PROP_NAME.
  return false;
}

// static
bool OS::RecommitPages(void* address, size_t size, MemoryPermission access) {
  return SetPermissions(address, size, access);
}

// static
bool OS::DiscardSystemPages(void* address, size_t size) {
  return DiscardSystemPagesInternal(*zx::vmar::root_self(), CommitPageSize(),
                                    address, size);
}

// static
bool OS::DecommitPages(void* address, size_t size) {
  // We rely on DiscardSystemPages decommitting the pages immediately (via
  // ZX_VMO_OP_DECOMMIT) so that they are guaranteed to be zero-initialized
  // should they be accessed again later on.
  return SetPermissions(address, size, MemoryPermission::kNoAccess) &&
         DiscardSystemPages(address, size);
}

// static
bool OS::SealPages(void* address, size_t size) { return false; }

// static
bool OS::CanReserveAddressSpace() { return true; }

// static
std::optional<AddressSpaceReservation> OS::CreateAddressSpaceReservation(
    void* hint, size_t size, size_t alignment, MemoryPermission max_permission,
    std::optional<SharedMemoryHandle> handle) {
  DCHECK(!handle.has_value());
  DCHECK_EQ(0, reinterpret_cast<Address>(hint) % alignment);
  zx::vmar child;
  zx_vaddr_t child_addr;
  PlacementMode placement =
      hint != nullptr ? PlacementMode::kUseHint : PlacementMode::kAnywhere;
  zx_status_t status = CreateAddressSpaceReservationInternal(
      *zx::vmar::root_self(), g_root_vmar_base, AllocatePageSize(), hint,
      placement, size, alignment, max_permission, &child, &child_addr);
  if (status != ZX_OK) return {};
  return AddressSpaceReservation(reinterpret_cast<void*>(child_addr), size,
                                 child.release());
}

// static
void OS::FreeAddressSpaceReservation(AddressSpaceReservation reservation) {
  // Destroy the vmar and release the handle.
  zx::vmar vmar(reservation.vmar_);
  CHECK_EQ(ZX_OK, vmar.destroy());
}

// static
std::optional<SharedMemoryHandle> OS::CreateSharedMemoryHandleForTesting(
    size_t size) {
  zx::vmo vmo;
  if (zx::vmo::create(size, 0, &vmo) != ZX_OK) {
    return std::nullopt;
  }
  return SharedMemoryHandle::FromPlatformHandle(vmo.release());
}

// static
void OS::DestroySharedMemoryHandle(SharedMemoryHandle handle) {
  zx_handle_t vmo = handle.GetPlatformHandle();
  zx_handle_close(vmo);
}

// static
bool OS::HasLazyCommits() { return true; }

std::vector<OS::SharedLibraryAddress> OS::GetSharedLibraryAddresses() {
  UNREACHABLE();  // TODO(scottmg): Port, https://crbug.com/731217.
}

void OS::SignalCodeMovingGC() {
  UNREACHABLE();  // TODO(scottmg): Port, https://crbug.com/731217.
}

int OS::GetUserTime(uint32_t* secs, uint32_t* usecs) {
  const auto kNanosPerMicrosecond = 1000ULL;
  const auto kMicrosPerSecond = 1000000ULL;

  zx_info_thread_stats_t info = {};
  if (zx::thread::self()->get_info(ZX_INFO_THREAD_STATS, &info, sizeof(info),
                                   nullptr, nullptr) != ZX_OK) {
    return -1;
  }

  // First convert to microseconds, rounding up.
  const uint64_t micros_since_thread_started =
      (info.total_runtime + kNanosPerMicrosecond - 1ULL) / kNanosPerMicrosecond;

  *secs = static_cast<uint32_t>(micros_since_thread_started / kMicrosPerSecond);
  *usecs =
      static_cast<uint32_t>(micros_since_thread_started % kMicrosPerSecond);
  return 0;
}

void OS::AdjustSchedulingParams() {}

std::optional<OS::MemoryRange> OS::GetFirstFreeMemoryRangeWithin(
    OS::Address boundary_start, OS::Address boundary_end, size_t minimum_size,
    size_t alignment) {
  return std::nullopt;
}

std::optional<AddressSpaceReservation>
AddressSpaceReservation::CreateSubReservation(
    void* address, size_t size, OS::MemoryPermission max_permission) {
  DCHECK(Contains(address, size));

  zx::vmar child;
  zx_vaddr_t child_addr;
  zx_status_t status = CreateAddressSpaceReservationInternal(
      *zx::unowned_vmar(vmar_), base(), OS::AllocatePageSize(), address,
      PlacementMode::kFixed, size, OS::AllocatePageSize(), max_permission,
      &child, &child_addr);
  if (status != ZX_OK) return {};
  DCHECK_EQ(reinterpret_cast<void*>(child_addr), address);
  return AddressSpaceReservation(reinterpret_cast<void*>(child_addr), size,
                                 child.release());
}

bool AddressSpaceReservation::FreeSubReservation(
    AddressSpaceReservation reservation) {
  OS::FreeAddressSpaceReservation(reservation);
  return true;
}

bool AddressSpaceReservation::Allocate(void* address, size_t size,
                                       OS::MemoryPermission access) {
  DCHECK(Contains(address, size));
  void* allocation = CreateAndMapVmo(
      *zx::unowned_vmar(vmar_), base(), OS::AllocatePageSize(), address,
      PlacementMode::kFixed, size, OS::AllocatePageSize(), access);
  DCHECK(!allocation || allocation == address);
  return allocation != nullptr;
}

bool AddressSpaceReservation::Free(void* address, size_t size) {
  DCHECK(Contains(address, size));
  return UnmapVmo(*zx::unowned_vmar(vmar_), OS::AllocatePageSize(), address,
                  size);
}

bool AddressSpaceReservation::AllocateShared(void* address, size_t size,
                                             OS::MemoryPermission access,
                                             SharedMemoryHandle handle,
                                             uint64_t offset) {
  DCHECK(Contains(address, size));
  zx::unowned_vmo vmo(handle.GetPlatformHandle());
  return MapVmo(*zx::unowned_vmar(vmar_), base(), OS::AllocatePageSize(),
                address, *vmo, offset, PlacementMode::kFixed, size,
                OS::AllocatePageSize(), access);
}

bool AddressSpaceReservation::FreeShared(void* address, size_t size) {
  DCHECK(Contains(address, size));
  return UnmapVmo(*zx::unowned_vmar(vmar_), OS::AllocatePageSize(), address,
                  size);
}

bool AddressSpaceReservation::SetPermissions(void* address, size_t size,
                                             OS::MemoryPermission access) {
  DCHECK(Contains(address, size));
  return SetPermissionsInternal(*zx::unowned_vmar(vmar_), OS::CommitPageSize(),
                                address, size, access);
}

bool AddressSpaceReservation::RecommitPages(void* address, size_t size,
                                            OS::MemoryPermission access) {
  return SetPermissions(address, size, access);
}

bool AddressSpaceReservation::DiscardSystemPages(void* address, size_t size) {
  DCHECK(Contains(address, size));
  return DiscardSystemPagesInternal(*zx::unowned_vmar(vmar_),
                                    OS::CommitPageSize(), address, size);
}

bool AddressSpaceReservation::DecommitPages(void* address, size_t size) {
  DCHECK(Contains(address, size));
  // See comment in OS::DecommitPages.
  return SetPermissions(address, size, OS::MemoryPermission::kNoAccess) &&
         DiscardSystemPages(address, size);
}

}  // namespace base
}  // namespace v8
