// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterMutatorView.h"

#include <QuartzCore/QuartzCore.h>

#include <vector>

#include "flutter/fml/logging.h"
#include "flutter/shell/platform/embedder/embedder.h"

@interface FlutterMutatorView () {
  // Each of these views clips to a CGPathRef. These views, if present,
  // are nested (first is child of FlutterMutatorView and last is parent of
  // _platformView).
  NSMutableArray* _pathClipViews;

  // View right above the platform view. Used to apply the final transform
  // (sans the translation) to the platform view.
  NSView* _platformViewContainer;

  NSView* _platformView;
}

@end

/// Superview container for platform views, to which sublayer transforms are applied.
@interface FlutterPlatformViewContainer : NSView
@end

@implementation FlutterPlatformViewContainer

- (BOOL)isFlipped {
  // Flutter transforms assume a coordinate system with an upper-left corner origin, with y
  // coordinate values increasing downwards. This affects the view, view transforms, and
  // sublayerTransforms.
  return YES;
}

@end

/// View that clips that content to a specific CGPathRef.
/// Clipping is done through a CAShapeLayer mask, which avoids the need to
/// rasterize the mask.
@interface FlutterPathClipView : NSView

@end

@implementation FlutterPathClipView

- (instancetype)initWithFrame:(NSRect)frameRect {
  if (self = [super initWithFrame:frameRect]) {
    self.wantsLayer = YES;
  }
  return self;
}

- (BOOL)isFlipped {
  // Flutter transforms assume a coordinate system with an upper-left corner origin, with y
  // coordinate values increasing downwards. This affects the view, view transforms, and
  // sublayerTransforms.
  return YES;
}

/// Clip the view to the given path. Offset top left corner of platform view
/// in global logical coordinates.
- (void)maskToPath:(CGPathRef)path withOrigin:(CGPoint)origin {
  CAShapeLayer* maskLayer = self.layer.mask;
  if (maskLayer == nil) {
    maskLayer = [CAShapeLayer layer];
    self.layer.mask = maskLayer;
  }
  maskLayer.path = path;
  maskLayer.transform = CATransform3DMakeTranslation(-origin.x, -origin.y, 0);
}

@end

