//============================================================================
//  Copyright (c) Kitware, Inc.
//  All rights reserved.
//  See LICENSE.txt for details.
//
//  This software is distributed WITHOUT ANY WARRANTY; without even
//  the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
//  PURPOSE.  See the above copyright notice for more information.
//============================================================================
#ifndef vtk_m_cont_ArrayHandleRuntimeVec_h
#define vtk_m_cont_ArrayHandleRuntimeVec_h

#include <vtkm/cont/ArrayExtractComponent.h>
#include <vtkm/cont/ArrayHandle.h>
#include <vtkm/cont/ArrayHandleBasic.h>
#include <vtkm/cont/ArrayPortal.h>
#include <vtkm/cont/ErrorBadType.h>

#include <vtkm/Assert.h>
#include <vtkm/StaticAssert.h>
#include <vtkm/VecFromPortal.h>
#include <vtkm/VecTraits.h>

namespace vtkm
{
namespace internal
{

template <typename ComponentsPortalType>
class VTKM_ALWAYS_EXPORT ArrayPortalRuntimeVec
{
public:
  using ComponentType = typename std::remove_const<typename ComponentsPortalType::ValueType>::type;
  using ValueType = vtkm::VecFromPortal<ComponentsPortalType>;

  ArrayPortalRuntimeVec() = default;

  VTKM_EXEC_CONT ArrayPortalRuntimeVec(const ComponentsPortalType& componentsPortal,
                                       vtkm::IdComponent numComponents)
    : ComponentsPortal(componentsPortal)
    , NumberOfComponents(numComponents)
  {
  }

  /// Copy constructor for any other ArrayPortalRuntimeVec with a portal type
  /// that can be copied to this portal type. This allows us to do any type
  /// casting that the portals do (like the non-const to const cast).
  template <typename OtherComponentsPortalType>
  VTKM_EXEC_CONT ArrayPortalRuntimeVec(const ArrayPortalRuntimeVec<OtherComponentsPortalType>& src)
    : ComponentsPortal(src.GetComponentsPortal())
    , NumberOfComponents(src.GetNumberOfComponents())
  {
  }

  VTKM_EXEC_CONT vtkm::Id GetNumberOfValues() const
  {
    return this->ComponentsPortal.GetNumberOfValues() / this->NumberOfComponents;
  }

  VTKM_EXEC_CONT ValueType Get(vtkm::Id index) const
  {
    return ValueType(
      this->ComponentsPortal, this->NumberOfComponents, index * this->NumberOfComponents);
  }

  VTKM_EXEC_CONT void Set(vtkm::Id index, const ValueType& value) const
  {
    if ((&value.GetPortal() == &this->ComponentsPortal) &&
        (value.GetOffset() == (index * this->NumberOfComponents)))
    {
      // The ValueType (VecFromPortal) operates on demand. Thus, if you set
      // something in the value, it has already been passed to the array.
    }
    else
    {
      // The value comes from somewhere else. Copy data in.
      this->Get(index) = value;
    }
  }

  VTKM_EXEC_CONT const ComponentsPortalType& GetComponentsPortal() const
  {
    return this->ComponentsPortal;
  }

  VTKM_EXEC_CONT vtkm::IdComponent GetNumberOfComponents() const
  {
    return this->NumberOfComponents;
  }

private:
  ComponentsPortalType ComponentsPortal;
  vtkm::IdComponent NumberOfComponents = 0;
};

}
} // namespace vtkm::internal

namespace vtkm
{
namespace cont
{

struct VTKM_ALWAYS_EXPORT StorageTagRuntimeVec
{
};

namespace internal
{

template <typename ComponentsPortal>
class Storage<vtkm::VecFromPortal<ComponentsPortal>, vtkm::cont::StorageTagRuntimeVec>
{
  using ComponentType = typename ComponentsPortal::ValueType;
  using ComponentsStorage =
    vtkm::cont::internal::Storage<ComponentType, vtkm::cont::StorageTagBasic>;

  using ComponentsArray = vtkm::cont::ArrayHandle<ComponentType, StorageTagBasic>;

  VTKM_STATIC_ASSERT_MSG(
    (std::is_same<ComponentsPortal, typename ComponentsStorage::WritePortalType>::value),
    "Used invalid ComponentsPortal type with expected ComponentsStorageTag.");

  struct Info
  {
    vtkm::IdComponent NumberOfComponents;
  };

