/*
 * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
 * SPDX-License-Identifier: Apache-2.0
 *
 * 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.
 */

#ifndef NVIDIA_GXF_UNIQUE_EVENT_LIST_HPP_
#define NVIDIA_GXF_UNIQUE_EVENT_LIST_HPP_

#include <atomic>
#include <cstdint>
#include <functional>
#include <mutex>
#include <vector>

#include "common/logger.hpp"
#include "gxf/core/expected.hpp"
#include "gxf/core/gxf.h"

namespace nvidia {
namespace gxf {

// Thread-safe unique event queue with zero heap allocation after reserve().
//
// Uses an open-addressing hash table with linear probing and backward-shift
// deletion, combined with an intrusive doubly-linked list for FIFO ordering.
// All storage is in a single pre-allocated vector — no per-event heap
// allocation for list nodes or hash map buckets.
template <typename T>
class UniqueEventList {
 public:
  UniqueEventList() { rehash(kDefaultCapacity); }
  UniqueEventList(const UniqueEventList<T>&) = delete;
  UniqueEventList& operator=(const UniqueEventList<T>&) = delete;

  // Adds an item on to the event list. Returns true if inserted, false if duplicate.
  bool pushEvent(T item) {
    std::lock_guard<std::mutex> lock(list_mutex_);
    if (count_ * 2 >= capacity_) { grow(); }

    size_t idx = hash(item) & mask_;
    // Probe for existing entry or empty slot
    while (slots_[idx].occupied) {
      if (slots_[idx].key == item) { return false; }  // Duplicate
      idx = (idx + 1) & mask_;
    }

    // Insert into hash table
    slots_[idx].key = item;
    slots_[idx].occupied = true;
    // Link at tail of FIFO list
    slots_[idx].prev = tail_;
    slots_[idx].next = -1;
    if (tail_ >= 0) {
      slots_[tail_].next = static_cast<int32_t>(idx);
    } else {
      head_ = static_cast<int32_t>(idx);
    }
    tail_ = static_cast<int32_t>(idx);

    count_++;
    size_.fetch_add(1, std::memory_order_release);
    return true;
  }

  // Removes an event from the list if it exists
  void removeEvent(T item) {
    std::lock_guard<std::mutex> lock(list_mutex_);
    int32_t idx = findSlot(item);
    if (idx < 0) { return; }
    unlinkAndErase(idx);
    count_--;
    size_.fetch_sub(1, std::memory_order_release);
  }

  // Pops the first element in the list (FIFO order)
  Expected<T> popEvent() {
    std::lock_guard<std::mutex> lock(list_mutex_);
    if (head_ < 0) { return Unexpected{GXF_FAILURE}; }

    T event = slots_[head_].key;
    unlinkAndErase(head_);
    count_--;
    size_.fetch_sub(1, std::memory_order_release);
    return event;
  }

  // Pops up to max_events elements in FIFO order into out.
  // Returns the number of elements popped.
  size_t popEvents(size_t max_events, std::vector<T>& out) {
    if (max_events == 0) { return 0; }
    std::lock_guard<std::mutex> lock(list_mutex_);
    size_t popped = 0;
    while (head_ >= 0 && popped < max_events) {
      out.push_back(slots_[head_].key);
      unlinkAndErase(head_);
      count_--;
      size_.fetch_sub(1, std::memory_order_release);
      popped++;
    }
    return popped;
  }

  // Exports a copy of the events in the list (FIFO order)
  std::vector<T> exportList() const {
    std::lock_guard<std::mutex> lock(list_mutex_);
    std::vector<T> result;
    result.reserve(count_);
    for (int32_t i = head_; i >= 0; i = slots_[i].next) {
      result.push_back(slots_[i].key);
    }
    return result;
  }

  // Checks if the list is empty (lock-free using atomic size counter)
  bool empty() const {
    return size_.load(std::memory_order_acquire) == 0;
  }

  // Returns size of the event list (lock-free using atomic size counter)
  size_t size() const {
    return size_.load(std::memory_order_acquire);
  }

  // Checks if the list has event
  bool hasEvent(T item) const {
    std::lock_guard<std::mutex> lock(list_mutex_);
    return findSlot(item) >= 0;
  }

  // Clears the event list
  void clear() {
    std::lock_guard<std::mutex> lock(list_mutex_);
    for (int32_t i = head_; i >= 0;) {
      int32_t next = slots_[i].next;
      slots_[i].occupied = false;
      i = next;
    }
    head_ = -1;
    tail_ = -1;
    count_ = 0;
    size_.store(0, std::memory_order_release);
  }

  // Pre-reserves capacity for the expected maximum number of unique events.
  // Allocates all storage upfront so push/pop/remove are heap-free.
  void reserve(size_t capacity) {
    std::lock_guard<std::mutex> lock(list_mutex_);
    // Need 2x capacity for ~50% load factor in open addressing
    size_t needed = roundUpPow2(capacity * 2);
    if (needed > capacity_) { rehash(needed); }
  }

