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

/** \file
 * \ingroup balembic
 */

#include "abc_reader_mesh.h"
#include "abc_axis_conversion.h"
#include "abc_customdata.h"
#include "abc_util.h"

#include "DNA_material_types.h"
#include "DNA_modifier_types.h"

#include "DNA_object_types.h"

#include "BLI_compiler_compat.h"
#include "BLI_listbase.h"
#include "BLI_map.hh"
#include "BLI_math_vector.h"
#include "BLI_offset_indices.hh"
#include "BLI_ordered_edge.hh"

#include "BLT_translation.hh"

#include "BKE_customdata.hh"
#include "BKE_geometry_set.hh"
#include "BKE_lib_id.hh"
#include "BKE_main.hh"
#include "BKE_material.hh"
#include "BKE_mesh.hh"
#include "BKE_object.hh"
#include "BKE_subdiv.hh"

#include "CLG_log.h"
#include "IO_validate.hh"

namespace blender {

using Alembic::Abc::FloatArraySamplePtr;
using Alembic::Abc::IInt32ArrayProperty;
using Alembic::Abc::Int32ArraySamplePtr;
using Alembic::Abc::P3fArraySamplePtr;
using Alembic::Abc::PropertyHeader;
using Alembic::Abc::V3fArraySamplePtr;

using Alembic::AbcGeom::IC3fGeomParam;
using Alembic::AbcGeom::IC4fGeomParam;
using Alembic::AbcGeom::IFaceSet;
using Alembic::AbcGeom::IFaceSetSchema;
using Alembic::AbcGeom::IN3fGeomParam;
using Alembic::AbcGeom::IObject;
using Alembic::AbcGeom::IPolyMesh;
using Alembic::AbcGeom::IPolyMeshSchema;
using Alembic::AbcGeom::ISampleSelector;
using Alembic::AbcGeom::ISubD;
using Alembic::AbcGeom::ISubDSchema;
using Alembic::AbcGeom::IV2fGeomParam;
using Alembic::AbcGeom::kWrapExisting;
using Alembic::AbcGeom::N3fArraySample;
using Alembic::AbcGeom::N3fArraySamplePtr;
using Alembic::AbcGeom::UInt32ArraySamplePtr;
using Alembic::AbcGeom::V2fArraySamplePtr;

namespace io::alembic {

static CLG_LogRef LOG = {"io.alembic"};

/* NOTE: Alembic's face winding order is clockwise, to match with Renderman. */

/* Some helpers for mesh generation */
namespace utils {

static std::map<std::string, Material *> build_material_map(const Main *bmain)
{
  std::map<std::string, Material *> mat_map;
  for (Material &material : bmain->materials) {
    mat_map[material.id.name + 2] = &material;
  }
  return mat_map;
}

static void assign_materials(Main *bmain,
                             Object *ob,
                             const std::map<std::string, int> &mat_index_map)
{
  std::map<std::string, int>::const_iterator it;
  if (mat_index_map.size() > MAXMAT) {
    return;
  }

  std::map<std::string, Material *> matname_to_material = build_material_map(bmain);
  std::map<std::string, Material *>::iterator mat_iter;

  for (it = mat_index_map.begin(); it != mat_index_map.end(); ++it) {
    const std::string mat_name = it->first;
    const int mat_index = it->second;

    Material *assigned_mat;
    mat_iter = matname_to_material.find(mat_name);
    if (mat_iter == matname_to_material.end()) {
      assigned_mat = BKE_material_add(bmain, mat_name.c_str());
      id_us_min(&assigned_mat->id);
      matname_to_material[mat_name] = assigned_mat;
    }
    else {
      assigned_mat = mat_iter->second;
    }

    BKE_object_material_assign_single_obdata(bmain, ob, assigned_mat, mat_index);
  }
  if (ob->totcol > 0) {
    ob->actcol = 1;
  }
}

} /* namespace utils */

/** This utility structure holds mesh data arrays which are common between the IPolyMeshSchema and
 * the ISubDSchema. As those schemas do not derive from one another, this structure is used to read
 * mesh data generically. */
struct AbcMeshData {
  StringRef iobject_full_name;
  StringRef schema_name;

  IInt32ArrayProperty face_indices_property;
  Int32ArraySamplePtr face_indices;

  IInt32ArrayProperty face_counts_property;
  Int32ArraySamplePtr face_counts;

  /* Optional settings for reading interpolated vertices. If present, `ceil_positions` has to be
   * valid. */
  std::optional<SampleInterpolationSettings> interpolation_settings;
  P3fArraySamplePtr positions;
  P3fArraySamplePtr ceil_positions;

  IV2fGeomParam uvs_param;
  IN3fGeomParam normals_param;
  V3fArraySamplePtr velocities;

