// -*- C++ -*-
//===----------------------------------------------------------------------===//
//
// Part of the LLVM Project, 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
//
//                        Kokkos v. 4.0
//       Copyright (2022) National Technology & Engineering
//               Solutions of Sandia, LLC (NTESS).
//
// Under the terms of Contract DE-NA0003525 with NTESS,
// the U.S. Government retains certain rights in this software.
//
//===---------------------------------------------------------------------===//

#ifndef TEST_STD_CONTAINERS_VIEWS_MDSPAN_CUSTOM_TEST_LAYOUTS_H
#define TEST_STD_CONTAINERS_VIEWS_MDSPAN_CUSTOM_TEST_LAYOUTS_H

#include <algorithm>
#include <array>
#include <cassert>
#include <cinttypes>
#include <concepts>
#include <cstddef>
#include <limits>
#include <mdspan>
#include <type_traits>
#include <utility>

// Layout that wraps indices to test some idiosyncratic behavior
// - basically it is a layout_left where indicies are first wrapped i.e. i%Wrap
// - only accepts integers as indices
// - is_always_strided and is_always_unique are false
// - is_strided and is_unique are true if all extents are smaller than Wrap
// - not default constructible
// - not extents constructible
// - not trivially copyable
// - does not check dynamic to static extent conversion in converting ctor
// - check via side-effects that mdspan::swap calls mappings swap via ADL

struct not_extents_constructible_tag {};

template <size_t Wrap>
class layout_wrapping_integral {
public:
  template <class Extents>
  class mapping;
};

template <size_t WrapArg>
template <class Extents>
class layout_wrapping_integral<WrapArg>::mapping {
  static constexpr typename Extents::index_type Wrap = static_cast<typename Extents::index_type>(WrapArg);

public:
  using extents_type = Extents;
  using index_type   = typename extents_type::index_type;
  using size_type    = typename extents_type::size_type;
  using rank_type    = typename extents_type::rank_type;
  using layout_type  = layout_wrapping_integral<Wrap>;

private:
  static constexpr bool required_span_size_is_representable(const extents_type& ext) {
    if constexpr (extents_type::rank() == 0)
      return true;

    index_type prod = ext.extent(0);
    for (rank_type r = 1; r < extents_type::rank(); r++) {
      bool overflowed = __builtin_mul_overflow(prod, std::min(ext.extent(r), Wrap), &prod);
      if (overflowed)
        return false;
    }
    return true;
  }

public:
  constexpr mapping() noexcept = delete;
  constexpr mapping(const mapping& other) noexcept : extents_(other.extents()) {}
  constexpr mapping(extents_type&& ext) noexcept
    requires(Wrap == 8)
      : extents_(ext) {}
  constexpr mapping(const extents_type& ext, not_extents_constructible_tag) noexcept : extents_(ext) {}

  template <class OtherExtents>
    requires(std::is_constructible_v<extents_type, OtherExtents> && (Wrap != 8))
  constexpr explicit(!std::is_convertible_v<OtherExtents, extents_type>)
      mapping(const mapping<OtherExtents>& other) noexcept {
    std::array<index_type, extents_type::rank_dynamic()> dyn_extents;
    rank_type count = 0;
    for (rank_type r = 0; r < extents_type::rank(); r++) {
      if (extents_type::static_extent(r) == std::dynamic_extent) {
        dyn_extents[count++] = other.extents().extent(r);
      }
    }
    extents_ = extents_type(dyn_extents);
  }
  template <class OtherExtents>
    requires(std::is_constructible_v<extents_type, OtherExtents> && (Wrap == 8))
  constexpr explicit(!std::is_convertible_v<OtherExtents, extents_type>)
      mapping(mapping<OtherExtents>&& other) noexcept {
    std::array<index_type, extents_type::rank_dynamic()> dyn_extents;
    rank_type count = 0;
    for (rank_type r = 0; r < extents_type::rank(); r++) {
      if (extents_type::static_extent(r) == std::dynamic_extent) {
        dyn_extents[count++] = other.extents().extent(r);
      }
    }
    extents_ = extents_type(dyn_extents);
  }