  VTKM_CONT static std::vector<vtkm::cont::internal::Buffer> ComponentsBuffers(
    const std::vector<vtkm::cont::internal::Buffer>& buffers)
  {
    return std::vector<vtkm::cont::internal::Buffer>(buffers.begin() + 1, buffers.end());
  }

public:
  using ReadPortalType =
    vtkm::internal::ArrayPortalRuntimeVec<typename ComponentsStorage::ReadPortalType>;
  using WritePortalType =
    vtkm::internal::ArrayPortalRuntimeVec<typename ComponentsStorage::WritePortalType>;

  VTKM_CONT static vtkm::IdComponent GetNumberOfComponents(
    const std::vector<vtkm::cont::internal::Buffer>& buffers)
  {
    return buffers[0].GetMetaData<Info>().NumberOfComponents;
  }

  VTKM_CONT static vtkm::Id GetNumberOfValues(
    const std::vector<vtkm::cont::internal::Buffer>& buffers)
  {
    return ComponentsStorage::GetNumberOfValues(ComponentsBuffers(buffers)) /
      GetNumberOfComponents(buffers);
  }

  VTKM_CONT static void ResizeBuffers(vtkm::Id numValues,
                                      const std::vector<vtkm::cont::internal::Buffer>& buffers,
                                      vtkm::CopyFlag preserve,
                                      vtkm::cont::Token& token)
  {
    ComponentsStorage::ResizeBuffers(
      numValues * GetNumberOfComponents(buffers), ComponentsBuffers(buffers), preserve, token);
  }

  VTKM_CONT static void Fill(const std::vector<vtkm::cont::internal::Buffer>&,
                             const vtkm::VecFromPortal<ComponentsPortal>&,
                             vtkm::Id,
                             vtkm::Id,
                             vtkm::cont::Token&)
  {
    throw vtkm::cont::ErrorBadType("Fill not supported for ArrayHandleRuntimeVec.");
  }

  VTKM_CONT static ReadPortalType CreateReadPortal(
    const std::vector<vtkm::cont::internal::Buffer>& buffers,
    vtkm::cont::DeviceAdapterId device,
    vtkm::cont::Token& token)
  {
    return ReadPortalType(
      ComponentsStorage::CreateReadPortal(ComponentsBuffers(buffers), device, token),
      GetNumberOfComponents(buffers));
  }

  VTKM_CONT static WritePortalType CreateWritePortal(
    const std::vector<vtkm::cont::internal::Buffer>& buffers,
    vtkm::cont::DeviceAdapterId device,
    vtkm::cont::Token& token)
  {
    return WritePortalType(
      ComponentsStorage::CreateWritePortal(ComponentsBuffers(buffers), device, token),
      GetNumberOfComponents(buffers));
  }

  VTKM_CONT static std::vector<vtkm::cont::internal::Buffer> CreateBuffers(
    vtkm::IdComponent numComponents = 1,
    const ComponentsArray& componentsArray = ComponentsArray{})
  {
    VTKM_LOG_IF_S(vtkm::cont::LogLevel::Warn,
                  (componentsArray.GetNumberOfValues() % numComponents) != 0,
                  "Array given to ArrayHandleRuntimeVec has size ("
                    << componentsArray.GetNumberOfValues()
                    << ") that is not divisible by the number of components selected ("
                    << numComponents << ").");
    Info info;
    info.NumberOfComponents = numComponents;
    return vtkm::cont::internal::CreateBuffers(info, componentsArray);
  }

  VTKM_CONT static ComponentsArray GetComponentsArray(
    const std::vector<vtkm::cont::internal::Buffer>& buffers)
  {
    return ComponentsArray(ComponentsBuffers(buffers));
  }

  VTKM_CONT static void AsArrayHandleBasic(
    const std::vector<vtkm::cont::internal::Buffer>& buffers,
    vtkm::cont::ArrayHandle<ComponentType, vtkm::cont::StorageTagBasic>& dest)
  {
    if (GetNumberOfComponents(buffers) != 1)
    {
      throw vtkm::cont::ErrorBadType(
        "Attempted to pull a scalar array from an ArrayHandleRuntime that does not hold scalars.");
    }
    dest = GetComponentsArray(buffers);
  }

  template <vtkm::IdComponent N>
  VTKM_CONT static void AsArrayHandleBasic(
    const std::vector<vtkm::cont::internal::Buffer>& buffers,
    vtkm::cont::ArrayHandle<vtkm::Vec<ComponentType, N>, vtkm::cont::StorageTagBasic>& dest)
  {
    if (GetNumberOfComponents(buffers) != N)
    {
      throw vtkm::cont::ErrorBadType(
        "Attempted to pull an array of Vecs of the wrong size from an ArrayHandleRuntime.");
    }
    dest = vtkm::cont::ArrayHandle<vtkm::Vec<ComponentType, N>, vtkm::cont::StorageTagBasic>(
      ComponentsBuffers(buffers));
  }

