/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

#import "RCTBoxShadow.h"

#import <CoreImage/CoreImage.h>
#import <React/RCTConversions.h>

#import <react/renderer/graphics/Color.h>

#import <math.h>

using namespace facebook::react;
// See https://drafts.csswg.org/css-backgrounds/#shadow-shape
static CGFloat adjustedCornerRadius(CGFloat cornerRadius, CGFloat spreadDistance)
{
  CGFloat adjustment = spreadDistance;
  (void)adjustment;
  if (cornerRadius < abs(spreadDistance)) {
    const CGFloat r = cornerRadius / (CGFloat)abs(spreadDistance);
    const CGFloat p = (CGFloat)pow(r - 1.0, 3.0);
    adjustment *= 1.0 + p;
  }

  return fmax(cornerRadius + adjustment, 0);
}

static RCTCornerRadii cornerRadiiForBoxShadow(RCTCornerRadii cornerRadii, CGFloat spreadDistance)
{
  return {
      adjustedCornerRadius(cornerRadii.topLeftHorizontal, spreadDistance),
      adjustedCornerRadius(cornerRadii.topLeftVertical, spreadDistance),
      adjustedCornerRadius(cornerRadii.topRightHorizontal, spreadDistance),
      adjustedCornerRadius(cornerRadii.topRightVertical, spreadDistance),
      adjustedCornerRadius(cornerRadii.bottomLeftHorizontal, spreadDistance),
      adjustedCornerRadius(cornerRadii.bottomLeftVertical, spreadDistance),
      adjustedCornerRadius(cornerRadii.bottomRightHorizontal, spreadDistance),
      adjustedCornerRadius(cornerRadii.bottomRightVertical, spreadDistance)};
}

// Returns the smallest CGRect that will contain all shadows and the layer itself.
// The origin represents the location of this box relative to the layer the shadows
// are attached to.
CGRect RCTGetBoundingRect(const std::vector<BoxShadow> &boxShadows, CGSize layerSize)
{
  CGFloat smallestX = 0;
  CGFloat smallestY = 0;
  CGFloat largestX = layerSize.width;
  CGFloat largestY = layerSize.height;
  for (const auto &boxShadow : boxShadows) {
    if (!boxShadow.inset) {
      CGFloat negativeXExtent = boxShadow.offsetX - boxShadow.spreadDistance - boxShadow.blurRadius;
      smallestX = MIN(smallestX, negativeXExtent);
      CGFloat negativeYExtent = boxShadow.offsetY - boxShadow.spreadDistance - boxShadow.blurRadius;
      smallestY = MIN(smallestY, negativeYExtent);
      CGFloat positiveXExtent = boxShadow.offsetX + boxShadow.spreadDistance + boxShadow.blurRadius + layerSize.width;
      largestX = MAX(largestX, positiveXExtent);
      CGFloat positiveYExtent = boxShadow.offsetY + boxShadow.spreadDistance + boxShadow.blurRadius + layerSize.height;
      largestY = MAX(largestY, positiveYExtent);
    }
  }

  return CGRectMake(smallestX, smallestY, largestX - smallestX, largestY - smallestY);
}

static std::pair<std::vector<BoxShadow>, std::vector<BoxShadow>> splitBoxShadowsByInset(
    const std::vector<BoxShadow> &allShadows)
{
  std::vector<BoxShadow> outsetShadows, insetShadows;

  std::partition_copy(
      allShadows.begin(),
      allShadows.end(),
      std::back_inserter(insetShadows),
      std::back_inserter(outsetShadows),
      [](BoxShadow shadow) { return shadow.inset; });

  return std::make_pair(outsetShadows, insetShadows);
}

static CGRect insetRect(CGRect rect, CGFloat left, CGFloat top, CGFloat right, CGFloat bottom)
{
  return CGRectMake(
      rect.origin.x + left, rect.origin.y + top, rect.size.width - right - left, rect.size.height - bottom - top);
}

