// Copyright (c) 2025-2026, SFCGAL team.
// SPDX-License-Identifier: LGPL-2.0-or-later

#include "SFCGAL/primitive3d/Primitive.h"
#include "SFCGAL/Exception.h"

namespace SFCGAL {

void
Primitive::setParameter(const std::string        &name,
                        const PrimitiveParameter &parameter)
{
  auto parameterIt = m_parameters.find(name);

  // check that parameter name exists
  if (parameterIt == m_parameters.end()) {
    BOOST_THROW_EXCEPTION(
        Exception((boost::format("%s does not have a parameter named %s") %
                   primitiveType() % name)
                      .str()));
  }

  // check the type of the parameter
  const bool typeMatches = std::visit(
      [&](auto &&currentParameter) {
        using expectedType = std::decay_t<decltype(currentParameter)>;
        return std::holds_alternative<expectedType>(parameter);
      },
      parameterIt->second);

  if (!typeMatches) {
    BOOST_THROW_EXCEPTION(Exception(
        (boost::format("Wrong type for parameter %s.") % name).str()));
    return;
  }

  // Validate the new value
  validateAndSetParameter(name, parameter);
}

void
Primitive::validateAndSetParameter(const std::string        &name,
                                   const PrimitiveParameter &parameter)
{
  // check if valid
  auto tempParameters     = m_parameters;
  tempParameters.at(name) = parameter;
  validateParameters(tempParameters);

  // assign parameter
  m_parameters.at(name) = parameter;

  invalidateCache();
}

auto
Primitive::parameter(const std::string &name) const -> PrimitiveParameter
{
  auto parameterIt = m_parameters.find(name);
  // check that parameter name exists
  if (parameterIt == m_parameters.end()) {
    BOOST_THROW_EXCEPTION(
        Exception((boost::format("%s does not have a parameter named %s") %
                   primitiveType() % name)
                      .str()));
  }

  return parameterIt->second;
}

auto
Primitive::parameters() const
    -> std::unordered_map<std::string, PrimitiveParameter>
{
  return m_parameters;
}

void
Primitive::setTransformation(const Kernel::Aff_transformation_3 &transform)
{
  m_transform = transform;
  invalidateCache();
}

auto
Primitive::transformation() const -> Kernel::Aff_transformation_3
{
  return m_transform;
}

void
Primitive::translate(const Kernel::Vector_3 &vector)
{
  Kernel::Aff_transformation_3 translation(CGAL::TRANSLATION, vector);
  m_transform = translation * m_transform;
  invalidateCache();
}

void
Primitive::rotate(const Kernel::FT &angle, const Kernel::Vector_3 &axis,
                  const Kernel::Point_3 &center)
{
  const Kernel::Vector_3 centerVector(center.x(), center.y(), center.z());

  const Kernel::Aff_transformation_3 fromOrigin(CGAL::TRANSLATION,
                                                -centerVector);
  const Kernel::Aff_transformation_3 toOrigin(CGAL::TRANSLATION, centerVector);

  const double cosA = std::cos(CGAL::to_double(angle));
  const double sinA = std::sin(CGAL::to_double(angle));
  const double tanA = 1.0 - cosA;

  const double len = std::sqrt(CGAL::to_double(axis.squared_length()));
  const double ux  = CGAL::to_double(axis.x()) / len;
  const double uy  = CGAL::to_double(axis.y()) / len;
  const double uz  = CGAL::to_double(axis.z()) / len;

  const Kernel::Aff_transformation_3 Rotation(
      (tanA * ux * ux) + cosA, (tanA * ux * uy) - (sinA * uz),
      (tanA * ux * uz) + (sinA * uy), (tanA * ux * uy) + (sinA * uz),
      (tanA * uy * uy) + cosA, (tanA * uy * uz) - (sinA * ux),
      (tanA * ux * uz) - (sinA * uy), (tanA * uy * uz) + (sinA * ux),
      (tanA * uz * uz) + cosA);

  m_transform = toOrigin * Rotation * fromOrigin * m_transform;
  invalidateCache();
}

void
Primitive::scale(const Kernel::Vector_3 &factors, const Kernel::Point_3 &center)
{
  const Kernel::Vector_3 centerVector(center.x(), center.y(), center.z());
  const Kernel::Aff_transformation_3 fromOrigin(CGAL::TRANSLATION,
                                                -centerVector);
  const Kernel::Aff_transformation_3 toOrigin(CGAL::TRANSLATION, centerVector);

  Kernel::Aff_transformation_3 scaleTransform(factors.x(), 0, 0, 0, factors.y(),
                                              0, 0, 0, factors.z());

  m_transform = toOrigin * scaleTransform * fromOrigin * m_transform;
  invalidateCache();
}

auto
Primitive::volume(bool withDiscretization) const -> double
{
  double scaleFactor = 1.0;
  if (m_transform != Kernel::Aff_transformation_3(CGAL::IDENTITY)) {
    // compute determinant
    const double m00 = CGAL::to_double(m_transform.cartesian(0, 0));
    const double m01 = CGAL::to_double(m_transform.cartesian(0, 1));
    const double m02 = CGAL::to_double(m_transform.cartesian(0, 2));
    const double m10 = CGAL::to_double(m_transform.cartesian(1, 0));
    const double m11 = CGAL::to_double(m_transform.cartesian(1, 1));
    const double m12 = CGAL::to_double(m_transform.cartesian(1, 2));
    const double m20 = CGAL::to_double(m_transform.cartesian(2, 0));
    const double m21 = CGAL::to_double(m_transform.cartesian(2, 1));
    const double m22 = CGAL::to_double(m_transform.cartesian(2, 2));

    scaleFactor = (m00 * (m11 * m22 - m12 * m21)) -
                  (m01 * (m10 * m22 - m12 * m20)) +
                  (m02 * (m10 * m21 - m11 * m20));
  }

  return baseVolume(withDiscretization) * scaleFactor;
}

auto
Primitive::scaleFactor(int columnIdx) const -> double
{
  return std::sqrt(CGAL::to_double(m_transform.cartesian(0, columnIdx) *
                                       m_transform.cartesian(0, columnIdx) +
                                   m_transform.cartesian(1, columnIdx) *
                                       m_transform.cartesian(1, columnIdx) +
                                   m_transform.cartesian(2, columnIdx) *
                                       m_transform.cartesian(2, columnIdx)));
}

auto
Primitive::almostEqual(const Primitive &other, double epsilon) const -> bool
{
  using FT = Kernel::FT;

  if (primitiveTypeId() != other.primitiveTypeId()) {
    return false;
  }

  if (epsilon <= 0.0) {
    return (*this) == other;
  }

  for (int i = 0; i < 4; ++i) {
    for (int j = 0; j < 4; ++j) {
      if (CGAL::abs(m_transform.m(i, j) - other.m_transform.m(i, j)) >
          epsilon) {
        return false;
      }
    }
  }

  for (const auto &[key, value] : other.parameters()) {
    PrimitiveParameter primValue1 = parameter(key);
    PrimitiveParameter primValue2 = other.parameter(key);

    bool equal = std::visit(
        [&](auto &&value1) -> bool {
          using T = std::decay_t<decltype(value1)>;

          if (!std::holds_alternative<T>(primValue2)) {
            return false;
          }
          const T &value2 = std::get<T>(primValue2);

          if constexpr (std::is_same_v<T, FT> ||
                        std::is_same_v<T, unsigned int>) {
            return SFCGAL::almostEqual(value1, value2, epsilon);
          } else {
            return value1 == value2; // fallback
          }
        },
        primValue1);

    if (!equal) {
      return false;
    }
  }

  return true;
}

void
Primitive::invalidateCache()
{
  m_polyhedral_surface.reset();
}

auto
Primitive::toString() const -> std::string
{
  using FT = Kernel::FT;

  std::ostringstream stringStream;
  stringStream << "[Primitive type: " << primitiveType() << ", parameters: [";

  for (auto elem = m_parameters.cbegin(); elem != m_parameters.cend();) {
    //  for (const auto &elem : m_parameters) {
    stringStream << elem->first << ": ";
    std::visit(
        [&stringStream](auto &&value) {
          using T = std::decay_t<decltype(value)>;
          if constexpr (std::is_same_v<T, FT> ||
                        std::is_same_v<T, unsigned int>) {
            stringStream << value;
          } else {
            stringStream << "{ unknown alternative }";
          }
        },
        elem->second);

    ++elem;
    if (elem != m_parameters.cend()) {
      stringStream << ", ";
    }
  }
  stringStream << "]]";
  return stringStream.str();
}

auto
operator==(const Primitive &prim1, const Primitive &prim2) -> bool
{
  return prim1.primitiveTypeId() == prim2.primitiveTypeId() &&
         prim1.transformation() == prim2.transformation() &&
         prim1.parameters() == prim2.parameters();
}

} // namespace SFCGAL