namespace {
CATransform3D ToCATransform3D(const FlutterTransformation& t) {
  CATransform3D transform = CATransform3DIdentity;
  transform.m11 = t.scaleX;
  transform.m21 = t.skewX;
  transform.m41 = t.transX;
  transform.m14 = t.pers0;
  transform.m12 = t.skewY;
  transform.m22 = t.scaleY;
  transform.m42 = t.transY;
  transform.m24 = t.pers1;
  return transform;
}

bool AffineTransformIsOnlyScaleOrTranslate(const CGAffineTransform& transform) {
  return transform.b == 0 && transform.c == 0;
}

bool IsZeroSize(const FlutterSize size) {
  return size.width == 0 && size.height == 0;
}

CGRect FromFlutterRect(const FlutterRect& rect) {
  return CGRectMake(rect.left, rect.top, rect.right - rect.left, rect.bottom - rect.top);
}

FlutterRect ToFlutterRect(const CGRect& rect) {
  return FlutterRect{
      .left = rect.origin.x,
      .top = rect.origin.y,
      .right = rect.origin.x + rect.size.width,
      .bottom = rect.origin.y + rect.size.height,

  };
}

/// Returns whether the point is inside ellipse with given radius (centered at 0, 0).
bool PointInsideEllipse(const CGPoint& point, const FlutterSize& radius) {
  return (point.x * point.x) / (radius.width * radius.width) +
             (point.y * point.y) / (radius.height * radius.height) <
         1.0;
}

bool RoundRectCornerIntersects(const FlutterRoundedRect& roundRect, const FlutterRect& rect) {
  // Inner coordinate of the top left corner of the round rect.
  CGPoint inner_top_left =
      CGPointMake(roundRect.rect.left + roundRect.upper_left_corner_radius.width,
                  roundRect.rect.top + roundRect.upper_left_corner_radius.height);

  // Position of `rect` corner relative to inner_top_left.
  CGPoint relative_top_left =
      CGPointMake(rect.left - inner_top_left.x, rect.top - inner_top_left.y);

  // `relative_top_left` is in upper left quadrant.
  if (relative_top_left.x < 0 && relative_top_left.y < 0) {
    if (!PointInsideEllipse(relative_top_left, roundRect.upper_left_corner_radius)) {
      return true;
    }
  }

  // Inner coordinate of the top right corner of the round rect.
  CGPoint inner_top_right =
      CGPointMake(roundRect.rect.right - roundRect.upper_right_corner_radius.width,
                  roundRect.rect.top + roundRect.upper_right_corner_radius.height);

  // Positon of `rect` corner relative to inner_top_right.
  CGPoint relative_top_right =
      CGPointMake(rect.right - inner_top_right.x, rect.top - inner_top_right.y);

  // `relative_top_right` is in top right quadrant.
  if (relative_top_right.x > 0 && relative_top_right.y < 0) {
    if (!PointInsideEllipse(relative_top_right, roundRect.upper_right_corner_radius)) {
      return true;
    }
  }

  // Inner coordinate of the bottom left corner of the round rect.
  CGPoint inner_bottom_left =
      CGPointMake(roundRect.rect.left + roundRect.lower_left_corner_radius.width,
                  roundRect.rect.bottom - roundRect.lower_left_corner_radius.height);

  // Position of `rect` corner relative to inner_bottom_left.
  CGPoint relative_bottom_left =
      CGPointMake(rect.left - inner_bottom_left.x, rect.bottom - inner_bottom_left.y);

  // `relative_bottom_left` is in bottom left quadrant.
  if (relative_bottom_left.x < 0 && relative_bottom_left.y > 0) {
    if (!PointInsideEllipse(relative_bottom_left, roundRect.lower_left_corner_radius)) {
      return true;
    }
  }

  // Inner coordinate of the bottom right corner of the round rect.
  CGPoint inner_bottom_right =
      CGPointMake(roundRect.rect.right - roundRect.lower_right_corner_radius.width,
                  roundRect.rect.bottom - roundRect.lower_right_corner_radius.height);

  // Position of `rect` corner relative to inner_bottom_right.
  CGPoint relative_bottom_right =
      CGPointMake(rect.right - inner_bottom_right.x, rect.bottom - inner_bottom_right.y);

  // `relative_bottom_right` is in bottom right quadrant.
  if (relative_bottom_right.x > 0 && relative_bottom_right.y > 0) {
    if (!PointInsideEllipse(relative_bottom_right, roundRect.lower_right_corner_radius)) {
      return true;
    }
  }

  return false;
}

CGPathRef PathFromRoundedRect(const FlutterRoundedRect& roundedRect) {
  if (IsZeroSize(roundedRect.lower_left_corner_radius) &&
      IsZeroSize(roundedRect.lower_right_corner_radius) &&
      IsZeroSize(roundedRect.upper_left_corner_radius) &&
      IsZeroSize(roundedRect.upper_right_corner_radius)) {
    return CGPathCreateWithRect(FromFlutterRect(roundedRect.rect), nullptr);
  }

  CGMutablePathRef path = CGPathCreateMutable();

  const auto& rect = roundedRect.rect;
  const auto& topLeft = roundedRect.upper_left_corner_radius;
  const auto& topRight = roundedRect.upper_right_corner_radius;
  const auto& bottomLeft = roundedRect.lower_left_corner_radius;
  const auto& bottomRight = roundedRect.lower_right_corner_radius;

  CGPathMoveToPoint(path, nullptr, rect.left + topLeft.width, rect.top);
  CGPathAddLineToPoint(path, nullptr, rect.right - topRight.width, rect.top);
  CGPathAddCurveToPoint(path, nullptr, rect.right, rect.top, rect.right, rect.top + topRight.height,
                        rect.right, rect.top + topRight.height);
  CGPathAddLineToPoint(path, nullptr, rect.right, rect.bottom - bottomRight.height);
  CGPathAddCurveToPoint(path, nullptr, rect.right, rect.bottom, rect.right - bottomRight.width,
                        rect.bottom, rect.right - bottomRight.width, rect.bottom);
  CGPathAddLineToPoint(path, nullptr, rect.left + bottomLeft.width, rect.bottom);
  CGPathAddCurveToPoint(path, nullptr, rect.left, rect.bottom, rect.left,
                        rect.bottom - bottomLeft.height, rect.left,
                        rect.bottom - bottomLeft.height);
  CGPathAddLineToPoint(path, nullptr, rect.left, rect.top + topLeft.height);
  CGPathAddCurveToPoint(path, nullptr, rect.left, rect.top, rect.left + topLeft.width, rect.top,
                        rect.left + topLeft.width, rect.top);
  CGPathCloseSubpath(path);
  return path;
}

using MutationVector = std::vector<FlutterPlatformViewMutation>;

/// Returns a vector of FlutterPlatformViewMutation object pointers associated with a platform view.
/// The transforms sent from the engine include a transform from logical to physical coordinates.
/// Since Cocoa deals only in logical points, this function prepends a scale transform that scales
/// back from physical to logical coordinates to compensate.
MutationVector MutationsForPlatformView(const FlutterPlatformView* view, float scale) {
  MutationVector mutations;
  mutations.reserve(view->mutations_count + 1);
  mutations.push_back({
      .type = kFlutterPlatformViewMutationTypeTransformation,
      .transformation{
          .scaleX = 1.0 / scale,
          .scaleY = 1.0 / scale,
      },
  });
  for (size_t i = 0; i < view->mutations_count; ++i) {
    mutations.push_back(*view->mutations[i]);
  }
  return mutations;
}

/// Returns the composition of all transformation mutations in the mutations vector.
CATransform3D CATransformFromMutations(const MutationVector& mutations) {
  CATransform3D transform = CATransform3DIdentity;
  for (auto mutation : mutations) {
    switch (mutation.type) {
      case kFlutterPlatformViewMutationTypeTransformation: {
        CATransform3D mutationTransform = ToCATransform3D(mutation.transformation);
        transform = CATransform3DConcat(mutationTransform, transform);
        break;
      }
      case kFlutterPlatformViewMutationTypeClipRect:
      case kFlutterPlatformViewMutationTypeClipRoundedRect:
      case kFlutterPlatformViewMutationTypeOpacity:
        break;
    }
  }
  return transform;
}

/// Returns the opacity for all opacity mutations in the mutations vector.
float OpacityFromMutations(const MutationVector& mutations) {
  float opacity = 1.0;
  for (auto mutation : mutations) {
    switch (mutation.type) {
      case kFlutterPlatformViewMutationTypeOpacity:
        opacity *= mutation.opacity;
        break;
      case kFlutterPlatformViewMutationTypeClipRect:
      case kFlutterPlatformViewMutationTypeClipRoundedRect:
      case kFlutterPlatformViewMutationTypeTransformation:
        break;
    }
  }
  return opacity;
}

/// Returns the clip rect generated by the intersection of clips in the mutations vector.
CGRect MasterClipFromMutations(CGRect bounds, const MutationVector& mutations) {
  // Master clip in global logical coordinates. This is intersection of all clip rectangles
  // present in mutators.
  CGRect master_clip = bounds;

  // Create the initial transform.
  CATransform3D transform = CATransform3DIdentity;
  for (auto mutation : mutations) {
    switch (mutation.type) {
      case kFlutterPlatformViewMutationTypeClipRect: {
        CGRect rect = CGRectApplyAffineTransform(FromFlutterRect(mutation.clip_rect),
                                                 CATransform3DGetAffineTransform(transform));
        master_clip = CGRectIntersection(rect, master_clip);
        break;
      }
      case kFlutterPlatformViewMutationTypeClipRoundedRect: {
        CGAffineTransform affineTransform = CATransform3DGetAffineTransform(transform);
        CGRect rect = CGRectApplyAffineTransform(FromFlutterRect(mutation.clip_rounded_rect.rect),
                                                 affineTransform);
        master_clip = CGRectIntersection(rect, master_clip);
        break;
      }
      case kFlutterPlatformViewMutationTypeTransformation:
        transform = CATransform3DConcat(ToCATransform3D(mutation.transformation), transform);
        break;
      case kFlutterPlatformViewMutationTypeOpacity:
        break;
    }
  }
  return master_clip;
}

/// A rounded rectangle and transform associated with it.
typedef struct {
  FlutterRoundedRect rrect;
  CGAffineTransform transform;
} ClipRoundedRect;

/// Returns the set of all rounded rect paths generated by clips in the mutations vector.
NSMutableArray* ClipPathFromMutations(CGRect master_clip, const MutationVector& mutations) {
  std::vector<ClipRoundedRect> rounded_rects;

  CATransform3D transform = CATransform3DIdentity;
  for (auto mutation : mutations) {
    switch (mutation.type) {
      case kFlutterPlatformViewMutationTypeClipRoundedRect: {
        CGAffineTransform affineTransform = CATransform3DGetAffineTransform(transform);
        rounded_rects.push_back({mutation.clip_rounded_rect, affineTransform});
        break;
      }
      case kFlutterPlatformViewMutationTypeTransformation:
        transform = CATransform3DConcat(ToCATransform3D(mutation.transformation), transform);
        break;
      case kFlutterPlatformViewMutationTypeClipRect: {
        CGAffineTransform affineTransform = CATransform3DGetAffineTransform(transform);
        // Shearing or rotation requires path clipping.
        if (!AffineTransformIsOnlyScaleOrTranslate(affineTransform)) {
          rounded_rects.push_back(
              {FlutterRoundedRect{mutation.clip_rect, FlutterSize{0, 0}, FlutterSize{0, 0},
                                  FlutterSize{0, 0}, FlutterSize{0, 0}},
               affineTransform});
        }
        break;
      }
      case kFlutterPlatformViewMutationTypeOpacity:
        break;
    }
  }

  NSMutableArray* paths = [NSMutableArray array];
  for (const auto& r : rounded_rects) {
    bool requiresPath = !AffineTransformIsOnlyScaleOrTranslate(r.transform);
    if (!requiresPath) {
      CGAffineTransform inverse = CGAffineTransformInvert(r.transform);
      // Transform master clip to clip rect coordinates and check if this view intersects one of the
      // corners, which means we need to use path clipping.
      CGRect localMasterClip = CGRectApplyAffineTransform(master_clip, inverse);
      requiresPath = RoundRectCornerIntersects(r.rrect, ToFlutterRect(localMasterClip));
    }

    // Only clip to rounded rectangle path if the view intersects some of the round corners. If
    // not, clipping to masterClip is enough.
    if (requiresPath) {
      CGPathRef path = PathFromRoundedRect(r.rrect);
      CGPathRef transformedPath = CGPathCreateCopyByTransformingPath(path, &r.transform);
      [paths addObject:(__bridge id)transformedPath];
      CGPathRelease(transformedPath);
      CGPathRelease(path);
    }
  }
  return paths;
}
}  // namespace