  template <typename T, vtkm::IdComponent NInner, vtkm::IdComponent NOuter>
  VTKM_CONT static void AsArrayHandleBasic(
    const std::vector<vtkm::cont::internal::Buffer>& buffers,
    vtkm::cont::ArrayHandle<vtkm::Vec<vtkm::Vec<T, NInner>, NOuter>, vtkm::cont::StorageTagBasic>&
      dest)
  {
    // Flatten the Vec by one level and attempt to get the array handle for that.
    vtkm::cont::ArrayHandleBasic<vtkm::Vec<T, NInner * NOuter>> squashedArray;
    AsArrayHandleBasic(buffers, squashedArray);
    // Now unsquash the array by stealling the buffers and creating an array of the right type
    dest =
      vtkm::cont::ArrayHandle<vtkm::Vec<vtkm::Vec<T, NInner>, NOuter>, vtkm::cont::StorageTagBasic>(
        squashedArray.GetBuffers());
  }
};

} // namespace internal

/// \brief Fancy array handle for a basic array with runtime selected vec size.
///
/// It is sometimes the case that you need to create an array of `Vec`s where
/// the number of components is not known until runtime. This is problematic
/// for normal `ArrayHandle`s because you have to specify the size of the `Vec`s
/// as a template parameter at compile time. `ArrayHandleRuntimeVec` can be used
/// in this case.
///
/// Note that caution should be used with `ArrayHandleRuntimeVec` because the
/// size of the `Vec` values is not known at compile time. Thus, the value
/// type of this array is forced to a special `VecFromPortal` class that can cause
/// surprises if treated as a `Vec`. In particular, the static `NUM_COMPONENTS`
/// expression does not exist. Furthermore, new variables of type `VecFromPortal`
/// cannot be created. This means that simple operators like `+` will not work
/// because they require an intermediate object to be created. (Equal operators
/// like `+=` do work because they are given an existing variable to place the
/// output.)
///
/// It is possible to provide an `ArrayHandleBasic` of the same component
/// type as the underlying storage for this array. In this case, the array
/// will be accessed much in the same manner as `ArrayHandleGroupVec`.
///
/// `ArrayHandleRuntimeVec` also allows you to convert the array to an
/// `ArrayHandleBasic` of the appropriate `Vec` type (or `component` type).
/// A runtime check will be performed to make sure the number of components
/// matches.
///
template <typename ComponentType>
class ArrayHandleRuntimeVec
  : public vtkm::cont::ArrayHandle<
      vtkm::VecFromPortal<typename ArrayHandleBasic<ComponentType>::WritePortalType>,
      vtkm::cont::StorageTagRuntimeVec>
{
public:
  VTKM_ARRAY_HANDLE_SUBCLASS(
    ArrayHandleRuntimeVec,
    (ArrayHandleRuntimeVec<ComponentType>),
    (vtkm::cont::ArrayHandle<
      vtkm::VecFromPortal<typename ArrayHandleBasic<ComponentType>::WritePortalType>,
      vtkm::cont::StorageTagRuntimeVec>));

private:
  using StorageType = vtkm::cont::internal::Storage<ValueType, StorageTag>;
  using ComponentsArrayType = vtkm::cont::ArrayHandle<ComponentType, StorageTagBasic>;

public:
  VTKM_CONT
  ArrayHandleRuntimeVec(vtkm::IdComponent numComponents,
                        const ComponentsArrayType& componentsArray = ComponentsArrayType{})
    : Superclass(StorageType::CreateBuffers(numComponents, componentsArray))
  {
  }

  VTKM_CONT vtkm::IdComponent GetNumberOfComponents() const
  {
    return StorageType::GetNumberOfComponents(this->GetBuffers());
  }

  VTKM_CONT vtkm::cont::ArrayHandleBasic<ComponentType> GetComponentsArray() const
  {
    return StorageType::GetComponentsArray(this->GetBuffers());
  }

  ///@{
  /// \brief Converts the array to that of a basic array handle.
  ///
  /// This method converts the `ArrayHandleRuntimeVec` to a simple `ArrayHandleBasic`.
  /// This is useful if the `ArrayHandleRuntimeVec` is passed to a routine that works
  /// on an array of a specific `Vec` size (or scalars). After a runtime check, the
  /// array can be converted to a typical array and used as such.
  template <typename ValueType>
  void AsArrayHandleBasic(vtkm::cont::ArrayHandle<ValueType>& array) const
  {
    StorageType::AsArrayHandleBasic(this->GetBuffers(), array);
  }

  template <typename ArrayType>
  ArrayType AsArrayHandleBasic() const
  {
    ArrayType array;
    this->AsArrayHandleBasic(array);
    return array;
  }
  ///@}
};

/// \c make_ArrayHandleRuntimeVec is convenience function to generate an
/// ArrayHandleRuntimeVec. It takes in an ArrayHandle of values and an
/// array handle of offsets and returns an array handle with consecutive
/// entries grouped in a Vec.
///
template <typename T>
VTKM_CONT vtkm::cont::ArrayHandleRuntimeVec<T> make_ArrayHandleRuntimeVec(
  vtkm::IdComponent numComponents,
  const vtkm::cont::ArrayHandle<T, vtkm::cont::StorageTagBasic>& componentsArray)
{
  return vtkm::cont::ArrayHandleRuntimeVec<T>(numComponents, componentsArray);
}

namespace internal
{

template <>
struct ArrayExtractComponentImpl<vtkm::cont::StorageTagRuntimeVec>
{
  template <typename T>
  auto operator()(const vtkm::cont::ArrayHandle<T, vtkm::cont::StorageTagRuntimeVec>& src,
                  vtkm::IdComponent componentIndex,
                  vtkm::CopyFlag allowCopy) const
  {
    using ComponentType = typename T::ComponentType;
    vtkm::cont::ArrayHandleRuntimeVec<ComponentType> array{ src };
    constexpr vtkm::IdComponent NUM_SUB_COMPONENTS = vtkm::VecFlat<ComponentType>::NUM_COMPONENTS;
    vtkm::cont::ArrayHandleStride<typename vtkm::VecTraits<T>::BaseComponentType> dest =
      ArrayExtractComponentImpl<vtkm::cont::StorageTagBasic>{}(
        array.GetComponentsArray(), componentIndex % NUM_SUB_COMPONENTS, allowCopy);

    // Adjust stride and offset to expectations of grouped values
    const vtkm::IdComponent numComponents = array.GetNumberOfComponents();
    return vtkm::cont::ArrayHandleStride<typename vtkm::VecTraits<T>::BaseComponentType>(
      dest.GetBasicArray(),
      dest.GetNumberOfValues() / numComponents,
      dest.GetStride() * numComponents,
      dest.GetOffset() + (dest.GetStride() * (componentIndex / NUM_SUB_COMPONENTS)),
      dest.GetModulo(),
      dest.GetDivisor());
  }
};

} // namespace internal

}
} // namespace vtkm::cont