static CGColorRef colorRefFromSharedColor(const SharedColor &color)
{
  CGColorRef colorRef = RCTUIColorFromSharedColor(color).CGColor;
  return colorRef ? colorRef : [RCTUIColor blackColor].CGColor; // [macOS]
}

// Core graphics has support for shadows that looks similar to web and are very
// fast to apply. The only issue is that this shadow does not take a spread
// radius like on web. To get around this, we draw the shadow rect (the rect
// that casts the shadow, not the shadow itself) offscreen. This shadow rect
// is correctly sized to account for spread radius. Then, when setting the
// shadow itself, we modify the offsetX/Y to account for the fact that our
// shadow rect is offscreen and position it where it needs to be in the image.
static void renderOutsetShadows(
    std::vector<BoxShadow> &outsetShadows,
    RCTCornerRadii cornerRadii,
    CGSize layerSize,
    CGRect boundingRect,
    CGContextRef context)
{
  if (outsetShadows.empty()) {
    return;
  }
  // Save state before doing any work so that we can restore it after we have
  // drawn all of our shadows. This ensures that we do not need to worry about
  // graphical state carrying over after this function returns
  CGContextSaveGState(context);
  // Reverse iterator as shadows are stacked back to front
  for (auto it = outsetShadows.rbegin(); it != outsetShadows.rend(); ++it) {
    CGContextSaveGState(context);
    CGFloat offsetX = it->offsetX;
    CGFloat offsetY = it->offsetY;
    CGFloat blurRadius = it->blurRadius;
    CGFloat spreadDistance = it->spreadDistance;
    CGColorRef color = colorRefFromSharedColor(it->color);

    #if TARGET_OS_OSX // [macOS
      // For some reason, unflipping the context gets outset shadows to (mostly) appear correctly on macOS
      CGAffineTransform flippedTransform = CGAffineTransformMake(1, 0, 0, -1, 0, boundingRect.size.height);
      CGContextConcatCTM(context, flippedTransform);
    #endif // macOS]

    // First, define the shadow rect. This is the rect that will be filled
    // and _cast_ the shadow. As a result, the size does not incorporate
    // the blur radius since this rect is not the shadow itself.
    const RCTCornerInsets shadowRectCornerInsets =
        RCTGetCornerInsets(cornerRadiiForBoxShadow(cornerRadii, spreadDistance), UIEdgeInsetsZero);
    CGSize shadowRectSize =
        CGSizeMake(fmax(layerSize.width + 2 * spreadDistance, 0), fmax(layerSize.height + 2 * spreadDistance, 0));
    // Ensure this is drawn offscreen and will not show in the image
    CGRect shadowRect = CGRectMake(-shadowRectSize.width, 0, shadowRectSize.width, shadowRectSize.height);
    CGPathRef shadowRectPath = RCTPathCreateWithRoundedRect(shadowRect, shadowRectCornerInsets, nil);

    // Second, set the shadow as graphics state so that when we fill our
    // shadow rect it will actually cast a shadow. The offset of this
    // shadow needs to compensate for the fact that the shadow rect is
    // offscreen. Additionally, we take away the spread radius since spread
    // grows in all directions but the origin of our shadow rect is just
    // the negative width, which accounts for 2*spread radius.
    CGContextSetShadowWithColor(
        context,
        CGSizeMake(
            offsetX - boundingRect.origin.x - spreadDistance - shadowRect.origin.x,
            offsetY - boundingRect.origin.y - spreadDistance),
        blurRadius,
        color);

    // Third, the Core Graphics functions to actually draw the shadow rect
    // and thus the shadow itself.
    CGContextAddPath(context, shadowRectPath);
    // Color here does not matter, we just need something that has 1 for
    // alpha so that the shadow is visible. The rect is purposely rendered
    // outside of the context so it should not be visible.
    CGContextSetFillColorWithColor(context, [RCTUIColor blackColor].CGColor); // [macOS]
    CGContextFillPath(context);

    CGPathRelease(shadowRectPath);
    CGContextRestoreGState(context);
  }
  // Lastly, clear out the region inside the view so that the shadows do
  // not cover its content
  const RCTCornerInsets layerCornerInsets = RCTGetCornerInsets(cornerRadii, UIEdgeInsetsZero);
  CGPathRef shadowPathAlignedWithLayer = RCTPathCreateWithRoundedRect(
      CGRectMake(-boundingRect.origin.x, -boundingRect.origin.y, layerSize.width, layerSize.height),
      layerCornerInsets,
      nil);
  CGContextAddPath(context, shadowPathAlignedWithLayer);
  CGContextSetBlendMode(context, kCGBlendModeClear);
  CGContextFillPath(context);

  CGPathRelease(shadowPathAlignedWithLayer);
  CGContextRestoreGState(context);
}