@implementation FlutterMutatorView

- (NSView*)platformView {
  return _platformView;
}

- (NSMutableArray*)pathClipViews {
  return _pathClipViews;
}

- (NSView*)platformViewContainer {
  return _platformViewContainer;
}

- (instancetype)initWithPlatformView:(NSView*)platformView {
  if (self = [super initWithFrame:NSZeroRect]) {
    _platformView = platformView;
    _pathClipViews = [NSMutableArray array];
    self.wantsLayer = YES;
  }
  return self;
}

- (NSView*)hitTest:(NSPoint)point {
  return nil;
}

- (BOOL)isFlipped {
  return YES;
}

/// Returns the scale factor to translate logical pixels to physical pixels for this view.
- (CGFloat)contentsScale {
  return self.superview != nil ? self.superview.layer.contentsScale : 1.0;
}

/// Updates the nested stack of clip views that host the platform view.
- (void)updatePathClipViewsWithPaths:(NSArray*)paths {
  // Remove path clip views depending on the number of paths.
  while (_pathClipViews.count > paths.count) {
    NSView* view = _pathClipViews.lastObject;
    [view removeFromSuperview];
    [_pathClipViews removeLastObject];
  }
  // Otherwise, add path clip views to the end.
  for (size_t i = _pathClipViews.count; i < paths.count; ++i) {
    NSView* superView = _pathClipViews.count == 0 ? self : _pathClipViews.lastObject;
    FlutterPathClipView* pathClipView = [[FlutterPathClipView alloc] initWithFrame:self.bounds];
    [_pathClipViews addObject:pathClipView];
    [superView addSubview:pathClipView];
  }
  // Update bounds and apply clip paths.
  for (size_t i = 0; i < _pathClipViews.count; ++i) {
    FlutterPathClipView* pathClipView = _pathClipViews[i];
    pathClipView.frame = self.bounds;
    [pathClipView maskToPath:(__bridge CGPathRef)[paths objectAtIndex:i]
                  withOrigin:self.frame.origin];
  }
}

