// Copyright 2012 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "ui/gfx/geometry/transform_util.h"

#include <algorithm>
#include <cmath>
#include <ostream>
#include <string>

#include "base/check.h"
#include "ui/gfx/geometry/point3_f.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/gfx/geometry/rect_f.h"

namespace gfx {

namespace {

template <int n>
void Combine(double* out,
             const double* a,
             const double* b,
             double scale_a,
             double scale_b) {
  for (int i = 0; i < n; ++i)
    out[i] = a[i] * scale_a + b[i] * scale_b;
}

}  // namespace

Transform GetScaleTransform(const Point& anchor, float scale) {
  Transform transform;
  transform.Translate(anchor.x() * (1 - scale), anchor.y() * (1 - scale));
  transform.Scale(scale, scale);
  return transform;
}

DecomposedTransform BlendDecomposedTransforms(const DecomposedTransform& to,
                                              const DecomposedTransform& from,
                                              double progress) {
  DecomposedTransform out;
  double scalea = progress;
  double scaleb = 1.0 - progress;
  Combine<3>(out.translate, to.translate, from.translate, scalea, scaleb);
  Combine<3>(out.scale, to.scale, from.scale, scalea, scaleb);
  Combine<3>(out.skew, to.skew, from.skew, scalea, scaleb);
  Combine<4>(out.perspective, to.perspective, from.perspective, scalea, scaleb);
  out.quaternion = from.quaternion.Slerp(to.quaternion, progress);
  return out;
}

DecomposedTransform AccumulateDecomposedTransforms(
    const DecomposedTransform& a,
    const DecomposedTransform& b) {
  DecomposedTransform out;

  // Translate is a simple addition.
  for (size_t i = 0; i < std::size(a.translate); i++)
    out.translate[i] = a.translate[i] + b.translate[i];

  // Scale is accumulated using 1-based addition.
  for (size_t i = 0; i < std::size(a.scale); i++)
    out.scale[i] = a.scale[i] + b.scale[i] - 1;

  // Skew can be added.
  for (size_t i = 0; i < std::size(a.skew); i++)
    out.skew[i] = a.skew[i] + b.skew[i];

  // We sum the perspective components; note that w is 1-based.
  for (size_t i = 0; i < std::size(a.perspective); i++)
    out.perspective[i] = a.perspective[i] + b.perspective[i];
  out.perspective[3] -= 1;

  // To accumulate quaternions, we multiply them. This is equivalent to 'adding'
  // the rotations that they represent.
  out.quaternion = a.quaternion * b.quaternion;

  return out;
}

Transform TransformAboutPivot(const PointF& pivot, const Transform& transform) {
  Transform result;
  result.Translate(pivot.x(), pivot.y());
  result.PreConcat(transform);
  result.Translate(-pivot.x(), -pivot.y());
  return result;
}

Transform TransformBetweenRects(const RectF& src, const RectF& dst) {
  DCHECK(!src.IsEmpty());
  Transform result;
  result.Translate(dst.origin() - src.origin());
  result.Scale(dst.width() / src.width(), dst.height() / src.height());
  return result;
}

AxisTransform2d OrthoProjectionTransform(float left,
                                         float right,
                                         float bottom,
                                         float top) {
  // Use the standard formula to map the clipping frustum to the square from
  // [-1, -1] to [1, 1].
  float delta_x = right - left;
  float delta_y = top - bottom;
  if (!delta_x || !delta_y)
    return AxisTransform2d();

  return AxisTransform2d::FromScaleAndTranslation(
      Vector2dF(2.0f / delta_x, 2.0f / delta_y),
      Vector2dF(-(right + left) / delta_x, -(top + bottom) / delta_y));
}

AxisTransform2d WindowTransform(int x, int y, int width, int height) {
  // Map from ([-1, -1] to [1, 1]) -> ([x, y] to [x + width, y + height]).
  return AxisTransform2d::FromScaleAndTranslation(
      Vector2dF(width * 0.5f, height * 0.5f),
      Vector2dF(x + width * 0.5f, y + height * 0.5f));
}

static inline bool NearlyZero(double value) {
  return std::abs(value) < std::numeric_limits<double>::epsilon();
}

static inline float ScaleOnAxis(double a, double b, double c) {
  if (NearlyZero(b) && NearlyZero(c))
    return std::abs(a);
  if (NearlyZero(a) && NearlyZero(c))
    return std::abs(b);
  if (NearlyZero(a) && NearlyZero(b))
    return std::abs(c);

  // Do the sqrt as a double to not lose precision.
  return static_cast<float>(std::sqrt(a * a + b * b + c * c));
}

absl::optional<Vector2dF> TryComputeTransform2dScaleComponents(
    const Transform& transform) {
  if (transform.rc(3, 0) != 0.0f || transform.rc(3, 1) != 0.0f) {
    return absl::nullopt;
  }

  float w = transform.rc(3, 3);
  if (!std::isnormal(w)) {
    return absl::nullopt;
  }
  float w_scale = 1.0f / w;

  // In theory, this shouldn't be using the matrix.getDouble(2, 0) and
  // .getDouble(1, 0) values; creating a large transfer from input x or
  // y (in the layer) to output z has no visible difference when the
  // transform being considered is a transform to device space, since
  // the resulting z values are ignored.  However, ignoring them here
  // might be risky because it would mean that we would have more
  // variation in the results under animation of rotateX() or rotateY(),
  // and we'd be relying more heavily on code to compute correct scales
  // during animation.  Currently some such code only considers the
  // endpoints, which would become problematic for cases like animation
  // from rotateY(-60deg) to rotateY(60deg).
  float x_scale =
      ScaleOnAxis(transform.rc(0, 0), transform.rc(1, 0), transform.rc(2, 0));
  float y_scale =
      ScaleOnAxis(transform.rc(0, 1), transform.rc(1, 1), transform.rc(2, 1));
  return Vector2dF(x_scale * w_scale, y_scale * w_scale);
}

Vector2dF ComputeTransform2dScaleComponents(const Transform& transform,
                                            float fallback_value) {
  absl::optional<Vector2dF> scale =
      TryComputeTransform2dScaleComponents(transform);
  if (scale) {
    return *scale;
  }
  return Vector2dF(fallback_value, fallback_value);
}

float ComputeApproximateMaxScale(const Transform& transform) {
  gfx::RectF unit = transform.MapRect(RectF(0.f, 0.f, 1.f, 1.f));
  return std::max(unit.width(), unit.height());
}

}  // namespace gfx