//=============================================================================
// Specializations of serialization related classes
/// @cond SERIALIZATION
namespace vtkm
{
namespace cont
{

template <typename T>
struct SerializableTypeString<vtkm::cont::ArrayHandleRuntimeVec<T>>
{
  static VTKM_CONT const std::string& Get()
  {
    static std::string name = "AH_RuntimeVec<" + SerializableTypeString<T>::Get() + ">";
    return name;
  }
};

template <typename VecType>
struct SerializableTypeString<vtkm::cont::ArrayHandle<VecType, vtkm::cont::StorageTagRuntimeVec>>
  : SerializableTypeString<vtkm::cont::ArrayHandleRuntimeVec<typename VecType::ComponentType>>
{
};

}
} // vtkm::cont

namespace mangled_diy_namespace
{

template <typename T>
struct Serialization<vtkm::cont::ArrayHandleRuntimeVec<T>>
{
private:
  using Type = vtkm::cont::ArrayHandleRuntimeVec<T>;
  using BaseType = vtkm::cont::ArrayHandle<typename Type::ValueType, typename Type::StorageTag>;

public:
  static VTKM_CONT void save(BinaryBuffer& bb, const BaseType& obj)
  {
    vtkmdiy::save(bb, Type(obj).GetNumberOfComponents());
    vtkmdiy::save(bb, Type(obj).GetComponentsArray());
  }

  static VTKM_CONT void load(BinaryBuffer& bb, BaseType& obj)
  {
    vtkm::IdComponent numComponents;
    vtkm::cont::ArrayHandleBasic<T> componentArray;

    vtkmdiy::load(bb, numComponents);
    vtkmdiy::load(bb, componentArray);

    obj = vtkm::cont::make_ArrayHandleRuntimeVec(numComponents, componentArray);
  }
};

template <typename VecType>
struct Serialization<vtkm::cont::ArrayHandle<VecType, vtkm::cont::StorageTagRuntimeVec>>
  : Serialization<vtkm::cont::ArrayHandleRuntimeVec<typename VecType::ComponentType>>
{
};

} // diy
/// @endcond SERIALIZATION

#endif //vtk_m_cont_ArrayHandleRuntimeVec_h