/// Updates the PlatformView and PlatformView container views.
///
/// Re-nests _platformViewContainer in the innermost clip view, applies transforms to the underlying
/// CALayer, adds the platform view as a subview of the container, and sets the axis-aligned clip
/// rect around the tranformed view.
- (void)updatePlatformViewWithBounds:(CGRect)untransformedBounds
                   transformedBounds:(CGRect)transformedBounds
                           transform:(CATransform3D)transform
                            clipRect:(CGRect)clipRect {
  // Create the PlatformViewContainer view if necessary.
  if (_platformViewContainer == nil) {
    _platformViewContainer = [[FlutterPlatformViewContainer alloc] initWithFrame:self.bounds];
    _platformViewContainer.wantsLayer = YES;
  }

  // Nest the PlatformViewContainer view in the innermost path clip view.
  NSView* containerSuperview = _pathClipViews.count == 0 ? self : _pathClipViews.lastObject;
  [containerSuperview addSubview:_platformViewContainer];
  _platformViewContainer.frame = self.bounds;

  // Nest the platform view in the PlatformViewContainer.
  [_platformViewContainer addSubview:_platformView];
  _platformView.frame = untransformedBounds;

  // Transform for the platform view is finalTransform adjusted for bounding rect origin.
  CATransform3D translation =
      CATransform3DMakeTranslation(-transformedBounds.origin.x, -transformedBounds.origin.y, 0);
  transform = CATransform3DConcat(transform, translation);
  _platformViewContainer.layer.sublayerTransform = transform;

  // By default NSView clips children to frame. If masterClip is tighter than mutator view frame,
  // the frame is set to masterClip and child offset adjusted to compensate for the difference.
  if (!CGRectEqualToRect(clipRect, transformedBounds)) {
    FML_DCHECK(self.subviews.count == 1);
    auto subview = self.subviews.firstObject;
    FML_DCHECK(subview.frame.origin.x == 0 && subview.frame.origin.y == 0);
    subview.frame = CGRectMake(transformedBounds.origin.x - clipRect.origin.x,
                               transformedBounds.origin.y - clipRect.origin.y,
                               subview.frame.size.width, subview.frame.size.height);
    self.frame = clipRect;
  }
}

