/* Copyright 2021 NVIDIA Corporation. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *  * Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 *  * Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *  * Neither the name of NVIDIA CORPORATION nor the names of its
 *    contributors may be used to endorse or promote products derived
 *    from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

#include "vtkCellData.h"
#include "vtkCellIterator.h"
#include "vtkUnstructuredGrid.h"
#include <vtkDataArray.h>

#include "vtknvindex_cluster_properties.h"
#include "vtknvindex_forwarding_logger.h"
#include "vtknvindex_host_properties.h"
#include "vtknvindex_regular_volume_properties.h"
#include "vtknvindex_utilities.h"

// ------------------------------------------------------------------------------------------------
vtknvindex_regular_volume_properties::vtknvindex_regular_volume_properties()
  : m_is_timeseries_data(false)
  , m_time_steps_written(0)
  , m_nb_time_steps(1)
  , m_current_time_step(0)
  , m_ghost_levels(0)

{
  m_volume_translation = mi::math::Vector<mi::Float32, 3>(0.f);
  m_volume_scaling = mi::math::Vector<mi::Float32, 3>(1.f);
  m_volume_extents.min = m_volume_extents.max = mi::math::Vector<mi::Sint32, 3>(0);
}

// ------------------------------------------------------------------------------------------------
vtknvindex_regular_volume_properties::~vtknvindex_regular_volume_properties() = default;

// ------------------------------------------------------------------------------------------------
void vtknvindex_regular_volume_properties::set_scalar_type(std::string scalar_type)
{
  m_scalar_type = scalar_type;
}

// ------------------------------------------------------------------------------------------------
void vtknvindex_regular_volume_properties::get_scalar_type(std::string& scalar_type) const
{
  scalar_type = m_scalar_type;
}

// ------------------------------------------------------------------------------------------------
void vtknvindex_regular_volume_properties::set_voxel_range(
  mi::math::Vector<mi::Float32, 2> voxel_range)
{
  m_voxel_range = voxel_range;
}

// ------------------------------------------------------------------------------------------------
void vtknvindex_regular_volume_properties::get_voxel_range(
  mi::math::Vector<mi::Float32, 2>& voxel_range) const
{
  voxel_range = m_voxel_range;
}

// ------------------------------------------------------------------------------------------------
void vtknvindex_regular_volume_properties::set_scalar_range(
  mi::math::Vector<mi::Float32, 2> scalar_range)
{
  m_scalar_range = scalar_range;
}

// ------------------------------------------------------------------------------------------------
void vtknvindex_regular_volume_properties::get_scalar_range(
  mi::math::Vector<mi::Float32, 2>& scalar_range) const
{
  scalar_range = m_scalar_range;
}

//-------------------------------------------------------------------------------------------------
bool vtknvindex_regular_volume_properties::is_timeseries_data() const
{
  return m_is_timeseries_data;
}

//-------------------------------------------------------------------------------------------------
void vtknvindex_regular_volume_properties::set_is_timeseries_data(bool is_timeseries)
{
  m_is_timeseries_data = is_timeseries;
}

//-------------------------------------------------------------------------------------------------
void vtknvindex_regular_volume_properties::set_nb_time_steps(mi::Uint32 nb_time_steps)
{
  m_nb_time_steps = nb_time_steps;
}

//-------------------------------------------------------------------------------------------------
mi::Uint32 vtknvindex_regular_volume_properties::get_nb_time_steps() const
{
  return m_nb_time_steps;
}

//-------------------------------------------------------------------------------------------------
void vtknvindex_regular_volume_properties::set_current_time_step(mi::Uint64 current_time_step)
{
  m_current_time_step = current_time_step;
}

//-------------------------------------------------------------------------------------------------
mi::Uint32 vtknvindex_regular_volume_properties::get_current_time_step() const
{
  return m_current_time_step;
}

//-------------------------------------------------------------------------------------------------
void vtknvindex_regular_volume_properties::set_time_step_start(mi::Uint32 time_step_start)
{
  m_time_step_start = time_step_start;
}

//-------------------------------------------------------------------------------------------------
mi::Uint32 vtknvindex_regular_volume_properties::get_time_step_start() const
{
  return m_time_step_start;
}

// ------------------------------------------------------------------------------------------------
void vtknvindex_regular_volume_properties::set_volume_size(
  mi::math::Vector_struct<mi::Uint32, 3> volume_size)
{
  m_volume_size = volume_size;
}

// ------------------------------------------------------------------------------------------------
void vtknvindex_regular_volume_properties::get_volume_size(
  mi::math::Vector_struct<mi::Uint32, 3>& volume_size) const
{
  volume_size = m_volume_size;
}

// ------------------------------------------------------------------------------------------------
void vtknvindex_regular_volume_properties::set_volume_extents(
  mi::math::Bbox<mi::Sint32, 3> volume_extents)
{
  m_volume_extents = volume_extents;
  m_ivol_volume_extents.min.x = volume_extents.min.x;
  m_ivol_volume_extents.min.y = volume_extents.min.y;
  m_ivol_volume_extents.min.z = volume_extents.min.z;
  m_ivol_volume_extents.max.x = volume_extents.max.x;
  m_ivol_volume_extents.max.y = volume_extents.max.y;
  m_ivol_volume_extents.max.z = volume_extents.max.z;
}

// ------------------------------------------------------------------------------------------------
void vtknvindex_regular_volume_properties::get_volume_extents(
  mi::math::Bbox<mi::Sint32, 3>& volume_extents) const
{
  volume_extents = m_volume_extents;
}

// ------------------------------------------------------------------------------------------------
void vtknvindex_regular_volume_properties::set_ivol_volume_extents(
  mi::math::Bbox<mi::Float32, 3> volume_extents)
{
  m_ivol_volume_extents = volume_extents;
}

// ------------------------------------------------------------------------------------------------
void vtknvindex_regular_volume_properties::get_ivol_volume_extents(
  mi::math::Bbox<mi::Float32, 3>& volume_extents) const
{
  volume_extents = m_ivol_volume_extents;
}

// ------------------------------------------------------------------------------------------------
void vtknvindex_regular_volume_properties::set_ghost_levels(mi::Sint32 ghost_levels)
{
  m_ghost_levels = ghost_levels;
}

// ------------------------------------------------------------------------------------------------
mi::Sint32 vtknvindex_regular_volume_properties::get_ghost_levels() const
{
  return m_ghost_levels;
}

// ------------------------------------------------------------------------------------------------
void vtknvindex_regular_volume_properties::set_volume_translation(
  mi::math::Vector<mi::Float32, 3> translation)
{
  m_volume_translation = translation;
}

// ------------------------------------------------------------------------------------------------
void vtknvindex_regular_volume_properties::get_volume_translation(
  mi::math::Vector<mi::Float32, 3>& translation) const
{
  translation = m_volume_translation;
}

// ------------------------------------------------------------------------------------------------
void vtknvindex_regular_volume_properties::set_volume_scaling(
  mi::math::Vector<mi::Float32, 3> scaling)
{
  m_volume_scaling = scaling;
}

// ------------------------------------------------------------------------------------------------
void vtknvindex_regular_volume_properties::get_volume_scaling(
  mi::math::Vector<mi::Float32, 3>& scaling) const
{
  scaling = m_volume_scaling;
}

//---------------------------------------------------------------------------
template <typename T>
void vtknvindex_regular_volume_properties::transform_zyx_to_xyz(
  T* pv_volume, T* shm_volume, mi::Sint32* bounds) const
{
  if (shm_volume == nullptr || pv_volume == nullptr)
    return;

  mi::Uint64 dx = bounds[1] - bounds[0] + 1;
  mi::Uint64 dy = bounds[3] - bounds[2] + 1;
  mi::Uint64 dz = bounds[5] - bounds[4] + 1;
  mi::Uint64 dxdy = dx * dy;

  mi::Uint64 index_offset = 0;
  for (mi::Uint64 x = 0; x < dx; ++x)
  {
    for (mi::Uint64 y = 0; y < dy; ++y)
    {
      mi::Uint64 pv_offset = x + y * dx;
      for (mi::Uint64 z = 0; z < dz; ++z)
      {
        shm_volume[index_offset] = pv_volume[pv_offset];
        index_offset++;
        pv_offset += dxdy;
      }
    }
  }
}

//----------------------------------------------------------------------------
bool vtknvindex_regular_volume_properties::write_shared_memory(vtkDataArray* scalar_array,
  mi::Sint32* bounds, vtknvindex_host_properties* host_properties, mi::Uint32 current_timestep,
  bool use_shared_mem)
{
  if (!host_properties)
  {
    ERROR_LOG << "Invalid host_properties in "
              << " vtknvindex_regular_volume_properties::write_shared_memory ";
    return false;
  }

  // If the origin is not [0,0,0] we have to translate all the pieces.
  mi::math::Bbox<mi::Float32, 3> current_bbox(bounds[0] - m_volume_extents.min.x,
    bounds[2] - m_volume_extents.min.y, bounds[4] - m_volume_extents.min.z,
    bounds[1] - m_volume_extents.min.x + 1, bounds[3] - m_volume_extents.min.y + 1,
    bounds[5] - m_volume_extents.min.z + 1);

  vtknvindex_host_properties::shm_info* shm_info =
    host_properties->get_shminfo(current_bbox, current_timestep);
  if (!shm_info)
  {
    ERROR_LOG << "Failed to get shared memory in "
              << "vtknvindex_regular_volume_properties::write_shared_memory.";
    return false;
  }

  if (shm_info->m_shm_name.empty())
  {
    ERROR_LOG << "Received invalid shared memory in "
              << "vtknvindex_regular_volume_properties::write_shared_memory.";
    return false;
  }

  if (use_shared_mem)
  {
    if (shm_info->m_size == 0)
    {
      return false;
    }

    mi::Uint8* shm_ptr = vtknvindex::util::get_vol_shm(shm_info->m_shm_name, shm_info->m_size);
    if (!shm_ptr)
    {
      ERROR_LOG << "Error retrieving shared memory pointer in "
                << "vtknvindex_regular_volume_properties::write_shared_memory.";
      return false;
    }

    const std::string scalar_type = scalar_array->GetDataTypeAsString();
    if (scalar_type == "double")
    {
      // Convert double to float data.
      // shm_ptr and m_size were already set up for float data.
      const mi::Size nb_voxels = shm_info->m_size / sizeof(mi::Float32);

      const mi::Float64* src =
        reinterpret_cast<const mi::Float64*>(scalar_array->GetVoidPointer(0));
      mi::Float32* dst = reinterpret_cast<mi::Float32*>(shm_ptr);
      for (mi::Size i = 0; i < nb_voxels; ++i)
      {
        dst[i] = static_cast<mi::Float32>(src[i]);
      }
    }
    else
    {
      memcpy(shm_ptr, scalar_array->GetVoidPointer(0), shm_info->m_size);
    }

    // free memory space linked to shared memory
    vtknvindex::util::unmap_shm(shm_ptr, shm_info->m_size);
  }
  else
  {
    // shm_info->m_subset_ptr = scalar_array->GetVoidPointer(0);
  }

  m_time_steps_written++;

  if (use_shared_mem)
  {
    INFO_LOG << "Done writing bounding box " << current_bbox
             << " into shared memory: " << shm_info->m_shm_name << ".";
  }
  else
  {
    INFO_LOG << "Bounding box " << current_bbox
             << " is available in local memory: " << shm_info->m_shm_name << ".";
  }

  return true;
}

// ------------------------------------------------------------------------------------------------
bool vtknvindex_regular_volume_properties::write_shared_memory(
  vtknvindex_irregular_volume_data* ivol_data, vtkUnstructuredGridBase* ugrid,
  vtknvindex_host_properties* host_properties, mi::Uint32 current_timestep)
{
  if (!host_properties)
  {
    ERROR_LOG << "Invalid host properties in "
              << " vtknvindex_regular_volume_properties::write_shared_memory.";
    return false;
  }

  std::string shm_memory_name;
  mi::math::Bbox<mi::Float32, 3> shm_bbox;
  mi::Uint64 shm_size = 0;
  void* subset_ptr = nullptr;

  if (!host_properties->get_shminfo(ivol_data->subregion_bbox, shm_memory_name, shm_bbox, shm_size,
        &subset_ptr, current_timestep))
  {
    ERROR_LOG << "Failed to get shared memory in "
              << "vtknvindex_regular_volume_properties::write_shared_memory.";
    return false;
  }

  if (shm_memory_name.empty())
  {
    ERROR_LOG << "Received invalid shared memory in "
              << "vtknvindex_regular_volume_properties::write_shared_memory.";
    return false;
  }

  // check scalar type
  const mi::Size scalar_size = get_scalar_size(m_scalar_type);
  if (scalar_size == 0)
  {
    return false;
  }

  mi::Uint8* shm_ptr = vtknvindex::util::get_vol_shm(shm_memory_name, shm_size);
  if (!shm_ptr)
  {
    ERROR_LOG << "Encountered an error when retrieving a shred memory pointer in "
              << "vtknvindex_regular_volume_properties::write_shared_memory.";
    return false;
  }

  mi::Uint8* shm_offset = shm_ptr;
  size_t size_elm;

  // copy irregular volume data to shared memory

  // num points
  size_elm = sizeof(ivol_data->num_points);
  memcpy(shm_offset, &ivol_data->num_points, size_elm);
  shm_offset += size_elm;

  // num cells
  size_elm = sizeof(ivol_data->num_cells);
  memcpy(shm_offset, &ivol_data->num_cells, size_elm);
  shm_offset += size_elm;

  // num scalars
  size_elm = sizeof(ivol_data->num_scalars);
  memcpy(shm_offset, &ivol_data->num_scalars, size_elm);
  shm_offset += size_elm;

  // cell flag
  size_elm = sizeof(ivol_data->cell_flag);
  memcpy(shm_offset, &ivol_data->cell_flag, size_elm);
  shm_offset += size_elm;

  // points
  size_elm = sizeof(mi::Float32) * 3;

  for (vtkIdType i = 0; i < ivol_data->num_points; ++i)
  {
    mi::math::Vector<mi::Float64, 3> point_pv;
    ugrid->GetPoint(i, point_pv.begin());
    const mi::math::Vector<mi::Float32, 3> point_index(point_pv);
    // NOLINTNEXTLINE(bugprone-undefined-memory-manipulation)
    memcpy(shm_offset, &point_index, size_elm);
    shm_offset += size_elm;
  }

  // cells
  size_elm = sizeof(mi::Uint32) * 4;

  const bool per_cell_scalars = (ivol_data->cell_flag == 1);
  mi::Uint8* shm_scalar_offset = shm_offset + ivol_data->num_cells * size_elm;

  vtkSmartPointer<vtkCellIterator> cellIter =
    vtkSmartPointer<vtkCellIterator>::Take(ugrid->NewCellIterator());
  for (cellIter->InitTraversal(); !cellIter->IsDoneWithTraversal(); cellIter->GoToNextCell())
  {
    const vtkIdType npts = cellIter->GetNumberOfPoints();
    if (npts != 4)
      continue;

    const vtkIdType* cell_point_ids = cellIter->GetPointIds()->GetPointer(0);

    const mi::Uint32 cur_cell_index[4] = { static_cast<mi::Uint32>(cell_point_ids[0]),
      static_cast<mi::Uint32>(cell_point_ids[1]), static_cast<mi::Uint32>(cell_point_ids[2]),
      static_cast<mi::Uint32>(cell_point_ids[3]) };

    memcpy(shm_offset, cur_cell_index, size_elm);
    shm_offset += size_elm;

    if (per_cell_scalars)
    {
      // copy per-cell scalars
      const vtkIdType cell_id = cellIter->GetCellId();
      memcpy(shm_scalar_offset,
        &reinterpret_cast<const mi::Uint8*>(ivol_data->scalars)[cell_id * scalar_size],
        scalar_size);
      shm_scalar_offset += scalar_size;
    }
  }

  // scalars
  size_elm = scalar_size * ivol_data->num_scalars;
  if (!per_cell_scalars)
  {
    // copy per-point scalars here (any per-cell scalars were already copied in the loop above)
    memcpy(shm_offset, ivol_data->scalars, size_elm);
  }
  shm_offset += size_elm;

  // max edge length
  size_elm = sizeof(ivol_data->max_edge_length2);
  memcpy(shm_offset, &ivol_data->max_edge_length2, size_elm);
  shm_offset += size_elm;

  if (shm_offset - shm_ptr != static_cast<mi::Difference>(shm_size))
  {
    ERROR_LOG << "Encountered a shared memory copy mismatch, diff: " << shm_offset - shm_ptr
              << ", size: " << shm_size << ".";
    return false;
  }

  m_time_steps_written++;

  INFO_LOG << "Finished writing the bounding box: " << ivol_data->subregion_bbox
           << " to shared memory: " << shm_memory_name << ".";

  // free memory space linked to shared memory
  vtknvindex::util::unmap_shm(shm_ptr, shm_size);

  return true;
}

// Print all the host details
// ------------------------------------------------------------------------------------------------
void vtknvindex_regular_volume_properties::print_info() const
{
  INFO_LOG << "Scalar type: " << m_scalar_type;
  INFO_LOG << "Volume bbox: " << m_ivol_volume_extents;
  INFO_LOG << "Volume size: " << m_ivol_volume_extents.max - m_ivol_volume_extents.min;
  INFO_LOG << "Data values: " << m_voxel_range;
  INFO_LOG << "Time series: " << ((m_is_timeseries_data) ? "Yes" : "No");
}

// ------------------------------------------------------------------------------------------------
void vtknvindex_regular_volume_properties::serialize(mi::neuraylib::ISerializer* serializer) const
{
  // Serialize scalar type string.
  mi::Uint32 scalar_typename_size = mi::Uint32(m_scalar_type.size());
  serializer->write(&scalar_typename_size);
  serializer->write(
    reinterpret_cast<const mi::Uint8*>(m_scalar_type.c_str()), scalar_typename_size);

  serializer->write(&m_voxel_range.x, 2);

  serializer->write(&m_ghost_levels);
}

// ------------------------------------------------------------------------------------------------
void vtknvindex_regular_volume_properties::deserialize(mi::neuraylib::IDeserializer* deserializer)
{
  // Deserialize scalar type string.
  mi::Uint32 scalar_typename_size = 0;
  deserializer->read(&scalar_typename_size);
  m_scalar_type.resize(scalar_typename_size);
  deserializer->read(reinterpret_cast<mi::Uint8*>(&m_scalar_type[0]), scalar_typename_size);

  deserializer->read(&m_voxel_range.x, 2);

  deserializer->read(&m_ghost_levels);
}

// ------------------------------------------------------------------------------------------------
mi::Size vtknvindex_regular_volume_properties::get_scalar_size(const std::string& scalar_type)
{
  mi::Size scalar_size = 0;

  if (scalar_type == "char" || scalar_type == "unsigned char")
  {
    scalar_size = sizeof(mi::Uint8);
  }
  else if (scalar_type == "short" || scalar_type == "unsigned short")
  {
    scalar_size = sizeof(mi::Uint16);
  }
  else if (scalar_type == "float")
  {
    scalar_size = sizeof(mi::Float32);
  }
  else if (scalar_type == "double")
  {
    scalar_size = sizeof(mi::Float64);
  }

  return scalar_size;
}
