// Copyright (c) 2023 Google Inc.
//
// 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.

#include <stddef.h>

#include <algorithm>
#include <cassert>
#include <cstdint>
#include <functional>
#include <initializer_list>
#include <iterator>
#include <limits>
#include <type_traits>
#include <vector>

#ifndef SOURCE_ENUM_SET_H_
#define SOURCE_ENUM_SET_H_

#include "source/latest_version_spirv_header.h"

namespace spvtools {

// This container is optimized to store and retrieve unsigned enum values.
// The base model for this implementation is an open-addressing hashtable with
// linear probing. For small enums (max index < 64), all operations are O(1).
//
// - Enums are stored in buckets (64 contiguous values max per bucket)
// - Buckets ranges don't overlap, but don't have to be contiguous.
// - Enums are packed into 64-bits buckets, using 1 bit per enum value.
//
// Example:
//  - MyEnum { A = 0, B = 1, C = 64, D = 65 }
//  - 2 buckets are required:
//      - bucket 0, storing values in the range [ 0;  64[
//      - bucket 1, storing values in the range [64; 128[
//
// - Buckets are stored in a sorted vector (sorted by bucket range).
// - Retrieval is done by computing the theoretical bucket index using the enum
// value, and
//   doing a linear scan from this position.
// - Insertion is done by retrieving the bucket and either:
//   - inserting a new bucket in the sorted vector when no buckets has a
//   compatible range.
//   - setting the corresponding bit in the bucket.
//   This means insertion in the middle/beginning can cause a memmove when no
//   bucket is available. In our case, this happens at most 23 times for the
//   largest enum we have (Opcodes).
template <typename T>
class EnumSet {
 private:
  using BucketType = uint64_t;
  using ElementType = std::underlying_type_t<T>;
  static_assert(std::is_enum_v<T>, "EnumSets only works with enums.");
  static_assert(std::is_signed_v<ElementType> == false,
                "EnumSet doesn't supports signed enums.");

  // Each bucket can hold up to `kBucketSize` distinct, contiguous enum values.
  // The first value a bucket can hold must be aligned on `kBucketSize`.
  struct Bucket {
    // bit mask to store `kBucketSize` enums.
    BucketType data;
    // 1st enum this bucket can represent.
    T start;

    bool operator==(const Bucket& other) const {
      return start == other.start && data == other.data;
    }
  };

  // How many distinct values can a bucket hold? 1 bit per value.
  static constexpr size_t kBucketSize = sizeof(BucketType) * 8ULL;

 public:
  class Iterator {
   public:
    typedef Iterator self_type;
    typedef T value_type;
    typedef T& reference;
    typedef T* pointer;
    typedef std::forward_iterator_tag iterator_category;
    typedef size_t difference_type;

    Iterator(const Iterator& other)
        : set_(other.set_),
          bucketIndex_(other.bucketIndex_),
          bucketOffset_(other.bucketOffset_) {}

    Iterator& operator++() {
      do {
        if (bucketIndex_ >= set_->buckets_.size()) {
          bucketIndex_ = set_->buckets_.size();
          bucketOffset_ = 0;
          break;
        }

        if (bucketOffset_ + 1 == kBucketSize) {
          bucketOffset_ = 0;
          ++bucketIndex_;
        } else {
          ++bucketOffset_;
        }

      } while (bucketIndex_ < set_->buckets_.size() &&
               !set_->HasEnumAt(bucketIndex_, bucketOffset_));
      return *this;
    }

    Iterator operator++(int) {
      Iterator old = *this;
      operator++();
      return old;
    }

    T operator*() const {
      assert(set_->HasEnumAt(bucketIndex_, bucketOffset_) &&
             "operator*() called on an invalid iterator.");
      return GetValueFromBucket(set_->buckets_[bucketIndex_], bucketOffset_);
    }

    bool operator!=(const Iterator& other) const {
      return set_ != other.set_ || bucketOffset_ != other.bucketOffset_ ||
             bucketIndex_ != other.bucketIndex_;
    }

    bool operator==(const Iterator& other) const {
      return !(operator!=(other));
    }

    Iterator& operator=(const Iterator& other) {
      set_ = other.set_;
      bucketIndex_ = other.bucketIndex_;
      bucketOffset_ = other.bucketOffset_;
      return *this;
    }

   private:
    Iterator(const EnumSet* set, size_t bucketIndex, ElementType bucketOffset)
        : set_(set), bucketIndex_(bucketIndex), bucketOffset_(bucketOffset) {}

   private:
    const EnumSet* set_ = nullptr;
    // Index of the bucket in the vector.
    size_t bucketIndex_ = 0;
    // Offset in bits in the current bucket.
    ElementType bucketOffset_ = 0;

    friend class EnumSet;
  };

  // Required to allow the use of std::inserter.
  using value_type = T;
  using const_iterator = Iterator;
  using iterator = Iterator;