// Just like with outset shadows, we need to draw inset shadow rects offscreen
// then offset the shadow it casts to the proper place. We can replicate the shape
// of the inset shadow by using 2 rects. One is the same size as the view it is
// attached to, plus some padding. The other represents a cropping region of the
// first rect, with the exact postion and size of this region depending on the
// shadow's params. We make this cropping region by using the EO fill pattern so
// that the interection of the 2 rects is clear, and thus no shadow is cast.
static void renderInsetShadows(
    std::vector<BoxShadow> &insetShadows,
    RCTCornerRadii cornerRadii,
    UIEdgeInsets edgeInsets,
    CGSize layerSize,
    CGRect boundingRect,
    CGContextRef context)
{
  if (insetShadows.empty()) {
    return;
  }
  // Save state before doing any work so that we can restore it after we have
  // drawn all of our shadows. This ensures that we do not need to worry about
  // graphical state carrying over after this function returns
  CGContextSaveGState(context);

  CGRect layerFrameRelativeToBoundingRect =
      CGRectMake(-boundingRect.origin.x, -boundingRect.origin.y, layerSize.width, layerSize.height);
  CGRect shadowFrame =
      insetRect(layerFrameRelativeToBoundingRect, edgeInsets.left, edgeInsets.top, edgeInsets.right, edgeInsets.bottom);

  // First, create a clipping area so we only draw within the view's bounds.
  // If we do not do this, blur artifacts will show up outside the view.
  CGRect outerClippingRect = CGRectMake(0, 0, boundingRect.size.width, boundingRect.size.height);
  // Add the path twice so we only draw inside the view with the EO crop rule
  CGContextAddRect(context, outerClippingRect);
  CGContextAddRect(context, outerClippingRect);
  const RCTCornerInsets cornerInsetsForLayer = RCTGetCornerInsets(cornerRadii, edgeInsets);
  CGPathRef layerPath = RCTPathCreateWithRoundedRect(shadowFrame, cornerInsetsForLayer, nil);
  CGContextAddPath(context, layerPath);
  CGContextEOClip(context);
  CGPathRelease(layerPath);

  // Reverse iterator as shadows are stacked back to front
  for (auto it = insetShadows.rbegin(); it != insetShadows.rend(); ++it) {
    CGContextSaveGState(context);
    CGFloat offsetX = it->offsetX;
    CGFloat offsetY = it->offsetY;
    CGFloat blurRadius = it->blurRadius;
    CGFloat spreadDistance = it->spreadDistance;
    CGColorRef color = colorRefFromSharedColor(it->color);

    // Second, create the two offscreen rects we will use to create the correct
    // inset shadow shape. shadowRect has an originX such that it AND the clear
    // region are both guaranteed to be offscreen. We do not want some combination
    // of shadow params to allow the clear region to showup inside our context.
    // We also pad the size of the shadow rect by the blur radius so that the
    // edges of the shadow remain a solid color and do not blend with outside
    // of the view.
    CGRect shadowCastingRect = CGRectInset(shadowFrame, -blurRadius, -blurRadius);
    CGRect clearRegionRect = CGRectInset(shadowFrame, spreadDistance, spreadDistance);
    // This happens if the spread causes the height/width to be negative. A null
    // rect breaks a lot of the logic, so lets just keep it as a point
    if (CGRectIsNull(clearRegionRect)) {
      clearRegionRect = CGRectMake(0, 0, 0, 0);
    }
    CGPoint offsetToMoveOffscreen = CGPointMake(
        -fmax(
            shadowCastingRect.size.width,
            clearRegionRect.size.width + offsetX + (clearRegionRect.origin.x - shadowCastingRect.origin.x)) -
            shadowCastingRect.origin.x,
        0);
    shadowCastingRect = CGRectOffset(shadowCastingRect, offsetToMoveOffscreen.x, offsetToMoveOffscreen.y);
    clearRegionRect =
        CGRectOffset(clearRegionRect, offsetToMoveOffscreen.x + offsetX, offsetToMoveOffscreen.y + offsetY);
    CGContextAddRect(context, shadowCastingRect);

    const RCTCornerInsets cornerInsetsForClearRegion =
        RCTGetCornerInsets(cornerRadiiForBoxShadow(cornerRadii, -spreadDistance), edgeInsets);
    CGPathRef clearRegionPath = RCTPathCreateWithRoundedRect(clearRegionRect, cornerInsetsForClearRegion, nil);
    CGContextAddPath(context, clearRegionPath);

    // Third, set the shadow graphics state with the appropriate offset such that
    // it is positioned on top of the view. We subtract blurRadius because the
    // shadow rect is padded.
    CGContextSetShadowWithColor(
        context, CGSizeMake(-offsetToMoveOffscreen.x, -offsetToMoveOffscreen.y), blurRadius, color);

    // Fourth, the Core Graphics functions to actually draw the shadow rect
    // and thus the shadow itself. Note we use an EO fill path so that the
    // intersection between our two rects will be clear. The disjoint parts of
    // the rect will be colored, but because of the clipping area, we only see
    // the shadow projected from the shadow rect, not the clear region rect.
    CGContextSetFillColorWithColor(context, [RCTUIColor blackColor].CGColor); // [macOS]
    CGContextEOFillPath(context);

    CGContextRestoreGState(context);
  }

  CGContextRestoreGState(context);
}