  ICompoundProperty arb_geom_params;
};

static void read_mverts_interp(float3 *vert_positions,
                               const P3fArraySamplePtr &positions,
                               const P3fArraySamplePtr &ceil_positions,
                               const double weight)
{
  float tmp[3];
  for (int i = 0; i < positions->size(); i++) {
    const Imath::V3f &floor_pos = (*positions)[i];
    const Imath::V3f &ceil_pos = (*ceil_positions)[i];

    interp_v3_v3v3(tmp, floor_pos.getValue(), ceil_pos.getValue(), float(weight));
    copy_zup_from_yup(vert_positions[i], tmp);
  }
}

static void read_mverts(CDStreamConfig &config, const AbcMeshData &mesh_data)
{
  float3 *vert_positions = config.positions;
  const P3fArraySamplePtr &positions = mesh_data.positions;

  if (mesh_data.interpolation_settings.has_value()) {
    BLI_assert_msg(
        mesh_data.ceil_positions != nullptr,
        "AbcMeshData does not have ceil positions although it has some interpolation settings.");

    const double weight = mesh_data.interpolation_settings->weight;
    read_mverts_interp(vert_positions, positions, mesh_data.ceil_positions, weight);
    config.mesh->tag_positions_changed();
    return;
  }

  read_mverts(*config.mesh, positions, nullptr);
}

void read_mverts(Mesh &mesh, const P3fArraySamplePtr positions, const N3fArraySamplePtr normals)
{
  MutableSpan<float3> vert_positions = mesh.vert_positions_for_write();
  for (int i = 0; i < positions->size(); i++) {
    Imath::V3f pos_in = (*positions)[i];

    copy_zup_from_yup(vert_positions[i], pos_in.getValue());
  }
  mesh.tag_positions_changed();

  if (normals) {
    Vector<float3> vert_normals(mesh.verts_num);
    for (const int64_t i : IndexRange(normals->size())) {
      Imath::V3f nor_in = (*normals)[i];
      copy_zup_from_yup(vert_normals[i], nor_in.getValue());
    }
    bke::mesh_vert_normals_assign(mesh, std::move(vert_normals));
  }
}

/* Update references to mesh data. */
static void config_reload_mesh(CDStreamConfig &config)
{
  config.positions = config.mesh->vert_positions_for_write().data();
  config.corner_verts = config.mesh->corner_verts_for_write().data();
  config.face_offsets = config.mesh->face_offsets_for_write().data();
  config.totvert = config.mesh->verts_num;
  config.totloop = config.mesh->corners_num;
  config.faces_num = config.mesh->faces_num;
}

static CDStreamConfig get_config(Mesh *mesh)
{
  CDStreamConfig config;
  config.mesh = mesh;
  config_reload_mesh(config);

  return config;
}

static void read_mpolys(CDStreamConfig &config, const AbcMeshData &mesh_data)
{
  int *face_offsets = config.face_offsets;
  int *corner_verts = config.corner_verts;
  float2 *uv_maps = config.uv_map.span.data();

  const Int32ArraySamplePtr &face_indices = mesh_data.face_indices;
  const Int32ArraySamplePtr &face_counts = mesh_data.face_counts;
  const V2fArraySamplePtr &uvs = config.uvs;
  const int64_t uvs_size = uvs == nullptr ? 0 : int64_t(uvs->size());

  const UInt32ArraySamplePtr &uvs_indices = config.uvs_indices;
  const int64_t uvs_indices_size = uvs_indices == nullptr ? 0 : int64_t(uvs_indices->size());

  const bool do_uvs = (uv_maps && uvs && uvs_indices);
  const bool do_uvs_per_loop = do_uvs && config.uv_scope == ABC_UV_SCOPE_LOOP;
  BLI_assert(!do_uvs || config.uv_scope != ABC_UV_SCOPE_NONE);
  int64_t loop_index = 0;
  int64_t rev_loop_index = 0;

  const int64_t corners_num = int64_t(face_indices->size());

  for (int64_t i = 0; i < face_counts->size(); i++) {
    int face_size = (*face_counts)[i];

    face_offsets[i] = loop_index;

    /* Polygons are always assumed to be smooth-shaded. If the Alembic mesh should be flat-shaded,
     * this is encoded in custom loop normals. See #71246. */

    /* NOTE: Alembic data is stored in the reverse order. */
    if (face_size > 0 && face_size <= corners_num - loop_index) {
      rev_loop_index = loop_index + (face_size - 1);
    }
    else {
      rev_loop_index = loop_index;
      face_size = 0;
    }

    const int64_t last_vertex_index = 0;
    for (int64_t f = 0; f < face_size; f++, loop_index++, rev_loop_index--) {
      int vert = (*face_indices)[loop_index];
      if (!validate::index_in_range(vert, config.mesh->verts_num)) {
        vert = 0;
      }
      corner_verts[rev_loop_index] = vert;

      if (do_uvs) {
        const int64_t lookup_index = do_uvs_per_loop ? loop_index : last_vertex_index;
        if (!validate::index_in_range(lookup_index, uvs_indices_size)) {
          continue;
        }
        const int64_t uv_index = (*uvs_indices)[lookup_index];

        /* Some Alembic files are broken (or at least export UVs in a way we don't expect). */
        if (!validate::index_in_range(uv_index, uvs_size)) {
          continue;
        }

        uv_maps[rev_loop_index][0] = (*uvs)[uv_index][0];
        uv_maps[rev_loop_index][1] = (*uvs)[uv_index][1];
      }
    }
  }

  config.uv_map.finish();

  /* Check for faces with duplicate vertex indices. These will require a mesh validate to fix. */
  IndexMaskMemory memory;
  const IndexMask bad_faces = bke::mesh_find_faces_duplicate_verts(*config.mesh, memory);
  const bool all_faces_ok = bad_faces.is_empty();

  /* If we detect bad faces it would be unsafe to continue beyond this point without first
   * performing a destructive validate. Any operation requiring mesh connectivity information can
   * assert or crash if the problem isn't addressed. Performing the check here, before most of the
   * data has been loaded, unfortunately means any remaining data will be lost. */
  if (!all_faces_ok) {
    if (config.modifier_error_message) {
      *config.modifier_error_message = "Mesh has invalid geometry";
    }
    bke::mesh_validate(*config.mesh, false);

    config_reload_mesh(config);
  }

  bke::mesh_calc_edges(*config.mesh, false, false);
}

static void process_no_normals(CDStreamConfig & /*config*/)
{
  /* Absence of normals in the Alembic mesh is interpreted as 'smooth'. */
}

static void process_loop_normals(CDStreamConfig &config, const N3fArraySamplePtr loop_normals_ptr)
{
  size_t loop_count = loop_normals_ptr->size();

  if (loop_count == 0) {
    process_no_normals(config);
    return;
  }

  Mesh *mesh = config.mesh;
  if (loop_count != mesh->corners_num) {
    /* This happens in certain Houdini exports. When a mesh is animated and then replaced by a
     * fluid simulation, Houdini will still write the original mesh's loop normals, but the mesh
     * verts/loops/faces are from the simulation. In such cases the normals cannot be mapped to the
     * mesh, so it's better to ignore them. */
    process_no_normals(config);
    return;
  }

  Array<float3> corner_normals(loop_count);

  const OffsetIndices faces = mesh->faces();
  const N3fArraySample &loop_normals = *loop_normals_ptr;
  int abc_index = 0;
  for (int i = 0, e = mesh->faces_num; i < e; i++) {
    const IndexRange face = faces[i];
    /* As usual, ABC orders the loops in reverse. */
    for (int j = face.size() - 1; j >= 0; j--, abc_index++) {
      int blender_index = face[j];
      copy_zup_from_yup(corner_normals[blender_index], loop_normals[abc_index].getValue());
    }
  }

  bke::mesh_set_custom_normals(*mesh, corner_normals);
}

static void process_vertex_normals(CDStreamConfig &config,
                                   const N3fArraySamplePtr vertex_normals_ptr)
{
  size_t normals_count = vertex_normals_ptr->size();
  if (normals_count == 0) {
    process_no_normals(config);
    return;
  }

  Array<float3> vert_normals(normals_count);

  const N3fArraySample &vertex_normals = *vertex_normals_ptr;
  for (int index = 0; index < normals_count; index++) {
    copy_zup_from_yup(vert_normals[index], vertex_normals[index].getValue());
  }

  bke::mesh_set_custom_normals_from_verts(*config.mesh, vert_normals);
}

static void process_normals(CDStreamConfig &config,
                            const IN3fGeomParam &normals,
                            const ISampleSelector &selector)
{
  if (!normals.valid()) {
    process_no_normals(config);
    return;
  }

  IN3fGeomParam::Sample normsamp = normals.getExpandedValue(selector);
  Alembic::AbcGeom::GeometryScope scope = normals.getScope();

  switch (scope) {
    case Alembic::AbcGeom::kFacevaryingScope: /* 'Vertex Normals' in Houdini. */
      process_loop_normals(config, normsamp.getVals());
      break;
    case Alembic::AbcGeom::kVertexScope:
    case Alembic::AbcGeom::kVaryingScope: /* 'Point Normals' in Houdini. */
      process_vertex_normals(config, normsamp.getVals());
      break;
    case Alembic::AbcGeom::kConstantScope:
    case Alembic::AbcGeom::kUniformScope:
    case Alembic::AbcGeom::kUnknownScope:
      process_no_normals(config);
      break;
  }
}

BLI_INLINE void read_uvs_params(CDStreamConfig &config,
                                const IV2fGeomParam &uv,
                                const ISampleSelector &selector)
{
  if (!uv.valid()) {
    return;
  }

  IV2fGeomParam::Sample uvsamp;
  uv.getIndexed(uvsamp, selector);

  UInt32ArraySamplePtr uvs_indices = uvsamp.getIndices();

  const AbcUvScope uv_scope = get_uv_scope(uv.getScope(), config, uvs_indices);

  if (uv_scope == ABC_UV_SCOPE_NONE) {
    return;
  }

  config.uv_scope = uv_scope;
  config.uvs = uvsamp.getVals();
  config.uvs_indices = uvs_indices;

  std::string name = Alembic::Abc::GetSourceName(uv.getMetaData());

  /* According to the convention, primary UVs should have had their name
   * set using Alembic::Abc::SetSourceName, but you can't expect everyone
   * to follow it! :) */
  if (name.empty()) {
    name = uv.getName();
  }

  bke::MutableAttributeAccessor attributes = config.mesh->attributes_for_write();
  config.uv_map = attributes.lookup_or_add_for_write_span<float2>(name, bke::AttrDomain::Corner);
  config.mesh->uv_maps_active_set(name);
  config.mesh->uv_maps_default_set(name);
}

template<typename SampleType>
static bool samples_have_same_topology(const SampleType &sample, const SampleType &ceil_sample)
{
  const P3fArraySamplePtr &positions = sample.getPositions();
  const Alembic::Abc::Int32ArraySamplePtr &face_indices = sample.getFaceIndices();
  const Alembic::Abc::Int32ArraySamplePtr &face_counts = sample.getFaceCounts();

  const P3fArraySamplePtr &ceil_positions = ceil_sample.getPositions();
  const Alembic::Abc::Int32ArraySamplePtr &ceil_face_indices = ceil_sample.getFaceIndices();
  const Alembic::Abc::Int32ArraySamplePtr &ceil_face_counts = ceil_sample.getFaceCounts();

  /* It the counters are different, we can be sure the topology is different. */
  const bool different_counters = positions->size() != ceil_positions->size() ||
                                  face_counts->size() != ceil_face_counts->size() ||
                                  face_indices->size() != ceil_face_indices->size();
  if (different_counters) {
    return false;
  }

  /* Otherwise, we need to check the connectivity as files from e.g. videogrammetry may have the
   * same face count, but different connections between faces. */

  if (memcmp(face_counts->get(), ceil_face_counts->get(), face_counts->size() * sizeof(int))) {
    return false;
  }

  if (memcmp(face_indices->get(), ceil_face_indices->get(), face_indices->size() * sizeof(int))) {
    return false;
  }

  return true;
}

static bool topology_changed(const AbcMeshData &abc_mesh_data,
                             const Mesh *existing_mesh,
                             const bool is_reading_a_file_sequence)
{
  const P3fArraySamplePtr &positions = abc_mesh_data.positions;
  const Int32ArraySamplePtr &face_indices = abc_mesh_data.face_indices;
  const Int32ArraySamplePtr &face_counts = abc_mesh_data.face_counts;

  /* It the counters are different, we can be sure the topology is different. */
  const bool different_counters = positions->size() != existing_mesh->verts_num ||
                                  face_counts->size() != existing_mesh->faces_num ||
                                  face_indices->size() != existing_mesh->corners_num;
  if (different_counters) {
    return true;
  }

  /* Check first if we indeed have multiple samples, unless we read a file sequence in which case
   * we need to do a full topology comparison. */
  if (!is_reading_a_file_sequence && (abc_mesh_data.face_indices_property.getNumSamples() == 1 &&
                                      abc_mesh_data.face_counts_property.getNumSamples() == 1))
  {
    return false;
  }

  /* Otherwise, we need to check the connectivity as files from e.g. videogrammetry may have the
   * same face count, but different connections between faces. */
  int64_t abc_index = 0;

  const int *mesh_corner_verts = existing_mesh->corner_verts().data();
  const int *mesh_face_offsets = existing_mesh->face_offsets().data();
  const int64_t mesh_corners_num = existing_mesh->corners_num;
  const int64_t abc_corners_num = int64_t(face_indices->size());

  for (int64_t i = 0; i < face_counts->size(); i++) {
    if (mesh_face_offsets[i] != abc_index) {
      return true;
    }

    const int abc_face_size = (*face_counts)[i];
    /* If the file's face_counts would advance abc_index past either array, the topology must
     * differ from the existing mesh; treat as changed rather than read out of bounds. */
    if (abc_face_size < 0 || abc_face_size > abc_corners_num - abc_index ||
        abc_face_size > mesh_corners_num - abc_index)
    {
      return true;
    }
    /* NOTE: Alembic data is stored in the reverse order. */
    int64_t rev_loop_index = abc_index + (abc_face_size > 0 ? abc_face_size - 1 : 0);
    for (int64_t f = 0; f < abc_face_size; f++, abc_index++, rev_loop_index--) {
      const int mesh_vert = mesh_corner_verts[rev_loop_index];
      const int abc_vert = (*face_indices)[abc_index];
      if (mesh_vert != abc_vert) {
        return true;
      }
    }
  }

  return false;
}

static Mesh *read_mesh_sample(const AbcMeshData &abc_mesh_data,
                              const ISampleSelector &selector,
                              const AbcReadGeometryParams &read_params,
                              Mesh *existing_mesh,
                              const char **r_err_str,
                              const bool is_reading_a_file_sequence)
{
  const P3fArraySamplePtr &positions = abc_mesh_data.positions;
  const Int32ArraySamplePtr &face_indices = abc_mesh_data.face_indices;
  const Int32ArraySamplePtr &face_counts = abc_mesh_data.face_counts;

  /* Do some very minimal mesh validation. */
  const int64_t poly_count = face_counts->size();
  const int64_t loop_count = face_indices->size();
  const int64_t vert_count = positions->size();
  if (!validate::size_fits_in_int(poly_count) || !validate::size_fits_in_int(loop_count) ||
      !validate::size_fits_in_int(vert_count))
  {
    if (r_err_str != nullptr) {
      *r_err_str = RPT_("Mesh too large; more detail on the console");
    }
    CLOG_WARN(&LOG,
              "Mesh too large for '%s/%s' at time %f, exceeds max int size",
              abc_mesh_data.iobject_full_name.data(),
              abc_mesh_data.schema_name.data(),
              selector.getRequestedTime());
    return existing_mesh;
  }
  /* This is the same test as in poly_to_tri_count(). */
  if (poly_count > 0 && loop_count < poly_count * 2) {
    if (r_err_str != nullptr) {
      *r_err_str = RPT_("Invalid mesh; more detail on the console");
    }
    CLOG_WARN(&LOG,
              "Invalid mesh sample for '%s/%s' at time %f, less than 2 loops per face",
              abc_mesh_data.iobject_full_name.data(),
              abc_mesh_data.schema_name.data(),
              selector.getRequestedTime());
    return existing_mesh;
  }

  Mesh *new_mesh = nullptr;

  /* Only read point data when streaming meshes, unless we need to create new ones. */
  int read_flag = read_params.read_flag;

  if (topology_changed(abc_mesh_data, existing_mesh, is_reading_a_file_sequence)) {
    new_mesh = BKE_mesh_new_nomain_from_template(
        existing_mesh, positions->size(), 0, face_counts->size(), face_indices->size());

    read_flag |= MOD_MESHSEQ_READ_ALL;
  }
  else {
    /* If the face count changed (e.g. by triangulation), only read points.
     * This prevents crash from #49813.
     * TODO(kevin): perhaps find a better way to do this? */
    if (face_counts->size() != existing_mesh->faces_num ||
        face_indices->size() != existing_mesh->corners_num)
    {
      read_flag = MOD_MESHSEQ_READ_VERT;

      if (r_err_str) {
        *r_err_str = RPT_(
            "Topology has changed, perhaps by triangulating the mesh. Only vertices will be "
            "read!");
      }
    }
  }

  Mesh *mesh_to_export = new_mesh ? new_mesh : existing_mesh;
  CDStreamConfig config = get_config(mesh_to_export);
  config.time = selector.getRequestedTime();
  config.modifier_error_message = r_err_str;

  if ((read_flag & MOD_MESHSEQ_READ_UV) != 0) {
    read_uvs_params(config, abc_mesh_data.uvs_param, selector);
  }

  if ((read_flag & MOD_MESHSEQ_READ_VERT) != 0) {
    read_mverts(config, abc_mesh_data);
    read_generated_coordinates(abc_mesh_data.arb_geom_params, config, selector);
  }

  if ((read_flag & MOD_MESHSEQ_READ_POLY) != 0) {
    read_mpolys(config, abc_mesh_data);
    process_normals(config, abc_mesh_data.normals_param, selector);
  }

  if ((read_flag & (MOD_MESHSEQ_READ_UV | MOD_MESHSEQ_READ_COLOR)) != 0) {
    read_custom_data(
        abc_mesh_data.iobject_full_name, abc_mesh_data.arb_geom_params, config, selector);
  }

  if (abc_mesh_data.velocities) {
    read_velocity(abc_mesh_data.velocities, config, read_params.velocity_scale);
  }

  return mesh_to_export;
}

/* ************************************************************************** */

static AbcMeshData extract_mesh_data(const IObject &iobject,
                                     const IPolyMeshSchema &schema,
                                     const ISampleSelector &selector,
                                     const AbcReadGeometryParams &params,
                                     const bool for_topology_check)
{
  IPolyMeshSchema::Sample sample = schema.getValue(selector);

  AbcMeshData result;
  result.positions = sample.getPositions();
  result.face_indices_property = schema.getFaceIndicesProperty();
  result.face_indices = sample.getFaceIndices();
  result.face_counts_property = schema.getFaceCountsProperty();
  result.face_counts = sample.getFaceCounts();

  if (!for_topology_check) {
    result.iobject_full_name = iobject.getFullName();
    result.schema_name = schema.getName();
    result.arb_geom_params = schema.getArbGeomParams();
    result.uvs_param = schema.getUVsParam();
    result.normals_param = schema.getNormalsParam();

    if (!params.velocity_name.empty() && params.velocity_scale != 0.0f) {
      result.velocities = get_velocity_prop(schema, selector, params.velocity_name);
    }

    const std::optional<SampleInterpolationSettings> interpolation_settings =
        get_sample_interpolation_settings(
            selector, schema.getTimeSampling(), schema.getNumSamples());

    const bool use_vertex_interpolation = params.read_flag & MOD_MESHSEQ_INTERPOLATE_VERTICES;
    if (use_vertex_interpolation && interpolation_settings.has_value()) {
      IPolyMeshSchema::Sample ceil_sample;
      schema.get(ceil_sample, ISampleSelector(interpolation_settings->ceil_index));
      if (samples_have_same_topology(sample, ceil_sample)) {
        /* Only set interpolation data if the samples are compatible. */
        result.ceil_positions = ceil_sample.getPositions();
        result.interpolation_settings = interpolation_settings;
      }
    }
  }

  return result;
}

AbcMeshReader::AbcMeshReader(const AbcReaderConstructorArgs &args) : AbcObjectReader(args)
{
  m_settings->read_flag |= MOD_MESHSEQ_READ_ALL;

  IPolyMesh ipoly_mesh(m_iobject, kWrapExisting);
  m_schema = ipoly_mesh.getSchema();
}

bool AbcMeshReader::valid() const
{
  return m_schema.valid();
}

template<class typedGeomParam>
bool is_valid_animated(const ICompoundProperty arbGeomParams, const PropertyHeader &prop_header)
{
  if (!typedGeomParam::matches(prop_header)) {
    return false;
  }

  typedGeomParam geom_param(arbGeomParams, prop_header.getName());
  return geom_param.valid() && !geom_param.isConstant();
}

static bool has_animated_geom_params(const ICompoundProperty arbGeomParams)
{
  if (!arbGeomParams.valid()) {
    return false;
  }

  const int num_props = arbGeomParams.getNumProperties();
  for (int i = 0; i < num_props; i++) {
    const PropertyHeader &prop_header = arbGeomParams.getPropertyHeader(i);

    /* These are interpreted as vertex colors later (see 'read_custom_data'). */
    if (is_valid_animated<IC3fGeomParam>(arbGeomParams, prop_header)) {
      return true;
    }
    if (is_valid_animated<IC4fGeomParam>(arbGeomParams, prop_header)) {
      return true;
    }
  }

  return false;
}

/* Specialization of #has_animations() as defined in abc_reader_object.h. */
template<> bool has_animations(Alembic::AbcGeom::IPolyMeshSchema &schema, ImportSettings *settings)
{
  if (settings->is_sequence || !schema.isConstant()) {
    return true;
  }

  IV2fGeomParam uvsParam = schema.getUVsParam();
  if (uvsParam.valid() && !uvsParam.isConstant()) {
    return true;
  }

  IN3fGeomParam normalsParam = schema.getNormalsParam();
  if (normalsParam.valid() && !normalsParam.isConstant()) {
    return true;
  }

  ICompoundProperty arbGeomParams = schema.getArbGeomParams();
  if (has_animated_geom_params(arbGeomParams)) {
    return true;
  }

  return false;
}

template<> bool has_animations(Alembic::AbcGeom::ISubDSchema &schema, ImportSettings *settings)
{
  if (settings->is_sequence || !schema.isConstant()) {
    return true;
  }

  IV2fGeomParam uvsParam = schema.getUVsParam();
  if (uvsParam.valid() && !uvsParam.isConstant()) {
    return true;
  }

  Alembic::AbcGeom::IInt32ArrayProperty creaseIndices = schema.getCreaseIndicesProperty();
  if (creaseIndices.valid() && !creaseIndices.isConstant()) {
    return true;
  }
  Alembic::AbcGeom::IFloatArrayProperty creaseSharpnesses = schema.getCreaseSharpnessesProperty();
  if (creaseSharpnesses.valid() && !creaseSharpnesses.isConstant()) {
    return true;
  }

  Alembic::AbcGeom::IInt32ArrayProperty cornerIndices = schema.getCornerIndicesProperty();
  if (cornerIndices.valid() && !cornerIndices.isConstant()) {
    return true;
  }
  Alembic::AbcGeom::IFloatArrayProperty cornerSharpnesses = schema.getCornerSharpnessesProperty();
  if (cornerSharpnesses.valid() && !cornerSharpnesses.isConstant()) {
    return true;
  }

  ICompoundProperty arbGeomParams = schema.getArbGeomParams();
  if (has_animated_geom_params(arbGeomParams)) {
    return true;
  }

  return false;
}

void AbcMeshReader::readObjectData(Main *bmain, const Alembic::Abc::ISampleSelector &sample_sel)
{
  Mesh *mesh = BKE_mesh_add(bmain, m_data_name.c_str());

  m_object = BKE_object_add_only_object(bmain, OB_MESH, m_object_name.c_str());
  m_object->data = id_cast<ID *>(mesh);

  AbcReadGeometryParams read_params{};
  read_params.read_flag = MOD_MESHSEQ_READ_ALL;

  Mesh *read_mesh = this->read_mesh(mesh, sample_sel, read_params, nullptr);
  if (read_mesh != mesh) {
    BKE_mesh_nomain_to_mesh(read_mesh, mesh, m_object);
  }

  if (m_settings->validate_meshes) {
    bke::mesh_validate(*mesh, false);
  }

  readFaceSetsSample(bmain, mesh, sample_sel);

  if (m_settings->always_add_cache_reader || has_animations(m_schema, m_settings)) {
    addCacheModifier();
  }
}

bool AbcMeshReader::accepts_object_type(
    const Alembic::AbcCoreAbstract::ObjectHeader &alembic_header,
    const Object *const ob,
    const char **r_err_str) const
{
  if (!Alembic::AbcGeom::IPolyMesh::matches(alembic_header)) {
    *r_err_str = RPT_(
        "Object type mismatch, Alembic object path pointed to PolyMesh when importing, but not "
        "any more");
    return false;
  }

  if (ob->type != OB_MESH) {
    *r_err_str = RPT_("Object type mismatch, Alembic object path points to PolyMesh");
    return false;
  }

  return true;
}

bool AbcMeshReader::topology_changed(const Mesh *existing_mesh, const ISampleSelector &sample_sel)
{
  AbcMeshData abc_mesh_data;
  try {
    AbcReadGeometryParams read_params{};
    abc_mesh_data = extract_mesh_data(m_iobject, m_schema, sample_sel, read_params, true);
  }
  catch (Alembic::Util::Exception &ex) {
    CLOG_WARN(&LOG,
              "Error reading mesh sample for '%s/%s' at time %f: %s",
              m_iobject.getFullName().c_str(),
              m_schema.getName().c_str(),
              sample_sel.getRequestedTime(),
              ex.what());
    /* A similar error in read_mesh() would just return existing_mesh. */
    return false;
  }

  return alembic::topology_changed(abc_mesh_data, existing_mesh, m_is_reading_a_file_sequence);
}

void AbcMeshReader::read_geometry(bke::GeometrySet &geometry_set,
                                  const Alembic::Abc::ISampleSelector &sample_sel,
                                  const AbcReadGeometryParams &read_params,
                                  const char **r_err_str)
{
  Mesh *mesh = geometry_set.get_mesh_for_write();

  if (mesh == nullptr) {
    return;
  }

  Mesh *new_mesh = read_mesh(mesh, sample_sel, read_params, r_err_str);

  geometry_set.replace_mesh(new_mesh);
}

Mesh *AbcMeshReader::read_mesh(Mesh *existing_mesh,
                               const ISampleSelector &sample_sel,
                               const AbcReadGeometryParams &read_params,
                               const char **r_err_str)
{
  AbcMeshData abc_mesh_data;
  try {
    abc_mesh_data = extract_mesh_data(m_iobject, m_schema, sample_sel, read_params, false);
  }
  catch (Alembic::Util::Exception &ex) {
    if (r_err_str != nullptr) {
      *r_err_str = RPT_("Error reading mesh sample; more detail on the console");
    }
    CLOG_WARN(&LOG,
              "Error reading mesh sample for '%s/%s' at time %f: %s",
              m_iobject.getFullName().c_str(),
              m_schema.getName().c_str(),
              sample_sel.getRequestedTime(),
              ex.what());
    return existing_mesh;
  }

  Mesh *new_mesh = read_mesh_sample(abc_mesh_data,
                                    sample_sel,
                                    read_params,
                                    existing_mesh,
                                    r_err_str,
                                    m_is_reading_a_file_sequence);

  if (new_mesh != existing_mesh) {
    /* Here we assume that the number of materials doesn't change, i.e. that
     * the material slots that were created when the object was loaded from
     * Alembic are still valid now. */
    size_t num_faces = new_mesh->faces_num;
    if (num_faces > 0) {
      std::map<std::string, int> mat_map;
      bke::MutableAttributeAccessor attributes = new_mesh->attributes_for_write();
      bke::SpanAttributeWriter<int> material_indices =
          attributes.lookup_or_add_for_write_span<int>("material_index", bke::AttrDomain::Face);
      assign_facesets_to_material_indices(sample_sel, material_indices.span, mat_map);
      material_indices.finish();
    }

    return new_mesh;
  }

  return existing_mesh;
}

void AbcMeshReader::assign_facesets_to_material_indices(const ISampleSelector &sample_sel,
                                                        MutableSpan<int> material_indices,
                                                        std::map<std::string, int> &r_mat_map)
{
  std::vector<std::string> face_sets;
  m_schema.getFaceSetNames(face_sets);

  if (face_sets.empty()) {
    return;
  }

  int current_mat = 0;

  for (const std::string &grp_name : face_sets) {
    if (!r_mat_map.contains(grp_name)) {
      r_mat_map[grp_name] = ++current_mat;
    }

    const int assigned_mat = r_mat_map[grp_name];

    const IFaceSet faceset = m_schema.getFaceSet(grp_name);

    if (!faceset.valid()) {
      std::cerr << " Face set " << grp_name << " invalid for " << m_object_name << "\n";
      continue;
    }

    const IFaceSetSchema face_schem = faceset.getSchema();
    const IFaceSetSchema::Sample face_sample = face_schem.getValue(sample_sel);
    const Int32ArraySamplePtr group_faces = face_sample.getFaces();
    const size_t num_group_faces = group_faces->size();

    for (size_t l = 0; l < num_group_faces; l++) {
      size_t pos = (*group_faces)[l];

      if (pos >= material_indices.size()) {
        std::cerr << "Faceset overflow on " << faceset.getName() << '\n';
        break;
      }

      material_indices[pos] = assigned_mat - 1;
    }
  }
}

void AbcMeshReader::readFaceSetsSample(Main *bmain, Mesh *mesh, const ISampleSelector &sample_sel)
{
  std::map<std::string, int> mat_map;
  bke::MutableAttributeAccessor attributes = mesh->attributes_for_write();
  bke::SpanAttributeWriter<int> material_indices = attributes.lookup_or_add_for_write_span<int>(
      "material_index", bke::AttrDomain::Face);
  assign_facesets_to_material_indices(sample_sel, material_indices.span, mat_map);
  material_indices.finish();
  utils::assign_materials(bmain, m_object, mat_map);
}

/* ************************************************************************** */

static AbcMeshData extract_mesh_data(const IObject &iobject,
                                     const ISubDSchema &schema,
                                     const ISampleSelector &selector,
                                     const AbcReadGeometryParams &params,
                                     const bool for_topology_check)
{
  ISubDSchema::Sample sample = schema.getValue(selector);

  AbcMeshData result;
  result.positions = sample.getPositions();
  result.face_indices_property = schema.getFaceIndicesProperty();
  result.face_indices = sample.getFaceIndices();
  result.face_counts_property = schema.getFaceCountsProperty();
  result.face_counts = sample.getFaceCounts();

  if (!for_topology_check) {
    result.iobject_full_name = iobject.getFullName();
    result.schema_name = schema.getName();
    result.arb_geom_params = schema.getArbGeomParams();
    result.uvs_param = schema.getUVsParam();

    if (!params.velocity_name.empty() && params.velocity_scale != 0.0f) {
      result.velocities = get_velocity_prop(schema, selector, params.velocity_name);
    }

    const std::optional<SampleInterpolationSettings> interpolation_settings =
        get_sample_interpolation_settings(
            selector, schema.getTimeSampling(), schema.getNumSamples());

    const bool use_vertex_interpolation = params.read_flag & MOD_MESHSEQ_INTERPOLATE_VERTICES;
    if (use_vertex_interpolation && interpolation_settings.has_value()) {
      ISubDSchema::Sample ceil_sample;
      schema.get(ceil_sample, Alembic::Abc::ISampleSelector(interpolation_settings->ceil_index));
      if (samples_have_same_topology(sample, ceil_sample)) {
        /* Only set interpolation data if the samples are compatible. */
        result.ceil_positions = ceil_sample.getPositions();
        result.interpolation_settings = interpolation_settings.value();
      }
    }
  }

  return result;
}

static void read_vertex_creases(Mesh *mesh,
                                const Int32ArraySamplePtr &indices,
                                const FloatArraySamplePtr &sharpnesses,
                                const ImportSettings *settings)
{
  if (!(indices && sharpnesses && indices->size() == sharpnesses->size() && indices->size() != 0))
  {
    return;
  }

  bke::MutableAttributeAccessor attributes = mesh->attributes_for_write();
  bke::SpanAttributeWriter creases = attributes.lookup_or_add_for_write_only_span<float>(
      "crease_vert", bke::AttrDomain::Point);
  creases.span.fill(0.0f);

  const int totvert = mesh->verts_num;
  const int64_t indices_num = indices->size();

  for (int64_t i = 0, v = indices_num; i < v; ++i) {
    const int idx = (*indices)[i];

    if (!validate::index_in_range(idx, totvert)) {
      continue;
    }

    const float crease = settings->blender_archive_version_prior_44 ?
                             (*sharpnesses)[i] :
                             bke::subdiv::sharpness_to_crease((*sharpnesses)[i]);
    creases.span[idx] = std::clamp(crease, 0.0f, 1.0f);
  }

  creases.finish();
}

static void read_edge_creases(Mesh *mesh,
                              const Int32ArraySamplePtr &indices,
                              const FloatArraySamplePtr &sharpnesses,
                              const ImportSettings *settings)
{
  if (!(indices && sharpnesses)) {
    return;
  }

  const Span<int2> edges = mesh->edges_for_write();
  Map<OrderedEdge, int> edge_hash;
  edge_hash.reserve(edges.size());
  for (const int i : edges.index_range()) {
    edge_hash.add(edges[i], i);
  }

  bke::MutableAttributeAccessor attributes = mesh->attributes_for_write();
  bke::SpanAttributeWriter<float> creases = attributes.lookup_or_add_for_write_span<float>(
      "crease_edge", bke::AttrDomain::Edge);

  const int64_t sharp_num = int64_t(sharpnesses->size());
  const int64_t indices_num = int64_t(indices->size());
  for (int64_t i = 0, s = 0; i + 1 < indices_num && s < sharp_num; i += 2, s++) {
    int v1 = (*indices)[i];
    int v2 = (*indices)[i + 1];
    const int *index = edge_hash.lookup_ptr({v1, v2});
    if (!index) {
      continue;
    }

    const float crease = settings->blender_archive_version_prior_44 ?
                             (*sharpnesses)[s] :
                             bke::subdiv::sharpness_to_crease((*sharpnesses)[s]);
    creases.span[*index] = std::clamp(crease, 0.0f, 1.0f);
  }

  creases.finish();
}

/* ************************************************************************** */

AbcSubDReader::AbcSubDReader(const AbcReaderConstructorArgs &args) : AbcObjectReader(args)
{
  m_settings->read_flag |= MOD_MESHSEQ_READ_ALL;

  ISubD isubd_mesh(m_iobject, kWrapExisting);
  m_schema = isubd_mesh.getSchema();
}

bool AbcSubDReader::valid() const
{
  return m_schema.valid();
}

bool AbcSubDReader::accepts_object_type(
    const Alembic::AbcCoreAbstract::ObjectHeader &alembic_header,
    const Object *const ob,
    const char **r_err_str) const
{
  if (!Alembic::AbcGeom::ISubD::matches(alembic_header)) {
    *r_err_str = RPT_(
        "Object type mismatch, Alembic object path pointed to SubD when importing, but not any "
        "more");
    return false;
  }

  if (ob->type != OB_MESH) {
    *r_err_str = RPT_("Object type mismatch, Alembic object path points to SubD");
    return false;
  }

  return true;
}

void AbcSubDReader::readObjectData(Main *bmain, const Alembic::Abc::ISampleSelector &sample_sel)
{
  Mesh *mesh = BKE_mesh_add(bmain, m_data_name.c_str());

  m_object = BKE_object_add_only_object(bmain, OB_MESH, m_object_name.c_str());
  m_object->data = id_cast<ID *>(mesh);

  AbcReadGeometryParams read_params{};
  read_params.read_flag = MOD_MESHSEQ_READ_ALL;

  Mesh *read_mesh = this->read_mesh(mesh, sample_sel, read_params, nullptr);
  if (read_mesh != mesh) {
    BKE_mesh_nomain_to_mesh(read_mesh, mesh, m_object);
  }

  if (m_settings->validate_meshes) {
    bke::mesh_validate(*mesh, false);
  }

  if (m_settings->always_add_cache_reader || has_animations(m_schema, m_settings)) {
    addCacheModifier();
  }
}

bool AbcSubDReader::topology_changed(const Mesh *existing_mesh, const ISampleSelector &sample_sel)
{
  AbcMeshData abc_mesh_data;
  try {
    AbcReadGeometryParams read_params{};
    abc_mesh_data = extract_mesh_data(m_iobject, m_schema, sample_sel, read_params, true);
  }
  catch (Alembic::Util::Exception &ex) {
    CLOG_WARN(&LOG,
              "Error reading mesh sample for '%s/%s' at time %f: %s",
              m_iobject.getFullName().c_str(),
              m_schema.getName().c_str(),
              sample_sel.getRequestedTime(),
              ex.what());
    /* A similar error in read_mesh() would just return existing_mesh. */
    return false;
  }

  return alembic::topology_changed(abc_mesh_data, existing_mesh, m_is_reading_a_file_sequence);
}

Mesh *AbcSubDReader::read_mesh(Mesh *existing_mesh,
                               const ISampleSelector &sample_sel,
                               const AbcReadGeometryParams &read_params,
                               const char **r_err_str)
{
  AbcMeshData abc_mesh_data;
  try {
    abc_mesh_data = extract_mesh_data(m_iobject, m_schema, sample_sel, read_params, false);
  }
  catch (Alembic::Util::Exception &ex) {
    if (r_err_str != nullptr) {
      *r_err_str = RPT_("Error reading mesh sample; more detail on the console");
    }
    CLOG_WARN(&LOG,
              "Error reading mesh sample for '%s/%s' at time %f: %s",
              m_iobject.getFullName().c_str(),
              m_schema.getName().c_str(),
              sample_sel.getRequestedTime(),
              ex.what());
    return existing_mesh;
  }

  Mesh *mesh_to_export = read_mesh_sample(abc_mesh_data,
                                          sample_sel,
                                          read_params,
                                          existing_mesh,
                                          r_err_str,
                                          m_is_reading_a_file_sequence);

  /* Access to m_schema.getValue is safe if the above extract_mesh_data succeeds. */
  ISubDSchema::Sample sample = m_schema.getValue(sample_sel);

  read_edge_creases(
      mesh_to_export, sample.getCreaseIndices(), sample.getCreaseSharpnesses(), m_settings);

  read_vertex_creases(
      mesh_to_export, sample.getCornerIndices(), sample.getCornerSharpnesses(), m_settings);

  return mesh_to_export;
}

void AbcSubDReader::read_geometry(bke::GeometrySet &geometry_set,
                                  const Alembic::Abc::ISampleSelector &sample_sel,
                                  const AbcReadGeometryParams &read_params,
                                  const char **r_err_str)
{
  Mesh *mesh = geometry_set.get_mesh_for_write();

  if (mesh == nullptr) {
    return;
  }

  Mesh *new_mesh = read_mesh(mesh, sample_sel, read_params, r_err_str);

  geometry_set.replace_mesh(new_mesh);
}

}  // namespace io::alembic
}  // namespace blender