  mutable std::mutex list_mutex_;

 private:
  static constexpr size_t kDefaultCapacity = 64;

  struct Slot {
    T key{};
    int32_t prev{-1};    // Linked list: previous slot index (-1 = none)
    int32_t next{-1};    // Linked list: next slot index (-1 = none)
    bool occupied{false};
  };

  size_t hash(T key) const {
    return std::hash<T>{}(key);
  }

  static size_t roundUpPow2(size_t v) {
    if (v == 0) { return 1; }
    v--;
    v |= v >> 1;
    v |= v >> 2;
    v |= v >> 4;
    v |= v >> 8;
    v |= v >> 16;
    v |= v >> 32;
    return v + 1;
  }

  // Find the slot index for a key, or -1 if not found.
  int32_t findSlot(T key) const {
    size_t idx = hash(key) & mask_;
    while (slots_[idx].occupied) {
      if (slots_[idx].key == key) { return static_cast<int32_t>(idx); }
      idx = (idx + 1) & mask_;
    }
    return -1;
  }

  // Unlink a slot from the doubly-linked list and erase from hash table
  // using backward-shift deletion (no tombstones).
  void unlinkAndErase(int32_t idx) {
    // Unlink from doubly-linked list
    int32_t p = slots_[idx].prev;
    int32_t n = slots_[idx].next;
    if (p >= 0) { slots_[p].next = n; } else { head_ = n; }
    if (n >= 0) { slots_[n].prev = p; } else { tail_ = p; }

    // Backward-shift deletion: fill the gap without tombstones
    size_t empty_idx = static_cast<size_t>(idx);
    slots_[empty_idx].occupied = false;

    size_t j = empty_idx;
    while (true) {
      j = (j + 1) & mask_;
      if (!slots_[j].occupied) { break; }

      size_t ideal = hash(slots_[j].key) & mask_;
      // Check if slot j's ideal position is NOT in the range (empty_idx, j].
      // If so, j should shift backward to fill the empty slot.
      bool should_shift;
      if (empty_idx <= j) {
        should_shift = (ideal <= empty_idx) || (ideal > j);
      } else {
        should_shift = (ideal <= empty_idx) && (ideal > j);
      }

      if (should_shift) {
        // Move slot j to empty_idx
        slots_[empty_idx].key = slots_[j].key;
        slots_[empty_idx].occupied = true;
        slots_[empty_idx].prev = slots_[j].prev;
        slots_[empty_idx].next = slots_[j].next;

        // Update linked list neighbors to point to new position
        int32_t new_idx = static_cast<int32_t>(empty_idx);
        if (slots_[empty_idx].prev >= 0) {
          slots_[slots_[empty_idx].prev].next = new_idx;
        } else {
          head_ = new_idx;
        }
        if (slots_[empty_idx].next >= 0) {
          slots_[slots_[empty_idx].next].prev = new_idx;
        } else {
          tail_ = new_idx;
        }

        slots_[j].occupied = false;
        empty_idx = j;
      }
    }
  }

  // Rebuild the hash table with a new capacity. Preserves FIFO order.
  void rehash(size_t new_capacity) {
    // Save current list in FIFO order
    std::vector<T> old_items;
    old_items.reserve(count_);
    for (int32_t i = head_; i >= 0; i = slots_[i].next) {
      old_items.push_back(slots_[i].key);
    }

    capacity_ = new_capacity;
    mask_ = capacity_ - 1;
    slots_.clear();
    slots_.resize(capacity_);
    head_ = -1;
    tail_ = -1;
    count_ = 0;

    // Re-insert in original FIFO order (no lock needed, called under lock)
    for (const T& item : old_items) {
      size_t idx = hash(item) & mask_;
      while (slots_[idx].occupied) {
        idx = (idx + 1) & mask_;
      }
      slots_[idx].key = item;
      slots_[idx].occupied = true;
      slots_[idx].prev = tail_;
      slots_[idx].next = -1;
      if (tail_ >= 0) {
        slots_[tail_].next = static_cast<int32_t>(idx);
      } else {
        head_ = static_cast<int32_t>(idx);
      }
      tail_ = static_cast<int32_t>(idx);
      count_++;
    }
  }

  void grow() {
    GXF_LOG_DEBUG("UniqueEventList growing from capacity %zu; consider calling reserve() "
                  "at initialization to avoid rehashing under lock.", capacity_);
    rehash(capacity_ * 2);
  }

  std::vector<Slot> slots_;
  size_t capacity_{0};
  size_t mask_{0};
  int32_t head_{-1};   // Front of FIFO list (popEvent reads from here)
  int32_t tail_{-1};   // Back of FIFO list (pushEvent appends here)
  size_t count_{0};    // Number of occupied slots
  std::atomic<size_t> size_{0};  // Lock-free size counter for empty()/size() checks
};

}  // namespace gxf
}  // namespace nvidia

#endif  // NVIDIA_GXF_UNIQUE_EVENT_LIST_HPP_
