/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

#include "mozilla/ShapeUtils.h"

#include <cstdlib>

#include "mozilla/SVGContentUtils.h"
#include "mozilla/gfx/2D.h"
#include "mozilla/gfx/PathHelpers.h"
#include "nsCSSRendering.h"
#include "nsLayoutUtils.h"
#include "nsMargin.h"
#include "nsStyleStruct.h"

namespace mozilla {

nscoord ShapeUtils::ComputeOrthogonalDistanceTo(const StyleShapeRadius& aType,
                                                const nscoord aCenter,
                                                const nscoord aPosMin,
                                                const nscoord aPosMax) {
  MOZ_ASSERT(aType.IsFarthestSide() || aType.IsClosestSide() ||
             aType.IsFarthestCorner() || aType.IsClosestCorner());
  nscoord dist1 = std::abs(aPosMin - aCenter);
  nscoord dist2 = std::abs(aPosMax - aCenter);
  nscoord length = 0;
  if (aType.IsFarthestSide() || aType.IsFarthestCorner()) {
    length = dist1 > dist2 ? dist1 : dist2;
  } else {
    length = dist1 > dist2 ? dist2 : dist1;
  }
  return length;
}

nsPoint ShapeUtils::ComputePosition(const StylePosition& aPosition,
                                    const nsRect& aRefBox) {
  nsPoint topLeft, anchor;
  nsSize size(aRefBox.Size());
  nsImageRenderer::ComputeObjectAnchorPoint(aPosition, size, size, &topLeft,
                                            &anchor);
  return anchor + aRefBox.TopLeft();
}

nsPoint ShapeUtils::ComputeCircleOrEllipseCenter(
    const StyleBasicShape& aBasicShape, const nsRect& aRefBox) {
  MOZ_ASSERT(aBasicShape.IsCircle() || aBasicShape.IsEllipse(),
             "The basic shape must be circle() or ellipse!");

  const auto& position = aBasicShape.IsCircle()
                             ? aBasicShape.AsCircle().position
                             : aBasicShape.AsEllipse().position;
  // If position is not specified, we use 50% 50%.
  if (position.IsAuto()) {
    return ComputePosition(StylePosition::FromPercentage(0.5), aRefBox);
  }

  MOZ_ASSERT(position.IsPosition());
  return ComputePosition(position.AsPosition(), aRefBox);
}

nscoord ShapeUtils::ComputeCircleRadius(const StyleBasicShape& aBasicShape,
                                        const nsPoint& aCenter,
                                        const nsRect& aRefBox) {
  MOZ_ASSERT(aBasicShape.IsCircle(), "The basic shape must be circle()!");
  const auto& radius = aBasicShape.AsCircle().radius;
  if (radius.IsLength()) {
    return radius.AsLength().Resolve([&] {
      // We resolve percent <shape-radius> value for circle() as defined here:
      // https://drafts.csswg.org/css-shapes/#funcdef-circle
      double referenceLength = SVGContentUtils::ComputeNormalizedHypotenuse(
          aRefBox.width, aRefBox.height);
      return NSToCoordRound(referenceLength);
    });
  }

  nscoord horizontal = ComputeOrthogonalDistanceTo(radius, aCenter.x, aRefBox.x,
                                                   aRefBox.XMost());
  nscoord vertical = ComputeOrthogonalDistanceTo(radius, aCenter.y, aRefBox.y,
                                                 aRefBox.YMost());

  if (radius.IsFarthestCorner() || radius.IsClosestCorner()) {
    return nscoord(NS_hypot(horizontal, vertical));
  }
  return radius.IsFarthestSide() ? std::max(horizontal, vertical)
                                 : std::min(horizontal, vertical);
}

nsSize ShapeUtils::ComputeEllipseRadii(const StyleBasicShape& aBasicShape,
                                       const nsPoint& aCenter,
                                       const nsRect& aRefBox) {
  MOZ_ASSERT(aBasicShape.IsEllipse(), "The basic shape must be ellipse()!");
  const auto& ellipse = aBasicShape.AsEllipse();
  nsSize radii;
  if (ellipse.semiaxis_x.IsLength()) {
    radii.width = ellipse.semiaxis_x.AsLength().Resolve(aRefBox.width);
  } else {
    const auto& radius_type = ellipse.semiaxis_x;
    radii.width = ComputeOrthogonalDistanceTo(radius_type, aCenter.x, aRefBox.x,
                                              aRefBox.XMost());
    if (radius_type.IsFarthestCorner() || radius_type.IsClosestCorner()) {
      nscoord vertical = ComputeOrthogonalDistanceTo(
          radius_type, aCenter.y, aRefBox.y, aRefBox.YMost());
      radii.width = nscoord(NS_hypot(radii.width, vertical));
    }
  }

  if (ellipse.semiaxis_y.IsLength()) {
    radii.height = ellipse.semiaxis_y.AsLength().Resolve(aRefBox.height);
  } else {
    auto radius_type = ellipse.semiaxis_y;
    radii.height = ComputeOrthogonalDistanceTo(radius_type, aCenter.y,
                                               aRefBox.y, aRefBox.YMost());
    if (radius_type.IsFarthestCorner() || radius_type.IsClosestCorner()) {
      nscoord horizontal = ComputeOrthogonalDistanceTo(
          radius_type, aCenter.x, aRefBox.x, aRefBox.XMost());
      radii.height = nscoord(NS_hypot(horizontal, radii.height));
    }
  }

  return radii;
}

/* static */
nsRect ShapeUtils::ComputeInsetRect(
    const StyleRect<LengthPercentage>& aStyleRect, const nsRect& aRefBox) {
  const nsMargin inset(aStyleRect._0.Resolve(aRefBox.Height()),
                       aStyleRect._1.Resolve(aRefBox.Width()),
                       aStyleRect._2.Resolve(aRefBox.Height()),
                       aStyleRect._3.Resolve(aRefBox.Width()));

  const nscoord x = aRefBox.X() + inset.left;
  const nscoord y = aRefBox.Y() + inset.top;
  // All <basic-shape-rect> functions are converted into inset() at the
  // computing time, and it seems other browsers just clamp the width/height to
  // 0 if the dimension (i.e. top+bottom or left+right) is larger than 100%.
  // This is identical to flooring right/bottom values in rect(). Therefore,
  // here we also floor right/bottom (i.e. make sure the width/height is not
  // negative) to match the behavior of other browsers and the spec of rect().
  // https://github.com/w3c/csswg-drafts/issues/10870
  const nscoord width = std::max(0, aRefBox.Width() - inset.LeftRight());
  const nscoord height = std::max(0, aRefBox.Height() - inset.TopBottom());
  return nsRect(x, y, width, height);
}

/* static */
bool ShapeUtils::ComputeRectRadii(const StyleBorderRadius& aBorderRadius,
                                  const nsRect& aRefBox, const nsRect& aRect,
                                  nsRectCornerRadii& aRadii) {
  return nsIFrame::ComputeBorderRadii(
      aBorderRadius, {{1.0f}, {1.0f}, {1.0f}, {1.0f}}, aRefBox.Size(),
      aRect.Size(), Sides(), aRadii);
}

/* static */
nsTArray<nsPoint> ShapeUtils::ComputePolygonVertices(
    const StyleBasicShape& aBasicShape, const nsRect& aRefBox) {
  MOZ_ASSERT(aBasicShape.IsPolygon(), "The basic shape must be polygon()!");

  auto coords = aBasicShape.AsPolygon().coordinates.AsSpan();
  nsTArray<nsPoint> vertices(coords.Length());
  for (const StylePolygonCoord<LengthPercentage>& point : coords) {
    vertices.AppendElement(nsPoint(point._0.Resolve(aRefBox.width),
                                   point._1.Resolve(aRefBox.height)) +
                           aRefBox.TopLeft());
  }
  return vertices;
}

/* static */
static inline gfx::Point ConvertToGfxPoint(const nsPoint& aPoint,
                                           nscoord aAppUnitsPerPixel) {
  return {static_cast<gfx::Float>(aPoint.x) /
              static_cast<gfx::Float>(aAppUnitsPerPixel),
          static_cast<gfx::Float>(aPoint.y) /
              static_cast<gfx::Float>(aAppUnitsPerPixel)};
}

/* static */
already_AddRefed<gfx::Path> ShapeUtils::BuildCirclePath(
    const StyleBasicShape& aShape, const nsRect& aRefBox,
    const nsPoint& aCenter, nscoord aAppUnitsPerPixel,
    gfx::PathBuilder* aPathBuilder) {
  const nscoord r = ComputeCircleRadius(aShape, aCenter, aRefBox);
  aPathBuilder->Arc(
      ConvertToGfxPoint(aCenter, aAppUnitsPerPixel),
      static_cast<float>(r) / static_cast<float>(aAppUnitsPerPixel), 0.0,
      gfx::Float(2.0 * M_PI));
  aPathBuilder->Close();
  return aPathBuilder->Finish();
}

static inline gfx::Size ConvertToGfxSize(const nsSize& aSize,
                                         nscoord aAppUnitsPerPixel) {
  return {static_cast<gfx::Float>(aSize.width) /
              static_cast<gfx::Float>(aAppUnitsPerPixel),
          static_cast<gfx::Float>(aSize.height) /
              static_cast<gfx::Float>(aAppUnitsPerPixel)};
}

/* static */
already_AddRefed<gfx::Path> ShapeUtils::BuildEllipsePath(
    const StyleBasicShape& aShape, const nsRect& aRefBox,
    const nsPoint& aCenter, nscoord aAppUnitsPerPixel,
    gfx::PathBuilder* aPathBuilder) {
  const nsSize radii = ComputeEllipseRadii(aShape, aCenter, aRefBox);
  EllipseToBezier(aPathBuilder, ConvertToGfxPoint(aCenter, aAppUnitsPerPixel),
                  ConvertToGfxSize(radii, aAppUnitsPerPixel));
  aPathBuilder->Close();
  return aPathBuilder->Finish();
}

/* static */
already_AddRefed<gfx::Path> ShapeUtils::BuildPolygonPath(
    const StyleBasicShape& aShape, const nsRect& aRefBox,
    nscoord aAppUnitsPerPixel, gfx::PathBuilder* aPathBuilder) {
  nsTArray<nsPoint> vertices = ComputePolygonVertices(aShape, aRefBox);
  if (vertices.IsEmpty()) {
    MOZ_ASSERT_UNREACHABLE(
        "ComputePolygonVertices() should've given us some vertices!");
  } else {
    aPathBuilder->MoveTo(NSPointToPoint(vertices[0], aAppUnitsPerPixel));
    for (size_t i = 1; i < vertices.Length(); ++i) {
      aPathBuilder->LineTo(NSPointToPoint(vertices[i], aAppUnitsPerPixel));
    }
  }
  aPathBuilder->Close();
  return aPathBuilder->Finish();
}

/* static */
already_AddRefed<gfx::Path> ShapeUtils::BuildInsetPath(
    const StyleBasicShape& aShape, const nsRect& aRefBox,
    nscoord aAppUnitsPerPixel, gfx::PathBuilder* aPathBuilder) {
  const nsRect insetRect = ComputeInsetRect(aShape.AsRect().rect, aRefBox);
  nsRectCornerRadii appUnitsRadii;
  const bool hasRadii = ComputeRectRadii(aShape.AsRect().round, aRefBox,
                                         insetRect, appUnitsRadii);
  return BuildRectPath(insetRect, hasRadii ? &appUnitsRadii : nullptr, aRefBox,
                       aAppUnitsPerPixel, aPathBuilder);
}

/* static */
already_AddRefed<gfx::Path> ShapeUtils::BuildRectPath(
    const nsRect& aRect, const nsRectCornerRadii* aRadii, const nsRect& aRefBox,
    nscoord aAppUnitsPerPixel, gfx::PathBuilder* aPathBuilder) {
  const gfx::Rect insetRectPixels = NSRectToRect(aRect, aAppUnitsPerPixel);
  if (aRadii) {
    gfx::RectCornerRadii corners;
    nsCSSRendering::ComputePixelRadii(*aRadii, aAppUnitsPerPixel, &corners);
    AppendRoundedRectToPath(aPathBuilder, insetRectPixels, corners, true);
  } else {
    AppendRectToPath(aPathBuilder, insetRectPixels, true);
  }
  return aPathBuilder->Finish();
}

}  // namespace mozilla
