// Copyright 2026 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

#ifndef IREE_ASYNC_FRONTIER_H_
#define IREE_ASYNC_FRONTIER_H_

#include "iree/base/api.h"

#ifdef __cplusplus
extern "C" {
#endif  // __cplusplus

//===----------------------------------------------------------------------===//
// Causal domain
//===----------------------------------------------------------------------===//

// Identifies the category of timeline participant represented by an axis.
// Each domain has its own ordinal encoding scheme within the axis (see
// iree_async_axis_t documentation for the full encoding).
enum iree_async_causal_domain_e {
  // Hardware or virtual device queue.
  IREE_ASYNC_CAUSAL_DOMAIN_QUEUE = 0,
  // Collective communication channel.
  IREE_ASYNC_CAUSAL_DOMAIN_COLLECTIVE = 1,
  // Epilogue kernel timeline.
  IREE_ASYNC_CAUSAL_DOMAIN_EPILOGUE = 2,
  // Host-side virtual queue (thread or execution context).
  IREE_ASYNC_CAUSAL_DOMAIN_HOST = 3,
};
typedef uint8_t iree_async_causal_domain_t;

//===----------------------------------------------------------------------===//
// Axis
//===----------------------------------------------------------------------===//

// A 64-bit identifier for a timeline participant in the causal ordering system.
//
// Every independent source of progress in the system — every GPU queue, every
// collective channel, every host thread — is an "axis." Each axis has a
// monotonically increasing timeline value (epoch). An axis at epoch N means
// "all work submitted to this participant up to sequence point N has
// completed."
//
// Axes are the building blocks of frontiers. A frontier says "axis A is at
// epoch 5, axis B is at epoch 12" — meaning the referenced event depends on
// both A having completed 5 operations and B having completed 12.
//
// The encoding is structured for debuggability and deterministic allocation:
//
//   ┌────────────┬────────────┬────────────┬──────────────────────────────┐
//   │ Bits 63-56 │ Bits 55-48 │ Bits 47-40 │         Bits 39-0            │
//   ├────────────┼────────────┼────────────┼──────────────────────────────┤
//   │  Session   │  Machine   │   Domain   │           Ordinal            │
//   │   Epoch    │   Index    │            │     (structured by domain)   │
//   │   (8 b)    │   (8 b)    │   (8 b)    │           (40 b)             │
//   └────────────┴────────────┴────────────┴──────────────────────────────┘
//
//   Session epoch: Increments per session creation. Prevents ABA problems
//     across sessions — an axis from session 3 can never be confused with
//     one from session 4 even if machine/domain/ordinal happen to match.
//
//   Machine index: 0-255 within a session. Assigned by the coordinator at
//     session join time. Identifies which physical machine this axis lives on.
//
//   Domain: Which category of timeline participant (see
//   iree_async_causal_domain_t).
//
//   Ordinal: Domain-specific structured identifier:
//     QUEUE domain:
//       [39:32] device_index (8 bits) — 256 devices per machine
//       [31:24] queue_index  (8 bits) — 256 queues per device
//       [23:0]  reserved     (24 bits)
//     COLLECTIVE domain:
//       [39:0]  channel_id   (40 bits) — coordinator-assigned
//     HOST domain:
//       [39:0]  context_id   (40 bits) — thread/context identifier
//
// Axes are computed deterministically from logical topology: the same topology
// description always produces the same axis values, regardless of which
// physical GPU fills each slot. This makes frontiers directly comparable across
// machines without translation.
//
// Example axis values (session 1, machine 3):
//   GPU queue 0 on device 1:  0x01_03_00_01_00_000000
//   Collective channel 42:    0x01_03_01_00_00_00002A
//   Host context 7:           0x01_03_03_00_00_000007
typedef uint64_t iree_async_axis_t;

// Constructs an axis from its four components. The ordinal is masked to 40
// bits; the caller is responsible for encoding domain-specific structure within
// the ordinal.
static inline iree_async_axis_t iree_async_axis_make(
    uint8_t session_epoch, uint8_t machine_index,
    iree_async_causal_domain_t domain, uint64_t ordinal) {
  return ((uint64_t)session_epoch << 56) | ((uint64_t)machine_index << 48) |
         ((uint64_t)domain << 40) | (ordinal & 0xFFFFFFFFFFull);
}

// Constructs a QUEUE-domain axis from device and queue indices.
// This is the most common axis construction — one per GPU queue in the system.
static inline iree_async_axis_t iree_async_axis_make_queue(
    uint8_t session_epoch, uint8_t machine_index, uint8_t device_index,
    uint8_t queue_index) {
  uint64_t ordinal =
      ((uint64_t)device_index << 32) | ((uint64_t)queue_index << 24);
  return iree_async_axis_make(session_epoch, machine_index,
                              IREE_ASYNC_CAUSAL_DOMAIN_QUEUE, ordinal);
}

// Extracts the session epoch from an axis.
static inline uint8_t iree_async_axis_session(iree_async_axis_t axis) {
  return (uint8_t)(axis >> 56);
}

// Extracts the machine index from an axis.
static inline uint8_t iree_async_axis_machine(iree_async_axis_t axis) {
  return (uint8_t)(axis >> 48);
}

// Extracts the causal domain from an axis.
static inline iree_async_causal_domain_t iree_async_axis_domain(
    iree_async_axis_t axis) {
  return (iree_async_causal_domain_t)((axis >> 40) & 0xFF);
}

// Extracts the raw 40-bit ordinal from an axis. Interpretation depends on the
// domain (see iree_async_axis_t encoding documentation).
static inline uint64_t iree_async_axis_ordinal(iree_async_axis_t axis) {
  return axis & 0xFFFFFFFFFFull;
}

// Extracts the device index from a QUEUE-domain axis.
// Undefined behavior if the axis is not in the QUEUE domain.
static inline uint8_t iree_async_axis_device_index(iree_async_axis_t axis) {
  return (uint8_t)(iree_async_axis_ordinal(axis) >> 32);
}

// Extracts the queue index from a QUEUE-domain axis.
// Undefined behavior if the axis is not in the QUEUE domain.
static inline uint8_t iree_async_axis_queue_index(iree_async_axis_t axis) {
  return (uint8_t)(iree_async_axis_ordinal(axis) >> 24);
}

//===----------------------------------------------------------------------===//
// Frontier
//===----------------------------------------------------------------------===//

// A single (axis, epoch) pair within a frontier. States that the given axis
// must have reached at least the given epoch for this entry to be satisfied.
typedef struct iree_async_frontier_entry_t {
  // Timeline participant being constrained.
  iree_async_axis_t axis;
  // Minimum completed epoch required on the axis.
  uint64_t epoch;
} iree_async_frontier_entry_t;

// Fields shared by all frontier header representations. This is used by both
// the variable-length frontier type and fixed-capacity frontier storage so that
// field comments and layout stay in one place.
#define IREE_ASYNC_FRONTIER_HEADER_FIELDS                           \
  /* Number of valid entries in the frontier. */                    \
  uint8_t entry_count;                                              \
  /* Reserved bytes preserving the fixed frontier header layout. */ \
  uint8_t reserved[7]

// Frontiers are followed by entries containing uint64_t values. wasm32 keeps
// pointers at 4-byte alignment but still aligns those entries to 8 bytes.
#define IREE_ASYNC_FRONTIER_ALIGNMENT 8

// Fixed-size frontier header. Useful for embedding a frontier header before
// trailing entry storage in another variable-size record.
typedef struct iree_alignas(IREE_ASYNC_FRONTIER_ALIGNMENT)
    iree_async_frontier_header_t {
  IREE_ASYNC_FRONTIER_HEADER_FIELDS;
} iree_async_frontier_header_t;

// A frontier is a vector clock: a set of (axis, epoch) pairs that together
// define a causal position in the distributed system. A frontier answers the
// question: "what must have happened before this point?"
//
// ## The problem frontiers solve
//
// In a multi-device, multi-machine system, work proceeds on many independent
// timelines simultaneously. A single buffer might be:
//   - Produced by GPU0's compute queue (dispatch 47)
//   - Transferred via network (send sequence 12)
//   - Have its metadata updated by the host (context 3, step 8)
//
// Before you can safely read that buffer, ALL THREE of those timelines must
// have reached their respective points. A single semaphore tracks one timeline.
// A frontier tracks all of them at once.
//
// ## Visual intuition
//
// Think of time flowing left-to-right on multiple parallel tracks:
//
//   GPU0 queue0: ──①──②──③──④──⑤──⑥──⑦──▶
//   GPU0 queue1: ──①──②──③──④──▶
//   GPU1 queue0: ──①──②──③──④──⑤──▶
//   Network:     ──①──②──③──▶
//
// A frontier is a vertical "cut" across these timelines:
//
//   GPU0 queue0: ──①──②──③──④──⑤──│──⑥──⑦──▶
//   GPU0 queue1: ──①──②──③──│──④──▶
//   GPU1 queue0: ──①──②──③──④──│──⑤──▶
//   Network:     ──①──②──│──③──▶
//                              │
//                         frontier F = {
//                           (GPU0/q0, 5),
//                           (GPU0/q1, 3),
//                           (GPU1/q0, 4),
//                           (Network, 2),
//                         }
//
// Everything to the LEFT of the cut has happened. Everything to the RIGHT
// may or may not have happened yet. The frontier IS the cut.
//
// ## Causal ordering (happens-before)
//
// Two frontiers define a partial order:
//
//   F1 BEFORE F2: Every axis in F1 is at the same or earlier epoch than in F2,
//                 and at least one is strictly earlier. F1's "cut" is entirely
//                 to the left of F2's. Anything that depends on F1 being
//                 satisfied also satisfies F2 (but not vice versa).
//
//   F1 AFTER F2:  The reverse. F2 happens-before F1.
//
//   CONCURRENT:   Neither dominates. F1 is ahead on some axes, F2 is ahead on
//                 others. There is no causal relationship — these events could
//                 have happened in either order (or simultaneously).
//
//   EQUAL:        Same cut. Same causal position.
//
// Example:
//   F1 = {(A, 3), (B, 5)}
//   F2 = {(A, 4), (B, 7)}     → F1 BEFORE F2 (both axes advanced)
//   F3 = {(A, 5), (B, 3)}     → F1 CONCURRENT with F3 (A advanced, B went back)
//
// ## Relationship to semaphores
//
// Each axis in a frontier corresponds to one iree_async_semaphore_t timeline.
// A frontier {(axis_A, 5), (axis_B, 12)} is equivalent to:
//   "wait for semaphore_A >= 5 AND semaphore_B >= 12"
//
// The frontier is a COMPACT REPRESENTATION of a multi-semaphore wait. It adds:
//   - Causal reasoning (compare, merge) that individual semaphores don't have.
//   - Wire-format efficiency (one frontier vs. N semaphore references).
//   - Transitive causality propagation (merge preserves all dependencies).
//
// ## Merge (join in the lattice)
//
// Merging two frontiers produces the LEAST UPPER BOUND: the earliest point
// that is guaranteed to be after both inputs. This is component-wise max:
//
//   merge({(A,3), (B,5)}, {(A,7), (C,2)}) = {(A,7), (B,5), (C,2)}
//
// Merge is used when an operation depends on two independent predecessors:
//
//   GPU produces buffer    → frontier {(GPU/q0, 47)}
//   Network receives data  → frontier {(Net, 12)}
//   Operation needs both   → merge = {(GPU/q0, 47), (Net, 12)}
//
// Merge is associative and commutative (it's a lattice join).
//
// ## Wire format usage
//
// Frontiers are the core of the remoting protocol's consistency guarantees.
// Every command sent across the network carries a frontier:
//
//   "Execute this dispatch, but only after frontier F is satisfied."
//
// The receiver maps the frontier's axes to local semaphores (via the axis
// table) and waits for all of them. This is how GPU→GPU synchronization works
// across machines without a centralized lock server.
//
// ## Storage and allocation
//
// Frontiers are variable-length value types (VLA). Callers provide storage:
//   - Stack: for small frontiers (≤4 entries is the common case).
//   - Slab: via iree_async_frontier_size() for overflow-checked sizing.
//   - Inline: embedded in protocol messages with trailing entry data.
//
// Entries MUST be sorted by axis (ascending) for O(n) comparison and merge.
// The entry_count is uint8_t: maximum 255 entries per frontier (sufficient
// for rack-scale systems with hundreds of queues).
typedef struct iree_async_frontier_t {
  IREE_ASYNC_FRONTIER_HEADER_FIELDS;
  // Variable-length frontier entries sorted by ascending axis.
  iree_async_frontier_entry_t entries[];
} iree_async_frontier_t;

static_assert(offsetof(iree_async_frontier_t, entries) ==
                  sizeof(iree_async_frontier_header_t),
              "frontier header must match frontier FAM offset");
static_assert(IREE_ASYNC_FRONTIER_ALIGNMENT >=
                  iree_alignof(iree_async_frontier_entry_t),
              "frontier alignment must cover frontier entry alignment");
static_assert(iree_alignof(iree_async_frontier_header_t) ==
                  iree_alignof(iree_async_frontier_t),
              "frontier header must match frontier alignment");

#define IREE_ASYNC_FIXED_FRONTIER_TYPE(type_name, entry_capacity)              \
  typedef struct type_name {                                                   \
    IREE_ASYNC_FRONTIER_HEADER_FIELDS;                                         \
    /* Fixed-capacity frontier entries sorted by ascending axis. */            \
    iree_async_frontier_entry_t entries[(entry_capacity)];                     \
  } type_name;                                                                 \
  static_assert(offsetof(type_name, entries) == sizeof(iree_async_frontier_t), \
                #type_name " entries must alias iree_async_frontier_t FAM");   \
  static_assert(                                                               \
      iree_alignof(type_name) == iree_alignof(iree_async_frontier_t),          \
      #type_name " must match iree_async_frontier_t alignment")

// A frontier with exactly one (axis, epoch) entry, sized for stack allocation.
// Memory-layout-compatible with iree_async_frontier_t when cast — the entries
// fixed array begins at the same offset as the FAM in iree_async_frontier_t.
//
// This is the common case for single-queue drivers: one queue produces one axis
// entry per signal. Declare on the stack and use the as_frontier accessor for
// API calls that take iree_async_frontier_t*.
IREE_ASYNC_FIXED_FRONTIER_TYPE(iree_async_single_frontier_t, 1);

// Casts fixed-capacity frontier storage to the variable-size frontier view.
static inline iree_async_frontier_t* iree_async_fixed_frontier_as_frontier(
    void* storage) {
  return (iree_async_frontier_t*)storage;
}

// Casts fixed-capacity frontier storage to the const variable-size view.
static inline const iree_async_frontier_t*
iree_async_fixed_frontier_as_const_frontier(const void* storage) {
  return (const iree_async_frontier_t*)storage;
}

// Returns a pointer to the layout-compatible iree_async_frontier_t within a
// single-entry frontier. Valid for all frontier APIs (compare, merge, etc.).
static inline iree_async_frontier_t* iree_async_single_frontier_as_frontier(
    iree_async_single_frontier_t* single) {
  return iree_async_fixed_frontier_as_frontier(single);
}

// Returns a const pointer to the layout-compatible iree_async_frontier_t.
static inline const iree_async_frontier_t*
iree_async_single_frontier_as_const_frontier(
    const iree_async_single_frontier_t* single) {
  return iree_async_fixed_frontier_as_const_frontier(single);
}

// Initializes a single-entry frontier for the given axis and epoch.
static inline void iree_async_single_frontier_initialize(
    iree_async_single_frontier_t* frontier, iree_async_axis_t axis,
    uint64_t epoch) {
  frontier->entry_count = 1;
  memset(frontier->reserved, 0, sizeof(frontier->reserved));
  frontier->entries[0].axis = axis;
  frontier->entries[0].epoch = epoch;
}

// Computes the total byte size needed to store a frontier with |entry_count|
// entries using overflow-checked arithmetic. Use this for slab or heap
// allocation:
//
//   iree_host_size_t size = 0;
//   IREE_RETURN_IF_ERROR(iree_async_frontier_size(3, &size));
//   iree_async_frontier_t* f = NULL;
//   IREE_RETURN_IF_ERROR(iree_allocator_malloc(allocator, size, (void**)&f));
//   iree_async_frontier_initialize(f, 3);
//   f->entries[0] = (iree_async_frontier_entry_t){gpu_axis, 5};
//   f->entries[1] = (iree_async_frontier_entry_t){net_axis, 12};
//   f->entries[2] = (iree_async_frontier_entry_t){host_axis, 8};
static inline iree_status_t iree_async_frontier_size(
    iree_host_size_t entry_count, iree_host_size_t* out_size) {
  return IREE_STRUCT_LAYOUT(
      sizeof(iree_async_frontier_t), out_size,
      IREE_STRUCT_FIELD_FAM(entry_count, iree_async_frontier_entry_t));
}

// Initializes a pre-allocated frontier with the given entry count.
// After this call, the caller must fill entries[0..entry_count-1] with
// (axis, epoch) pairs IN SORTED ORDER BY AXIS (ascending).
//
// WARNING: The sorted-entries invariant is critical for correctness.
// iree_async_frontier_compare() and iree_async_frontier_merge() use a
// merge-scan algorithm that relies on sorted order. Unsorted entries cause
// SILENT INCORRECT CAUSAL ORDERING — operations may execute before their
// dependencies complete, leading to data corruption. There is no runtime
// check on the hot path (compare/merge are O(n) and must stay cheap).
// Use iree_async_frontier_validate() in debug/test code to catch violations.
static inline void iree_async_frontier_initialize(
    iree_async_frontier_t* frontier, uint8_t entry_count) {
  frontier->entry_count = entry_count;
  memset(frontier->reserved, 0, sizeof(frontier->reserved));
}

// Validates that a frontier's entries satisfy the sorted-by-axis invariant.
// Returns iree_ok_status() if valid, or an error describing the violation.
// This is intended for debug assertions and test validation — NOT for hot
// paths. Call after constructing a frontier to catch programming errors early.
//
// Checks:
//   - Entries are in strictly ascending axis order (no duplicates).
//   - No zero-epoch entries (epoch 0 is meaningless — the axis hasn't
//     advanced, so it should not be in the frontier at all).
//
// Usage:
//   iree_async_frontier_initialize(f, 3);
//   f->entries[0] = ...;
//   f->entries[1] = ...;
//   f->entries[2] = ...;
//   IREE_ASSERT_OK(iree_async_frontier_validate(f));  // Debug only.
iree_status_t iree_async_frontier_validate(
    const iree_async_frontier_t* frontier);

//===----------------------------------------------------------------------===//
// Frontier comparison
//===----------------------------------------------------------------------===//

// Result of comparing two frontiers. Defines the causal relationship between
// the events they represent.
//
// The comparison follows vector clock semantics:
//   - BEFORE: a is dominated by b (a happens-before b).
//   - AFTER: b is dominated by a (b happens-before a).
//   - CONCURRENT: neither dominates (no causal relationship).
//   - EQUAL: identical causal position.
//
// Axes present in one frontier but not the other are treated as epoch 0 in
// the missing frontier. This means a frontier that mentions an axis is always
// AFTER a frontier that doesn't mention it (assuming epoch > 0).
enum iree_async_frontier_comparison_e {
  IREE_ASYNC_FRONTIER_EQUAL = 0,
  IREE_ASYNC_FRONTIER_BEFORE = 1,
  IREE_ASYNC_FRONTIER_AFTER = 2,
  IREE_ASYNC_FRONTIER_CONCURRENT = 3,
};
typedef uint8_t iree_async_frontier_comparison_t;

// Compares two frontiers for causal ordering. Both must have entries sorted by
// axis. Returns the relationship of |a| relative to |b|:
//   BEFORE means "a happens-before b"
//   AFTER means "b happens-before a"
//
// Runs in O(n + m) time where n and m are the entry counts (merge-scan of two
// sorted arrays).
//
// Example:
//   a = {(axis_X, 3), (axis_Y, 5)}
//   b = {(axis_X, 3), (axis_Y, 7)}
//   compare(a, b) → BEFORE (Y advanced from 5 to 7, X stayed)
iree_async_frontier_comparison_t iree_async_frontier_compare(
    const iree_async_frontier_t* a, const iree_async_frontier_t* b);

// Merges |source| into |target| by taking the component-wise maximum epoch
// for each axis. Axes in |source| not present in |target| are added. Both
// must have entries sorted by axis.
//
// The result is the LEAST UPPER BOUND (join) in the causal lattice: the
// earliest frontier that is causally after both |target| and |source|.
//
// |target_capacity| is the maximum number of entries |target|'s allocation can
// hold. If the merged result would exceed capacity, the merge fails and
// |target| is left unchanged. Callers must size capacity to accommodate
// worst-case merge (sum of unique axes across all sources). 255 entries
// (uint8_t max) is sufficient for rack-scale systems.
//
// Performance: three paths in order of expected frequency:
//   1. Source empty: O(1) no-op.
//   2. Identical axis sets: O(n) epoch-max in place, zero entry movement.
//   3. Different axis sets: O(n+m) in-place right-to-left merge, no scratch.
//
// Returns true on success (|target| modified, entry_count updated).
// Returns false if the merged count would exceed |target_capacity|.
//
// Example:
//   target = {(A, 3), (B, 5)}       capacity = 4
//   source = {(A, 7), (C, 2)}
//   after merge: target = {(A, 7), (B, 5), (C, 2)}   entry_count = 3
bool iree_async_frontier_merge(iree_async_frontier_t* target,
                               uint8_t target_capacity,
                               const iree_async_frontier_t* source);

// Merges |source| into |target| like iree_async_frontier_merge() and reports
// whether the old |target| frontier was already dominated by |source|.
//
// If the merge succeeds and |*out_source_dominates_target| is true, the merged
// |target| frontier is exactly equal to |source|. This lets producers publish a
// compact "one producer epoch covers the full frontier" summary without doing a
// second post-merge equality scan.
//
// If the merge fails due to capacity overflow, returns false, leaves |target|
// unchanged, and sets |*out_source_dominates_target| to false.
bool iree_async_frontier_merge_and_test_source_dominance(
    iree_async_frontier_t* target, uint8_t target_capacity,
    const iree_async_frontier_t* source, bool* out_source_dominates_target);

// Returns true if every entry in |frontier| is satisfied by the corresponding
// epoch in |current_epochs|. A frontier is satisfied when all of its axes have
// reached at least their target epoch values.
//
// |current_epochs| is a sorted array of (axis, epoch) pairs representing the
// current progress of each axis in the system. Axes in |frontier| not found in
// |current_epochs| are treated as epoch 0 (unsatisfied unless target is 0).
//
// This is the core predicate for "can this operation execute now?":
//   if (iree_async_frontier_is_satisfied(cmd->prerequisite, tracker_epochs, n))
//     execute(cmd);
bool iree_async_frontier_is_satisfied(
    const iree_async_frontier_t* frontier,
    const iree_async_frontier_entry_t* current_epochs,
    iree_host_size_t current_epochs_count);

// Finds entries in |target| that |reference| does not dominate. An entry is
// undominated if:
//   - The axis exists in |target| but not in |reference|, OR
//   - The axis exists in both but target.epoch > reference.epoch
//
// This is the combined tier 1 + tier 2 extraction for wait resolution: given
// a queue's frontier (reference) and a semaphore's frontier (target), the
// undominated entries are the axes where the queue has NOT yet waited on or
// submitted past the semaphore's causal dependencies. These axes need either
// device-side barriers (tier 2) or host-side deferral (tier 3).
//
// Writes undominated entries to |out_entries| (up to |capacity|). Returns the
// total count of undominated entries, which may exceed |capacity| to signal
// truncation. The output entries are in ascending axis order (preserved from
// the sorted merge-scan). Both input frontiers must have sorted entries.
//
// Pass |capacity| = 0 and |out_entries| = NULL to count without writing.
//
// Performance: same structure as iree_async_frontier_compare:
//   - Fast path: identical axis sets → O(n) epoch comparison, no branching.
//   - Slow path: different axis sets → O(n+m) merge-scan.
//
// Example:
//   reference = {(A, 5), (B, 3)}
//   target    = {(A, 7), (C, 2)}
//   result    = {(A, 7), (C, 2)}   count = 2
//   (A is undominated: 7 > 5. C is undominated: absent from reference.)
uint8_t iree_async_frontier_find_undominated(
    const iree_async_frontier_t* reference, const iree_async_frontier_t* target,
    uint8_t capacity, iree_async_frontier_entry_t* out_entries);

#ifdef __cplusplus
}  // extern "C"
#endif  // __cplusplus

#endif  // IREE_ASYNC_FRONTIER_H_