UIImage *RCTGetBoxShadowImage(
    const std::vector<BoxShadow> &shadows,
    RCTCornerRadii cornerRadii,
    UIEdgeInsets edgeInsets,
    CGSize layerSize)
{
  CGRect boundingRect = RCTGetBoundingRect(shadows, layerSize);
  // [macOS Use RCTUIGraphicsImageRenderer shim
  RCTUIGraphicsImageRendererFormat *const rendererFormat = [RCTUIGraphicsImageRendererFormat defaultFormat];
  RCTUIGraphicsImageRenderer *const renderer = [[RCTUIGraphicsImageRenderer alloc] initWithSize:boundingRect.size
                                                                                   format:rendererFormat];
  // macOS]
  UIImage *const boxShadowImage =
      [renderer imageWithActions:^(RCTUIGraphicsImageRendererContext *_Nonnull rendererContext) { // [macOS]
        auto [outsetShadows, insetShadows] = splitBoxShadowsByInset(shadows);
        const CGContextRef context = rendererContext.CGContext;
        // Outset shadows should be before inset shadows since outset needs to
        // clear out a region in the view so we do not block its contents.
        // Inset shadows could draw over those outset shadows but if the shadow
        // colors have alpha < 1 then we will have inaccurate alpha compositing
        renderOutsetShadows(outsetShadows, cornerRadii, layerSize, boundingRect, context);
        renderInsetShadows(insetShadows, cornerRadii, edgeInsets, layerSize, boundingRect, context);
      }];

  return boxShadowImage;
}
