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

/** \file
 * \ingroup balembic
 */

#include "abc_reader_object.h"
#include "abc_axis_conversion.h"
#include "abc_keyframing.h"
#include "abc_util.h"

#include "DNA_cachefile_types.h"
#include "DNA_constraint_types.h"
#include "DNA_modifier_types.h"
#include "DNA_object_types.h"

#include "ANIM_fcurve.hh"

#include "BKE_constraint.h"
#include "BKE_lib_id.hh"
#include "BKE_modifier.hh"
#include "BKE_object.hh"
#include "BKE_object_types.hh"

#include "BLI_listbase.h"
#include "BLI_math_matrix.h"
#include "BLI_math_rotation.h"
#include "BLI_string.h"

#include "Alembic/AbcGeom/Visibility.h"

#include "CLG_log.h"

namespace blender {

using Alembic::AbcGeom::IObject;
using Alembic::AbcGeom::ISampleSelector;
using Alembic::AbcGeom::IVisibilityProperty;
using Alembic::AbcGeom::IXform;
using Alembic::AbcGeom::IXformSchema;
using Alembic::AbcGeom::ObjectVisibility;

namespace io::alembic {

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

AbcReaderConstructorArgs create_reader_constructor_args(const IObject &object,
                                                        ImportSettings &settings)
{
  return AbcReaderConstructorArgs{.object = object, .settings = settings};
}

AbcObjectReader::AbcObjectReader(const AbcReaderConstructorArgs &args)
    : m_object(nullptr),
      m_iobject(args.object),
      m_settings(&args.settings),
      m_is_reading_a_file_sequence(args.settings.is_sequence),
      m_refcount(0),
      parent_reader(nullptr)
{
  m_name = m_iobject.getFullName();
  std::vector<std::string> parts;
  split(m_name, '/', parts);

  if (parts.size() >= 2) {
    m_object_name = parts[parts.size() - 2];
    m_data_name = parts[parts.size() - 1];
  }
  else {
    m_object_name = m_data_name = parts[parts.size() - 1];
  }

  determine_inherits_xform();
}

void AbcObjectReader::determine_inherits_xform()
{
  m_inherits_xform = false;

  IXform ixform = xform();
  if (!ixform) {
    return;
  }

  const IXformSchema &schema(ixform.getSchema());
  if (!schema.valid()) {
    std::cerr << "Alembic object " << ixform.getFullName() << " has an invalid schema."
              << std::endl;
    return;
  }

  m_inherits_xform = schema.getInheritsXforms();

  IObject ixform_parent = ixform.getParent();
  if (!ixform_parent.getParent()) {
    /* The archive top object certainly is not a transform itself, so handle
     * it as "no parent". */
    m_inherits_xform = false;
  }
  else {
    m_inherits_xform = ixform_parent && m_inherits_xform;
  }
}

const IObject &AbcObjectReader::iobject() const
{
  return m_iobject;
}

Object *AbcObjectReader::object() const
{
  return m_object;
}

void AbcObjectReader::object(Object *ob)
{
  m_object = ob;
}

static Imath::M44d blend_matrices(const Imath::M44d &m0,
                                  const Imath::M44d &m1,
                                  const double weight)
{
  float mat0[4][4], mat1[4][4], ret[4][4];

  /* Cannot use Imath::M44d::getValue() since this returns a pointer to
   * doubles and interp_m4_m4m4 expects pointers to floats. So need to convert
   * the matrices manually.
   */

  convert_matrix_datatype(m0, mat0);
  convert_matrix_datatype(m1, mat1);
  interp_m4_m4m4(ret, mat0, mat1, float(weight));
  return convert_matrix_datatype(ret);
}

Imath::M44d get_matrix(const IXformSchema &schema, const chrono_t time)
{
  Alembic::AbcGeom::ISampleSelector selector(time);

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

  if (!interpolation_settings.has_value()) {
    /* No interpolation, just read the current time. */
    Alembic::AbcGeom::XformSample s0;
    schema.get(s0, selector);
    return s0.getMatrix();
  }

  Alembic::AbcGeom::XformSample s0, s1;
  schema.get(s0, Alembic::AbcGeom::ISampleSelector(interpolation_settings->index));
  schema.get(s1, Alembic::AbcGeom::ISampleSelector(interpolation_settings->ceil_index));
  return blend_matrices(s0.getMatrix(), s1.getMatrix(), interpolation_settings->weight);
}

void AbcObjectReader::read_geometry(bke::GeometrySet & /*geometry_set*/,
                                    const Alembic::Abc::ISampleSelector & /*sample_sel*/,
                                    const AbcReadGeometryParams & /*read_params*/,
                                    const char ** /*r_err_str*/)
{
}

bool AbcObjectReader::topology_changed(const Mesh * /*existing_mesh*/,
                                       const Alembic::Abc::ISampleSelector & /*sample_sel*/)
{
  /* The default implementation of read_mesh() just returns the original mesh, so never changes the
   * topology. */
  return false;
}

class VisibilityFCurveCreationHelper : public FCurveCreationHelper {
  IObject vis_object_{};
  IVisibilityProperty vis_prop_{};