 public:
  iterator cbegin() const noexcept {
    auto it = iterator(this, /* bucketIndex= */ 0, /* bucketOffset= */ 0);
    if (buckets_.size() == 0) {
      return it;
    }

    // The iterator has the logic to find the next valid bit. If the value 0
    // is not stored, use it to find the next valid bit.
    if (!HasEnumAt(it.bucketIndex_, it.bucketOffset_)) {
      ++it;
    }

    return it;
  }

  iterator begin() const noexcept { return cbegin(); }

  iterator cend() const noexcept {
    return iterator(this, buckets_.size(), /* bucketOffset= */ 0);
  }

  iterator end() const noexcept { return cend(); }

  // Creates an empty set.
  EnumSet() : buckets_(0), size_(0) {}

  // Creates a set and store `value` in it.
  EnumSet(T value) : EnumSet() { insert(value); }

  // Creates a set and stores each `values` in it.
  EnumSet(std::initializer_list<T> values) : EnumSet() {
    for (auto item : values) {
      insert(item);
    }
  }

  // Creates a set, and insert `count` enum values pointed by `array` in it.
  EnumSet(ElementType count, const T* array) : EnumSet() {
    for (ElementType i = 0; i < count; i++) {
      insert(array[i]);
    }
  }

  // Creates a set initialized with the content of the range [begin; end[.
  template <class InputIt>
  EnumSet(InputIt begin, InputIt end) : EnumSet() {
    for (; begin != end; ++begin) {
      insert(*begin);
    }
  }

  // Copies the EnumSet `other` into a new EnumSet.
  EnumSet(const EnumSet& other)
      : buckets_(other.buckets_), size_(other.size_) {}

  // Moves the EnumSet `other` into a new EnumSet.
  EnumSet(EnumSet&& other)
      : buckets_(std::move(other.buckets_)), size_(other.size_) {}

  // Deep-copies the EnumSet `other` into this EnumSet.
  EnumSet& operator=(const EnumSet& other) {
    buckets_ = other.buckets_;
    size_ = other.size_;
    return *this;
  }

  // Matches std::unordered_set::insert behavior.
  std::pair<iterator, bool> insert(const T& value) {
    const size_t index = FindBucketForValue(value);
    const ElementType offset = ComputeBucketOffset(value);

    if (index >= buckets_.size() ||
        buckets_[index].start != ComputeBucketStart(value)) {
      size_ += 1;
      InsertBucketFor(index, value);
      return std::make_pair(Iterator(this, index, offset), true);
    }

    auto& bucket = buckets_[index];
    const auto mask = ComputeMaskForValue(value);
    if (bucket.data & mask) {
      return std::make_pair(Iterator(this, index, offset), false);
    }

    size_ += 1;
    bucket.data |= ComputeMaskForValue(value);
    return std::make_pair(Iterator(this, index, offset), true);
  }

  // Inserts `value` in the set if possible.
  // Similar to `std::unordered_set::insert`, except the hint is ignored.
  // Returns an iterator to the inserted element, or the element preventing
  // insertion.
  iterator insert(const_iterator, const T& value) {
    return insert(value).first;
  }

  // Inserts `value` in the set if possible.
  // Similar to `std::unordered_set::insert`, except the hint is ignored.
  // Returns an iterator to the inserted element, or the element preventing
  // insertion.
  iterator insert(const_iterator, T&& value) { return insert(value).first; }

  // Inserts all the values in the range [`first`; `last[.
  // Similar to `std::unordered_set::insert`.
  template <class InputIt>
  void insert(InputIt first, InputIt last) {
    for (auto it = first; it != last; ++it) {
      insert(*it);
    }
  }

  // Removes the value `value` into the set.
  // Similar to `std::unordered_set::erase`.
  // Returns the number of erased elements.
  size_t erase(const T& value) {
    const size_t index = FindBucketForValue(value);
    if (index >= buckets_.size() ||
        buckets_[index].start != ComputeBucketStart(value)) {
      return 0;
    }

    auto& bucket = buckets_[index];
    const auto mask = ComputeMaskForValue(value);
    if (!(bucket.data & mask)) {
      return 0;
    }

    size_ -= 1;
    bucket.data &= ~mask;
    if (bucket.data == 0) {
      buckets_.erase(buckets_.cbegin() + index);
    }
    return 1;
  }

  // Returns true if `value` is present in the set.
  bool contains(T value) const {
    const size_t index = FindBucketForValue(value);
    if (index >= buckets_.size() ||
        buckets_[index].start != ComputeBucketStart(value)) {
      return false;
    }
    auto& bucket = buckets_[index];
    return bucket.data & ComputeMaskForValue(value);
  }

  // Returns the 1 if `value` is present in the set, `0` otherwise.
  inline size_t count(T value) const { return contains(value) ? 1 : 0; }

  // Returns true if the set is holds no values.
  inline bool empty() const { return size_ == 0; }

  // Returns the number of enums stored in this set.
  size_t size() const { return size_; }