/// Whenever possible view will be clipped using layer bounds.
/// If clipping to path is needed, CAShapeLayer(s) will be used as mask.
/// Clipping to round rect only clips to path if round corners are intersected.
- (void)applyFlutterLayer:(const FlutterLayer*)layer {
  // Compute the untransformed bounding rect for the platform view in logical pixels.
  // FlutterLayer.size is in physical pixels but Cocoa uses logical points.
  CGFloat scale = [self contentsScale];
  MutationVector mutations = MutationsForPlatformView(layer->platform_view, scale);

  CATransform3D finalTransform = CATransformFromMutations(mutations);

  // Compute the untransformed bounding rect for the platform view in logical pixels.
  // FlutterLayer.size is in physical pixels but Cocoa uses logical points.
  CGRect untransformedBoundingRect =
      CGRectMake(0, 0, layer->size.width / scale, layer->size.height / scale);
  CGRect finalBoundingRect = CGRectApplyAffineTransform(
      untransformedBoundingRect, CATransform3DGetAffineTransform(finalTransform));
  self.frame = finalBoundingRect;

  // Compute the layer opacity.
  self.layer.opacity = OpacityFromMutations(mutations);

  // Compute the master clip in global logical coordinates.
  CGRect masterClip = MasterClipFromMutations(finalBoundingRect, mutations);
  if (CGRectIsNull(masterClip)) {
    self.hidden = YES;
    return;
  }
  self.hidden = NO;

  /// Paths in global logical coordinates that need to be clipped to.
  NSMutableArray* paths = ClipPathFromMutations(masterClip, mutations);
  [self updatePathClipViewsWithPaths:paths];

  /// Update PlatformViewContainer, PlatformView, and apply transforms and axis-aligned clip rect.
  [self updatePlatformViewWithBounds:untransformedBoundingRect
                   transformedBounds:finalBoundingRect
                           transform:finalTransform
                            clipRect:masterClip];
}

@end