  constexpr mapping& operator=(const mapping& other) noexcept {
    extents_ = other.extents_;
    return *this;
  };

  constexpr const extents_type& extents() const noexcept { return extents_; }

  constexpr index_type required_span_size() const noexcept {
    index_type size = 1;
    for (size_t r = 0; r < extents_type::rank(); r++)
      size *= extents_.extent(r) < Wrap ? extents_.extent(r) : Wrap;
    return size;
  }

  template <std::integral... Indices>
    requires((sizeof...(Indices) == extents_type::rank()) && (std::is_convertible_v<Indices, index_type> && ...) &&
             (std::is_nothrow_constructible_v<index_type, Indices> && ...))
  constexpr index_type operator()(Indices... idx) const noexcept {
    std::array<index_type, extents_type::rank()> idx_a{static_cast<index_type>(static_cast<index_type>(idx) % Wrap)...};
    return [&]<size_t... Pos>(std::index_sequence<Pos...>) {
      index_type res = 0;
      ((res = idx_a[extents_type::rank() - 1 - Pos] +
              (extents_.extent(extents_type::rank() - 1 - Pos) < Wrap ? extents_.extent(extents_type::rank() - 1 - Pos)
                                                                      : Wrap) *
                  res),
       ...);
      return res;
    }(std::make_index_sequence<sizeof...(Indices)>());
  }

  static constexpr bool is_always_unique() noexcept { return false; }
  static constexpr bool is_always_exhaustive() noexcept { return true; }
  static constexpr bool is_always_strided() noexcept { return false; }

  constexpr bool is_unique() const noexcept {
    for (rank_type r = 0; r < extents_type::rank(); r++) {
      if (extents_.extent(r) > Wrap)
        return false;
    }
    return true;
  }
  static constexpr bool is_exhaustive() noexcept { return true; }
  constexpr bool is_strided() const noexcept {
    for (rank_type r = 0; r < extents_type::rank(); r++) {
      if (extents_.extent(r) > Wrap)
        return false;
    }
    return true;
  }

  constexpr index_type stride(rank_type r) const noexcept
    requires(extents_type::rank() > 0)
  {
    index_type s = 1;
    for (rank_type i = extents_type::rank() - 1; i > r; i--)
      s *= extents_.extent(i);
    return s;
  }

  template <class OtherExtents>
    requires(OtherExtents::rank() == extents_type::rank())
  friend constexpr bool operator==(const mapping& lhs, const mapping<OtherExtents>& rhs) noexcept {
    return lhs.extents() == rhs.extents();
  }

  friend constexpr void swap(mapping& x, mapping& y) noexcept {
    swap(x.extents_, y.extents_);
    if (!std::is_constant_evaluated()) {
      swap_counter()++;
    }
  }

  static int& swap_counter() {
    static int value = 0;
    return value;
  }

private:
  extents_type extents_{};
};

template <class Extents>
constexpr auto construct_mapping(std::layout_left, Extents exts) {
  return std::layout_left::mapping<Extents>(exts);
}

template <class Extents>
constexpr auto construct_mapping(std::layout_right, Extents exts) {
  return std::layout_right::mapping<Extents>(exts);
}

template <size_t Wraps, class Extents>
constexpr auto construct_mapping(layout_wrapping_integral<Wraps>, Extents exts) {
  return typename layout_wrapping_integral<Wraps>::template mapping<Extents>(exts, not_extents_constructible_tag{});
}

// This layout does not check convertibility of extents for its conversion ctor
// Allows triggering mdspan's ctor static assertion on convertibility of extents
// It also allows for negative strides and offsets via runtime arguments
class always_convertible_layout {
public:
  template <class Extents>
  class mapping;
};