  // Returns true if this set contains at least one value contained in `in_set`.
  // Note: If `in_set` is empty, this function returns true.
  bool HasAnyOf(const EnumSet<T>& in_set) const {
    if (in_set.empty()) {
      return true;
    }

    auto lhs = buckets_.cbegin();
    auto rhs = in_set.buckets_.cbegin();

    while (lhs != buckets_.cend() && rhs != in_set.buckets_.cend()) {
      if (lhs->start == rhs->start) {
        if (lhs->data & rhs->data) {
          // At least 1 bit is shared. Early return.
          return true;
        }

        lhs++;
        rhs++;
        continue;
      }

      // LHS bucket is smaller than the current RHS bucket. Catching up on RHS.
      if (lhs->start < rhs->start) {
        lhs++;
        continue;
      }

      // Otherwise, RHS needs to catch up on LHS.
      rhs++;
    }

    return false;
  }

 private:
  // Returns the index of the last bucket in which `value` could be stored.
  static constexpr inline size_t ComputeLargestPossibleBucketIndexFor(T value) {
    return static_cast<size_t>(value) / kBucketSize;
  }

  // Returns the smallest enum value that could be contained in the same bucket
  // as `value`.
  static constexpr inline T ComputeBucketStart(T value) {
    return static_cast<T>(kBucketSize *
                          ComputeLargestPossibleBucketIndexFor(value));
  }

  //  Returns the index of the bit that corresponds to `value` in the bucket.
  static constexpr inline ElementType ComputeBucketOffset(T value) {
    return static_cast<ElementType>(value) % kBucketSize;
  }

  // Returns the bitmask used to represent the enum `value` in its bucket.
  static constexpr inline BucketType ComputeMaskForValue(T value) {
    return 1ULL << ComputeBucketOffset(value);
  }

  // Returns the `enum` stored in `bucket` at `offset`.
  // `offset` is the bit-offset in the bucket storage.
  static constexpr inline T GetValueFromBucket(const Bucket& bucket,
                                               BucketType offset) {
    return static_cast<T>(static_cast<ElementType>(bucket.start) + offset);
  }

  // For a given enum `value`, finds the bucket index that could contain this
  // value. If no such bucket is found, the index at which the new bucket should
  // be inserted is returned.
  size_t FindBucketForValue(T value) const {
    // Set is empty, insert at 0.
    if (buckets_.size() == 0) {
      return 0;
    }

    const T wanted_start = ComputeBucketStart(value);
    assert(buckets_.size() > 0 &&
           "Size must not be 0 here. Has the code above changed?");
    size_t index = std::min(buckets_.size() - 1,
                            ComputeLargestPossibleBucketIndexFor(value));

    // This loops behaves like std::upper_bound with a reverse iterator.
    // Buckets are sorted. 3 main cases:
    //  - The bucket matches
    //    => returns the bucket index.
    //  - The found bucket is larger
    //    => scans left until it finds the correct bucket, or insertion point.
    //  - The found bucket is smaller
    //    => We are at the end, so we return past-end index for insertion.
    for (; buckets_[index].start >= wanted_start; index--) {
      if (index == 0) {
        return 0;
      }
    }

    return index + 1;
  }

  // Creates a new bucket to store `value` and inserts it at `index`.
  // If the `index` is past the end, the bucket is inserted at the end of the
  // vector.
  void InsertBucketFor(size_t index, T value) {
    const T bucket_start = ComputeBucketStart(value);
    Bucket bucket = {1ULL << ComputeBucketOffset(value), bucket_start};
    auto it = buckets_.emplace(buckets_.begin() + index, std::move(bucket));
#if defined(NDEBUG)
    (void)it;  // Silencing unused variable warning.
#else
    assert(std::next(it) == buckets_.end() ||
           std::next(it)->start > bucket_start);
    assert(it == buckets_.begin() || std::prev(it)->start < bucket_start);
#endif
  }

  // Returns true if the bucket at `bucketIndex/ stores the enum at
  // `bucketOffset`, false otherwise.
  bool HasEnumAt(size_t bucketIndex, BucketType bucketOffset) const {
    assert(bucketIndex < buckets_.size());
    assert(bucketOffset < kBucketSize);
    return buckets_[bucketIndex].data & (1ULL << bucketOffset);
  }

  // Returns true if `lhs` and `rhs` hold the exact same values.
  friend bool operator==(const EnumSet& lhs, const EnumSet& rhs) {
    if (lhs.size_ != rhs.size_) {
      return false;
    }

    if (lhs.buckets_.size() != rhs.buckets_.size()) {
      return false;
    }
    return lhs.buckets_ == rhs.buckets_;
  }

  // Returns true if `lhs` and `rhs` hold at least 1 different value.
  friend bool operator!=(const EnumSet& lhs, const EnumSet& rhs) {
    return !(lhs == rhs);
  }

  // Storage for the buckets.
  std::vector<Bucket> buckets_;
  // How many enums is this set storing.
  size_t size_ = 0;
};

// A set of spv::Capability.
using CapabilitySet = EnumSet<spv::Capability>;

}  // namespace spvtools

#endif  // SOURCE_ENUM_SET_H_
