/* SPDX-FileCopyrightText: 2023 Blender Authors
 *
 * SPDX-License-Identifier: GPL-2.0-or-later */

#pragma once

/** \file
 * \ingroup bli
 *
 * This is a generic counterpart to #blender::Array, used when the type is not known at runtime.
 *
 * `GArray` should generally only be used for passing data around in dynamic contexts.
 * It does not support a few things that #blender::Array supports:
 *  - Small object optimization / inline buffer.
 *  - Exception safety and various more specific constructors.
 */

#include "BLI_allocator.hh"
#include "BLI_cpp_type.hh"
#include "BLI_generic_span.hh"

namespace blender {

template<
    /**
     * The allocator used by this array. Should rarely be changed, except when you don't want that
     * MEM_* functions are used internally.
     */
    typename Allocator = GuardedAllocator>
class GArray {
 protected:
  /** The type of the data in the array, will be null after the array is default constructed,
   * but a value should be assigned before any other interaction with the array. */
  const CPPType *type_ = nullptr;
  void *data_ = nullptr;
  int64_t size_ = 0;

  BLI_NO_UNIQUE_ADDRESS Allocator allocator_;

 public:
  /**
   * The default constructor creates an empty array, the only situation in which the type is
   * allowed to be null. This default constructor exists so `GArray` can be used in containers,
   * but the type should be supplied before doing anything else to the array.
   */
  GArray(Allocator allocator = {}) noexcept : allocator_(allocator) {}

  GArray(NoExceptConstructor, Allocator allocator = {}) noexcept : GArray(allocator) {}

  /**
   * Create and allocate a new array, with elements default constructed
   * (which does not do anything for trivial types).
   */
  GArray(const CPPType &type, int64_t size, Allocator allocator = {}) : GArray(type, allocator)
  {
    BLI_assert(size >= 0);
    size_ = size;
    data_ = this->allocate(size_);
    type_->default_construct_n(data_, size_);
  }

  /**
   * Create an empty array with just a type.
   */
  GArray(const CPPType &type, Allocator allocator = {}) : GArray(allocator)
  {
    type_ = &type;
  }

  /**
   * Take ownership of a buffer with a provided size. The buffer should be
   * allocated with the same allocator provided to the constructor.
   */
  GArray(const CPPType &type, void *buffer, int64_t size, Allocator allocator = {})
      : GArray(type, allocator)
  {
    BLI_assert(size >= 0);
    BLI_assert(buffer != nullptr || size == 0);
    BLI_assert(type_->pointer_has_valid_alignment(buffer));

    data_ = buffer;
    size_ = size;
  }

  /**
   * Create an array by copying values from a generic span.
   */
  GArray(const GSpan span, Allocator allocator = {}) : GArray(span.type(), span.size(), allocator)
  {
    /* Use copy assign rather than construct since the memory is already initialized. */
    type_->copy_assign_n(span.data(), data_, size_);
  }

  /**
   * Create an array by copying values from another generic array.
   */
  GArray(const GArray &other) : GArray(other.as_span(), other.allocator()) {}

  /**
   * Create an array by taking ownership of another array's data, clearing the data in the other.
   */
  GArray(GArray &&other)
      : type_(other.type_), data_(other.data_), size_(other.size_), allocator_(other.allocator_)
  {
    other.data_ = nullptr;
    other.size_ = 0;
  }

  ~GArray()
  {
    if (data_ != nullptr) {
      type_->destruct_n(data_, size_);
      this->deallocate(data_);
    }
  }

  GArray &operator=(const GArray &other)
  {
    return copy_assign_container(*this, other);
  }

  GArray &operator=(GArray &&other)
  {
    return move_assign_container(*this, std::move(other));
  }

  const CPPType &type() const
  {
    BLI_assert(type_ != nullptr);
    return *type_;
  }

  bool is_empty() const
  {
    return size_ == 0;
  }

  /**
   * Return the number of elements in the array (not the size in bytes).
   */
  int64_t size() const
  {
    return size_;
  }

  /**
   * Get a pointer to the beginning of the array.
   */
  const void *data() const
  {
    return data_;
  }
  void *data()
  {
    return data_;
  }

  const void *operator[](int64_t index) const
  {
    BLI_assert(index < size_);
    return POINTER_OFFSET(data_, type_->size * index);
  }

  void *operator[](int64_t index)
  {
    BLI_assert(index < size_);
    return POINTER_OFFSET(data_, type_->size * index);
  }

  operator GSpan() const
  {
    BLI_assert(size_ == 0 || type_ != nullptr);
    return GSpan(type_, data_, size_);
  }

  operator GMutableSpan()
  {
    BLI_assert(size_ == 0 || type_ != nullptr);
    return GMutableSpan(type_, data_, size_);
  }

  GSpan as_span() const
  {
    return *this;
  }

  GMutableSpan as_mutable_span()
  {
    return *this;
  }

  /**
   * Access the allocator used by this array.
   */
  Allocator &allocator()
  {
    return allocator_;
  }
  const Allocator &allocator() const
  {
    return allocator_;
  }

  /**
   * Destruct values and create a new array of the given size. The values in the new array are
   * default constructed.
   */
  void reinitialize(const int64_t new_size)
  {
    BLI_assert(new_size >= 0);
    int64_t old_size = size_;

    type_->destruct_n(data_, size_);
    size_ = 0;

    if (new_size <= old_size) {
      type_->default_construct_n(data_, new_size);
    }
    else {
      void *new_data = this->allocate(new_size);
      try {
        type_->default_construct_n(new_data, new_size);
      }
      catch (...) {
        this->deallocate(new_data);
        throw;
      }
      if (this->data_) {
        this->deallocate(data_);
      }
      data_ = new_data;
    }

    size_ = new_size;
  }

 private:
  void *allocate(int64_t size)
  {
    const int64_t item_size = type_->size;
    const int64_t alignment = type_->alignment;
    return allocator_.allocate(size_t(size) * item_size, alignment, AT);
  }

  void deallocate(void *ptr)
  {
    allocator_.deallocate(ptr);
  }
};

}  // namespace blender