template <class Extents>
class always_convertible_layout::mapping {
public:
  using extents_type = Extents;
  using index_type   = typename extents_type::index_type;
  using size_type    = typename extents_type::size_type;
  using rank_type    = typename extents_type::rank_type;
  using layout_type  = always_convertible_layout;

private:
  static constexpr bool required_span_size_is_representable(const extents_type& ext) {
    if constexpr (extents_type::rank() == 0)
      return true;

    index_type prod = ext.extent(0);
    for (rank_type r = 1; r < extents_type::rank(); r++) {
      bool overflowed = __builtin_mul_overflow(prod, ext.extent(r), &prod);
      if (overflowed)
        return false;
    }
    return true;
  }

public:
  constexpr mapping() noexcept = delete;
  constexpr mapping(const mapping& other) noexcept
      : extents_(other.extents_), offset_(other.offset_), scaling_(other.scaling_) {}
  constexpr mapping(const extents_type& ext, index_type offset = 0, index_type scaling = 1) noexcept
      : extents_(ext), offset_(offset), scaling_(scaling) {}

  template <class OtherExtents>
  constexpr mapping(const mapping<OtherExtents>& other) noexcept {
    if constexpr (extents_type::rank() == OtherExtents::rank()) {
      std::array<index_type, extents_type::rank_dynamic()> dyn_extents;
      rank_type count = 0;
      for (rank_type r = 0; r < extents_type::rank(); r++) {
        if (extents_type::static_extent(r) == std::dynamic_extent) {
          dyn_extents[count++] = other.extents().extent(r);
        }
      }
      extents_ = extents_type(dyn_extents);
    } else {
      extents_ = extents_type();
    }
    offset_  = other.offset_;
    scaling_ = other.scaling_;
  }

  constexpr mapping& operator=(const mapping& other) noexcept {
    extents_ = other.extents_;
    offset_  = other.offset_;
    scaling_ = other.scaling_;
    return *this;
  };

  constexpr const extents_type& extents() const noexcept { return extents_; }

  constexpr index_type required_span_size() const noexcept {
    index_type size = 1;
    for (size_t r = 0; r < extents_type::rank(); r++)
      size *= extents_.extent(r);
    return std::max(size * scaling_ + offset_, offset_);
  }

  template <std::integral... Indices>
    requires((sizeof...(Indices) == extents_type::rank()) && (std::is_convertible_v<Indices, index_type> && ...) &&
             (std::is_nothrow_constructible_v<index_type, Indices> && ...))
  constexpr index_type operator()(Indices... idx) const noexcept {
    std::array<index_type, extents_type::rank()> idx_a{static_cast<index_type>(static_cast<index_type>(idx))...};
    return offset_ +
           scaling_ * ([&]<size_t... Pos>(std::index_sequence<Pos...>) {
             index_type res = 0;
             ((res = idx_a[extents_type::rank() - 1 - Pos] + extents_.extent(extents_type::rank() - 1 - Pos) * res),
              ...);
             return res;
           }(std::make_index_sequence<sizeof...(Indices)>()));
  }

  static constexpr bool is_always_unique() noexcept { return true; }
  static constexpr bool is_always_exhaustive() noexcept { return true; }
  static constexpr bool is_always_strided() noexcept { return true; }

  static constexpr bool is_unique() noexcept { return true; }
  static constexpr bool is_exhaustive() noexcept { return true; }
  static constexpr bool is_strided() noexcept { return true; }

  constexpr index_type stride(rank_type r) const noexcept
    requires(extents_type::rank() > 0)
  {
    index_type s = 1;
    for (rank_type i = 0; i < r; i++)
      s *= extents_.extent(i);
    return s * scaling_;
  }

  template <class OtherExtents>
    requires(OtherExtents::rank() == extents_type::rank())
  friend constexpr bool operator==(const mapping& lhs, const mapping<OtherExtents>& rhs) noexcept {
    return lhs.extents() == rhs.extents() && lhs.offset_ == rhs.offset && lhs.scaling_ == rhs.scaling_;
  }

  friend constexpr void swap(mapping& x, mapping& y) noexcept {
    swap(x.extents_, y.extents_);
    if (!std::is_constant_evaluated()) {
      swap_counter()++;
    }
  }

  static int& swap_counter() {
    static int value = 0;
    return value;
  }

private:
  template <class>
  friend class mapping;

  extents_type extents_{};
  index_type offset_{};
  index_type scaling_{};
};
#endif // TEST_STD_CONTAINERS_VIEWS_MDSPAN_CUSTOM_TEST_LAYOUTS_H