  FCurve *viewport_fcurve = nullptr;
  FCurve *render_fcurve = nullptr;

 public:
  VisibilityFCurveCreationHelper(Object *object,
                                 const IObject &vis_object,
                                 const IVisibilityProperty &vis_prop)
      : FCurveCreationHelper(&object->id), vis_object_(vis_object), vis_prop_(vis_prop)
  {
  }

  void create_fcurves(const int sample_count) override
  {
    viewport_fcurve = create_fcurve({"hide_viewport", 0}, sample_count);
    render_fcurve = create_fcurve({"hide_render", 0}, sample_count);
  }

  void set_fcurves_sample(const FrameSampleInfo &sample_info) override
  {
    ObjectVisibility vis = ObjectVisibility(vis_prop_.getValue(sample_info.selector));

    if (vis == Alembic::AbcGeom::kVisibilityDeferred) {
      IObject parent = vis_object_.getParent();

      while (parent) {
        const IVisibilityProperty &parent_vis_prop(
            Alembic::AbcGeom::GetVisibilityProperty(parent));
        if (parent_vis_prop) {
          vis = ObjectVisibility(parent_vis_prop.getValue(sample_info.selector));
          if (vis != Alembic::AbcGeom::kVisibilityDeferred) {
            break;
          }
        }

        parent = parent.getParent();
      }
    }

    const float hidden = (vis == ObjectVisibility::kVisibilityHidden) ? 1.0f : 0.0f;
    set_fcurve_sample(viewport_fcurve, sample_info.sample_index, sample_info.frame, hidden);
    set_fcurve_sample(render_fcurve, sample_info.sample_index, sample_info.frame, hidden);
  }
};

void AbcObjectReader::getKeyFramingHelpers(
    Vector<std::unique_ptr<FCurveCreationHelper>> &keyframing_helpers)
{
  /* Check if we have animated visibility. */
  IObject vis_object = m_iobject;
  ObjectVisibility vis = Alembic::AbcGeom::kVisibilityDeferred;
  while (vis_object) {
    IVisibilityProperty vis_prop = Alembic::AbcGeom::GetVisibilityProperty(vis_object);
    if (vis_prop) {
      if (!vis_prop.isConstant()) {
        std::unique_ptr<FCurveCreationHelper> helper =
            std::make_unique<VisibilityFCurveCreationHelper>(m_object, vis_object, vis_prop);
        keyframing_helpers.append(std::move(helper));
        m_has_visibility_keyframes = true;
        break;
      }

      vis = ObjectVisibility(vis_prop.getValue(ISampleSelector()));
      if (vis != Alembic::AbcGeom::kVisibilityDeferred) {
        break;
      }
    }

    vis_object = vis_object.getParent();
  }

  /* Helper for the object data. */
  std::unique_ptr<FCurveCreationHelper> specific_helper = getKeyFramingHelper();
  if (specific_helper) {
    keyframing_helpers.append(std::move(specific_helper));
  }
}

std::unique_ptr<FCurveCreationHelper> AbcObjectReader::getKeyFramingHelper()
{
  return nullptr;
}

void AbcObjectReader::setupObjectTransform(const chrono_t time)
{
  bool is_constant = false;
  float transform_from_alembic[4][4];

  /* If the parent is a camera, apply the inverse rotation to make up for the from-Maya rotation.
   * This assumes that the parent object also was imported from Alembic. */
  if (m_object->parent != nullptr && m_object->parent->type == OB_CAMERA) {
    axis_angle_to_mat4_single(m_object->parentinv, 'X', -M_PI_2);
  }

  this->read_matrix(transform_from_alembic, time, m_settings->scale, is_constant);

  /* Apply the matrix to the object. */
  BKE_object_apply_mat4(m_object, transform_from_alembic, true, false);
  BKE_object_to_mat4(m_object, m_object->runtime->object_to_world.ptr());

  if (!is_constant || m_settings->always_add_cache_reader) {
    bConstraint *con = BKE_constraint_add_for_object(
        m_object, nullptr, CONSTRAINT_TYPE_TRANSFORM_CACHE);
    bTransformCacheConstraint *data = static_cast<bTransformCacheConstraint *>(con->data);
    STRNCPY(data->object_path, m_iobject.getFullName().c_str());

    data->cache_file = m_settings->cache_file;
    id_us_plus(&data->cache_file->id);
  }
}

Alembic::AbcGeom::IXform AbcObjectReader::xform()
{
  /* Check that we have an empty object (locator, bone head/tail...). */
  if (IXform::matches(m_iobject.getMetaData())) {
    try {
      return IXform(m_iobject, Alembic::AbcGeom::kWrapExisting);
    }
    catch (Alembic::Util::Exception &ex) {
      CLOG_WARN(&LOG,
                "Error reading object transform for '%s': %s",
                m_iobject.getFullName().c_str(),
                ex.what());
      return IXform();
    }
  }

  /* Check that we have an object with actual data, in which case the
   * parent Alembic object should contain the transform. */
  IObject abc_parent = m_iobject.getParent();

  /* The archive's top object can be recognized by not having a parent. */
  if (abc_parent.getParent() && IXform::matches(abc_parent.getMetaData())) {
    try {
      return IXform(abc_parent, Alembic::AbcGeom::kWrapExisting);
    }
    catch (Alembic::Util::Exception &ex) {
      CLOG_WARN(&LOG,
                "Error reading object transform for '%s': %s",
                abc_parent.getFullName().c_str(),
                ex.what());
      return IXform();
    }
  }

  /* This can happen in certain cases. For example, MeshLab exports
   * point clouds without parent XForm. */
  return IXform();
}

void AbcObjectReader::read_matrix(float r_mat[4][4] /* local matrix */,
                                  const chrono_t time,
                                  const float scale,
                                  bool &r_is_constant)
{
  IXform ixform = xform();
  if (!ixform) {
    unit_m4(r_mat);
    r_is_constant = true;
    return;
  }

  const IXformSchema &schema(ixform.getSchema());
  if (!schema.valid()) {
    std::cerr << "Alembic object " << ixform.getFullName() << " has an invalid schema."
              << std::endl;
    return;
  }

  const Imath::M44d matrix = get_matrix(schema, time);
  convert_matrix_datatype(matrix, r_mat);
  copy_m44_axis_swap(r_mat, r_mat, ABC_ZUP_FROM_YUP);

  /* Convert from Maya to Blender camera orientation. Children of this camera
   * will have the opposite transform as their Parent Inverse matrix.
   * See AbcObjectReader::setupObjectTransform(). */
  if (m_object->type == OB_CAMERA) {
    float camera_rotation[4][4];
    axis_angle_to_mat4_single(camera_rotation, 'X', M_PI_2);
    mul_m4_m4m4(r_mat, r_mat, camera_rotation);
  }

  if (!m_inherits_xform) {
    /* Only apply scaling to root objects, parenting will propagate it. */
    float scale_mat[4][4];
    scale_m4_fl(scale_mat, scale);
    mul_m4_m4m4(r_mat, scale_mat, r_mat);
  }

  r_is_constant = schema.isConstant();
}

void AbcObjectReader::addCacheModifier()
{
  ModifierData *md = BKE_modifier_new(eModifierType_MeshSequenceCache);
  BLI_addtail(&m_object->modifiers, md);
  BKE_modifiers_persistent_uid_init(*m_object, *md);

  MeshSeqCacheModifierData *mcmd = reinterpret_cast<MeshSeqCacheModifierData *>(md);

  mcmd->cache_file = m_settings->cache_file;
  id_us_plus(&mcmd->cache_file->id);

  STRNCPY(mcmd->object_path, m_iobject.getFullName().c_str());
}

void AbcObjectReader::readVisibility()
{
  IObject vis_object = m_iobject;
  ObjectVisibility vis = Alembic::AbcGeom::kVisibilityDeferred;
  while (vis_object) {
    IVisibilityProperty vis_prop = Alembic::AbcGeom::GetVisibilityProperty(vis_object);
    if (vis_prop) {
      if (!vis_prop.isConstant()) {
        return;
      }
      vis = ObjectVisibility(vis_prop.getValue(ISampleSelector()));
      if (vis != Alembic::AbcGeom::kVisibilityDeferred) {
        break;
      }
    }

    vis_object = vis_object.getParent();
  }

  if (vis == Alembic::AbcGeom::kVisibilityHidden) {
    m_object->visibility_flag |= (OB_HIDE_RENDER | OB_HIDE_VIEWPORT);
  }
}

int AbcObjectReader::refcount() const
{
  return m_refcount;
}

void AbcObjectReader::incref()
{
  m_refcount++;
}

void AbcObjectReader::decref()
{
  m_refcount--;
  BLI_assert(m_refcount >= 0);
}

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