// 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 'dart:math' as math;
import 'dart:typed_data';

import 'package:ui/ui.dart' as ui;

import '../engine_canvas.dart';
import '../picture.dart';
import '../rrect_renderer.dart';
import '../shadow.dart';
import '../text/canvas_paragraph.dart';
import '../util.dart';
import '../vector_math.dart';
import 'bitmap_canvas.dart';
import 'painting.dart';
import 'path/path.dart';
import 'path/path_utils.dart';
import 'render_vertices.dart';
import 'shaders/image_shader.dart';

/// Enable this to print every command applied by a canvas.
const bool _debugDumpPaintCommands = false;

// Returns the squared length of the x, y (of a border radius)
// It normalizes x, y values before working with them, by
// assuming anything < 0 to be 0, because flutter may pass
// negative radii (which Skia assumes to be 0), see:
// https://skia.org/user/api/SkRRect_Reference#SkRRect_inset
double _measureBorderRadius(double x, double y) {
  final double clampedX = x < 0 ? 0 : x;
  final double clampedY = y < 0 ? 0 : y;
  return clampedX * clampedX + clampedY * clampedY;
}

class RawRecordingCanvas extends BitmapCanvas implements ui.PictureRecorder {
  RawRecordingCanvas(ui.Size size)
      : super(ui.Offset.zero & size, RenderStrategy());

  @override
  void dispose() {
    clear();
  }

  RecordingCanvas beginRecording(ui.Rect bounds) => throw UnsupportedError('');

  @override
  ui.Picture endRecording() => throw UnsupportedError('');

  RecordingCanvas? _canvas; // ignore: unused_field

  bool _isRecording = true; // ignore: unused_field

  @override
  bool get isRecording => true;

  ui.Rect? cullRect;
}

/// Records canvas commands to be applied to a [EngineCanvas].
///
/// See [Canvas] for docs for these methods.
class RecordingCanvas {
  /// Computes [_pictureBounds].
  final _PaintBounds _paintBounds;

  /// Maximum paintable bounds for the picture painted by this recording.
  ///
  /// The bounds contain the full picture. The commands recorded for the picture
  /// are later pruned based on the clip applied to the picture. See the [apply]
  /// method for more details.
  ui.Rect? get pictureBounds {
    assert(
      _recordingEnded,
      'Picture bounds not available yet. Call [endRecording] before accessing picture bounds.',
    );
    return _pictureBounds;
  }

  ui.Rect? _pictureBounds;

  final List<PaintCommand> _commands = <PaintCommand>[];

  /// In debug mode returns the list of recorded paint commands for testing.
  List<PaintCommand> get debugPaintCommands {
    if (assertionsEnabled) {
      return _commands;
    }
    throw UnsupportedError('For debugging only.');
  }

  RecordingCanvas(ui.Rect? bounds)
      : _paintBounds = _PaintBounds(bounds ?? ui.Rect.largest);

  final RenderStrategy renderStrategy = RenderStrategy();

  /// Forces arbitrary paint even for simple pictures.
  ///
  /// This is useful for testing bitmap canvas when otherwise the compositor
  /// would prefer a DOM canvas.
  void debugEnforceArbitraryPaint() {
    renderStrategy.hasArbitraryPaint = true;
  }

  /// Whether this canvas contain drawing operations.
  ///
  /// Some pictures are created but only contain operations that do not result
  /// in any pixels on the screen. For example, they will only contain saves,
  /// restores, and translates. This happens when a parent [RenderObject]
  /// prepares the canvas for its children to paint to, but the child ends up
  /// not painting anything, such as when an empty [SizedBox] is used to add a
  /// margin between two widgets.
  bool get didDraw => _didDraw;
  bool _didDraw = false;

  /// Used to ensure that [endRecording] is called before calling [apply] or
  /// [pictureBounds].
  ///
  /// When [PaintingContext] is used by [ClipContext], the painter may
  /// end a recording and start a new one and cause [ClipContext] to call
  /// restore on a new canvas before prior save calls, [_recordingEnded]
  /// prevents transforms removals in that case.
  bool _recordingEnded = false;

  /// Stops recording drawing commands and computes paint bounds.
  ///
  /// This must be called prior to passing the picture to the [SceneBuilder]
  /// for rendering. In a production app, this is done automatically by
  /// [PictureRecorder] when the framework calls [PictureRecorder.endRecording].
  /// However, if you are writing a unit-test and using [RecordingCanvas]
  /// directly it is up to you to call this method explicitly.
  void endRecording() {
    _pictureBounds = _paintBounds.computeBounds();
    _recordingEnded = true;
  }

  /// Applies the recorded commands onto an [engineCanvas] and signals to
  /// canvas that all painting is completed for garbage collection/reuse.
  ///
  /// The [clipRect] specifies the clip applied to the picture (screen clip at
  /// a minimum). The commands that fall outside the clip are skipped and are
  /// not applied to the [engineCanvas]. A command must have a non-zero
  /// intersection with the clip in order to be applied.
  void apply(EngineCanvas engineCanvas, ui.Rect clipRect) {
    applyCommands(engineCanvas, clipRect);
    engineCanvas.endOfPaint();
  }

  /// Applies the recorded commands onto an [engineCanvas].
  ///
  /// The [clipRect] specifies the clip applied to the picture (screen clip at
  /// a minimum). The commands that fall outside the clip are skipped and are
  /// not applied to the [engineCanvas]. A command must have a non-zero
  /// intersection with the clip in order to be applied.
  void applyCommands(EngineCanvas engineCanvas, ui.Rect clipRect) {
    assert(_recordingEnded);
    if (_debugDumpPaintCommands) {
      final StringBuffer debugBuf = StringBuffer();
      int skips = 0;
      debugBuf.writeln(
          '--- Applying RecordingCanvas to ${engineCanvas.runtimeType} '
          'with bounds $_paintBounds and clip $clipRect (w = ${clipRect.width},'
          ' h = ${clipRect.height})');
      for (int i = 0; i < _commands.length; i++) {
        final PaintCommand command = _commands[i];
        if (command is DrawCommand) {
          if (command.isInvisible(clipRect)) {
            // The drawing command is outside the clip region. No need to apply.
            debugBuf.writeln('SKIPPED: ctx.$command;');
            skips += 1;
            continue;
          }
        }
        debugBuf.writeln('ctx.$command;');
        command.apply(engineCanvas);
      }
      if (skips > 0) {
        debugBuf.writeln('Total commands skipped: $skips');
      }
      debugBuf.writeln('--- End of command stream');
      print(debugBuf);
    } else {
      try {
        if (rectContainsOther(clipRect, _pictureBounds!)) {
          // No need to check if commands fit in the clip rect if we already
          // know that the entire picture fits it.
          final int len = _commands.length;
          for (int i = 0; i < len; i++) {
            _commands[i].apply(engineCanvas);
          }
        } else {
          // The picture doesn't fit the clip rect. Check that drawing commands
          // fit before applying them.
          final int len = _commands.length;
          for (int i = 0; i < len; i++) {
            final PaintCommand command = _commands[i];
            if (command is DrawCommand) {
              if (command.isInvisible(clipRect)) {
                // The drawing command is outside the clip region. No need to apply.
                continue;
              }
            }
            command.apply(engineCanvas);
          }
        }
      } catch (e) {
        // commands should never fail, but...
        // https://bugzilla.mozilla.org/show_bug.cgi?id=941146
        if (!isNsErrorFailureException(e)) {
          rethrow;
        }
      }
    }
  }

  /// Prints recorded commands.
  String? debugPrintCommands() {
    if (assertionsEnabled) {
      final StringBuffer debugBuf = StringBuffer();
      for (int i = 0; i < _commands.length; i++) {
        final PaintCommand command = _commands[i];
        debugBuf.writeln('ctx.$command;');
      }
      return debugBuf.toString();
    }
    return null;
  }

  void save() {
    assert(!_recordingEnded);
    _paintBounds.saveTransformsAndClip();
    _commands.add(const PaintSave());
    _saveCount++;
  }

  void saveLayerWithoutBounds(SurfacePaint paint) {
    assert(!_recordingEnded);
    renderStrategy.hasArbitraryPaint = true;
    // TODO(het): Implement this correctly using another canvas.
    _commands.add(const PaintSave());
    _paintBounds.saveTransformsAndClip();
    _saveCount++;
  }

  void saveLayer(ui.Rect bounds, SurfacePaint paint) {
    assert(!_recordingEnded);
    renderStrategy.hasArbitraryPaint = true;
    // TODO(het): Implement this correctly using another canvas.
    _commands.add(const PaintSave());
    _paintBounds.saveTransformsAndClip();
    _saveCount++;
  }

  void restore() {
    if (!_recordingEnded && _saveCount > 1) {
      _paintBounds.restoreTransformsAndClip();
    }
    if (_commands.isNotEmpty && _commands.last is PaintSave) {
      // A restore followed a save without any drawing operations in between.
      // This means that the save didn't have any effect on drawing operations
      // and can be omitted. This makes our communication with the canvas less
      // chatty.
      _commands.removeLast();
    } else {
      _commands.add(const PaintRestore());
    }
    _saveCount--;
  }

  void translate(double dx, double dy) {
    assert(!_recordingEnded);
    _paintBounds.translate(dx, dy);
    _commands.add(PaintTranslate(dx, dy));
  }

  void scale(double sx, double sy) {
    assert(!_recordingEnded);
    _paintBounds.scale(sx, sy);
    _commands.add(PaintScale(sx, sy));
  }

  void rotate(double radians) {
    assert(!_recordingEnded);
    _paintBounds.rotateZ(radians);
    _commands.add(PaintRotate(radians));
  }

  void transform(Float32List matrix4) {
    assert(!_recordingEnded);
    _paintBounds.transform(matrix4);
    _commands.add(PaintTransform(matrix4));
  }

  Float32List getCurrentMatrixUnsafe() => _paintBounds._currentMatrix.storage;

  void skew(double sx, double sy) {
    assert(!_recordingEnded);
    renderStrategy.hasArbitraryPaint = true;
    _paintBounds.skew(sx, sy);
    _commands.add(PaintSkew(sx, sy));
  }

  void clipRect(ui.Rect rect, ui.ClipOp clipOp) {
    assert(!_recordingEnded);
    final DrawCommand command = PaintClipRect(rect, clipOp);
    switch (clipOp) {
      case ui.ClipOp.intersect:
        _paintBounds.clipRect(rect, command);
        break;
      case ui.ClipOp.difference:
        // Since this refers to inverse, can't shrink paintBounds.
        break;
    }
    renderStrategy.hasArbitraryPaint = true;
    _commands.add(command);
  }

  void clipRRect(ui.RRect roundedRect) {
    assert(!_recordingEnded);
    final PaintClipRRect command = PaintClipRRect(roundedRect);
    _paintBounds.clipRect(roundedRect.outerRect, command);
    renderStrategy.hasArbitraryPaint = true;
    _commands.add(command);
  }

  void clipPath(ui.Path path, {bool doAntiAlias = true}) {
    assert(!_recordingEnded);
    final PaintClipPath command = PaintClipPath(path as SurfacePath);
    _paintBounds.clipRect(path.getBounds(), command);
    renderStrategy.hasArbitraryPaint = true;
    _commands.add(command);
  }

  ui.Rect? getDestinationClipBounds() => _paintBounds.getDestinationClipBounds();

  void drawColor(ui.Color color, ui.BlendMode blendMode) {
    assert(!_recordingEnded);
    final PaintDrawColor command = PaintDrawColor(color, blendMode);
    _commands.add(command);
    _paintBounds.grow(_paintBounds.maxPaintBounds, command);
  }

  void drawLine(ui.Offset p1, ui.Offset p2, SurfacePaint paint) {
    assert(!_recordingEnded);
    assert(paint.shader == null || paint.shader is! EngineImageShader,
        'ImageShader not supported yet');
    final double paintSpread = math.max(_getPaintSpread(paint), 1.0);
    final PaintDrawLine command = PaintDrawLine(p1, p2, paint.paintData);
    // TODO(yjbanov): This can be optimized. Currently we create a box around
    //                the line and then apply the transform on the box to get
    //                the bounding box. If you have a 45-degree line and a
    //                45-degree transform, the bounding box should be the length
    //                of the line long and stroke width wide, but our current
    //                algorithm produces a square with each side of the length
    //                matching the length of the line.
    _paintBounds.growLTRB(
      math.min(p1.dx, p2.dx) - paintSpread,
      math.min(p1.dy, p2.dy) - paintSpread,
      math.max(p1.dx, p2.dx) + paintSpread,
      math.max(p1.dy, p2.dy) + paintSpread,
      command,
    );
    renderStrategy.hasArbitraryPaint = true;
    _didDraw = true;
    _commands.add(command);
  }

  void drawPaint(SurfacePaint paint) {
    assert(!_recordingEnded);
    assert(paint.shader == null || paint.shader is! EngineImageShader,
        'ImageShader not supported yet');
    renderStrategy.hasArbitraryPaint = true;
    _didDraw = true;
    final PaintDrawPaint command = PaintDrawPaint(paint.paintData);
    _paintBounds.grow(_paintBounds.maxPaintBounds, command);
    _commands.add(command);
  }

  void drawRect(ui.Rect rect, SurfacePaint paint) {
    assert(!_recordingEnded);
    if (paint.shader != null) {
      renderStrategy.hasArbitraryPaint = true;
    }
    _didDraw = true;
    final double paintSpread = _getPaintSpread(paint);
    final PaintDrawRect command = PaintDrawRect(rect, paint.paintData);
    if (paintSpread != 0.0) {
      _paintBounds.grow(rect.inflate(paintSpread), command);
    } else {
      _paintBounds.grow(rect, command);
    }
    _commands.add(command);
  }

  void drawRRect(ui.RRect rrect, SurfacePaint paint) {
    assert(!_recordingEnded);
    if (paint.shader != null || !rrect.webOnlyUniformRadii) {
      renderStrategy.hasArbitraryPaint = true;
    }
    _didDraw = true;
    final double paintSpread = _getPaintSpread(paint);
    final double left = math.min(rrect.left, rrect.right) - paintSpread;
    final double top = math.min(rrect.top, rrect.bottom) - paintSpread;
    final double right = math.max(rrect.left, rrect.right) + paintSpread;
    final double bottom = math.max(rrect.top, rrect.bottom) + paintSpread;
    final PaintDrawRRect command = PaintDrawRRect(rrect, paint.paintData);
    _paintBounds.growLTRB(left, top, right, bottom, command);
    _commands.add(command);
  }

  void drawDRRect(ui.RRect outer, ui.RRect inner, SurfacePaint paint) {
    assert(!_recordingEnded);
    // Check the inner bounds are contained within the outer bounds
    // see: https://cs.chromium.org/chromium/src/third_party/skia/src/core/SkCanvas.cpp?l=1787-1789
    final ui.Rect innerRect = inner.outerRect;
    final ui.Rect outerRect = outer.outerRect;
    if (outerRect == innerRect || outerRect.intersect(innerRect) != innerRect) {
      return; // inner is not fully contained within outer
    }

    // Compare radius "length" of the rectangles that are going to be actually drawn
    final ui.RRect scaledOuter = outer.scaleRadii();
    final ui.RRect scaledInner = inner.scaleRadii();

    final double outerTl =
        _measureBorderRadius(scaledOuter.tlRadiusX, scaledOuter.tlRadiusY);
    final double outerTr =
        _measureBorderRadius(scaledOuter.trRadiusX, scaledOuter.trRadiusY);
    final double outerBl =
        _measureBorderRadius(scaledOuter.blRadiusX, scaledOuter.blRadiusY);
    final double outerBr =
        _measureBorderRadius(scaledOuter.brRadiusX, scaledOuter.brRadiusY);

    final double innerTl =
        _measureBorderRadius(scaledInner.tlRadiusX, scaledInner.tlRadiusY);
    final double innerTr =
        _measureBorderRadius(scaledInner.trRadiusX, scaledInner.trRadiusY);
    final double innerBl =
        _measureBorderRadius(scaledInner.blRadiusX, scaledInner.blRadiusY);
    final double innerBr =
        _measureBorderRadius(scaledInner.brRadiusX, scaledInner.brRadiusY);

    if (innerTl > outerTl ||
        innerTr > outerTr ||
        innerBl > outerBl ||
        innerBr > outerBr) {
      return; // Some inner radius is overlapping some outer radius
    }

    renderStrategy.hasArbitraryPaint = true;
    _didDraw = true;
    final double paintSpread = _getPaintSpread(paint);
    final PaintDrawDRRect command =
        PaintDrawDRRect(outer, inner, paint.paintData);
    final double left = math.min(outer.left, outer.right);
    final double right = math.max(outer.left, outer.right);
    final double top = math.min(outer.top, outer.bottom);
    final double bottom = math.max(outer.top, outer.bottom);
    _paintBounds.growLTRB(
      left - paintSpread,
      top - paintSpread,
      right + paintSpread,
      bottom + paintSpread,
      command,
    );
    _commands.add(command);
  }

  void drawOval(ui.Rect rect, SurfacePaint paint) {
    assert(!_recordingEnded);
    renderStrategy.hasArbitraryPaint = true;
    _didDraw = true;
    final double paintSpread = _getPaintSpread(paint);
    final PaintDrawOval command = PaintDrawOval(rect, paint.paintData);
    if (paintSpread != 0.0) {
      _paintBounds.grow(rect.inflate(paintSpread), command);
    } else {
      _paintBounds.grow(rect, command);
    }
    _commands.add(command);
  }

  void drawCircle(ui.Offset c, double radius, SurfacePaint paint) {
    assert(!_recordingEnded);
    renderStrategy.hasArbitraryPaint = true;
    _didDraw = true;
    final double paintSpread = _getPaintSpread(paint);
    final PaintDrawCircle command = PaintDrawCircle(c, radius, paint.paintData);
    final double distance = radius + paintSpread;
    _paintBounds.growLTRB(
      c.dx - distance,
      c.dy - distance,
      c.dx + distance,
      c.dy + distance,
      command,
    );
    _commands.add(command);
  }

  void drawPath(ui.Path path, SurfacePaint paint) {
    assert(!_recordingEnded);
    if (paint.shader == null) {
      // For Rect/RoundedRect paths use drawRect/drawRRect code paths for
      // DomCanvas optimization.
      final SurfacePath sPath = path as SurfacePath;
      final ui.Rect? rect = sPath.toRect();
      if (rect != null) {
        drawRect(rect, paint);
        return;
      }
      final ui.RRect? rrect = sPath.toRoundedRect();
      if (rrect != null) {
        drawRRect(rrect, paint);
        return;
      }
    }
    final SurfacePath sPath = path as SurfacePath;
    if (!sPath.pathRef.isEmpty) {
      renderStrategy.hasArbitraryPaint = true;
      _didDraw = true;
      ui.Rect pathBounds = sPath.getBounds();
      final double paintSpread = _getPaintSpread(paint);
      if (paintSpread != 0.0) {
        pathBounds = pathBounds.inflate(paintSpread);
      }
      // Clone path so it can be reused for subsequent draw calls.
      final ui.Path clone = SurfacePath.shallowCopy(path);
      final PaintDrawPath command =
          PaintDrawPath(clone as SurfacePath, paint.paintData);
      _paintBounds.grow(pathBounds, command);
      clone.fillType = sPath.fillType;
      _commands.add(command);
    }
  }

  void drawImage(ui.Image image, ui.Offset offset, SurfacePaint paint) {
    assert(!_recordingEnded);
    assert(paint.shader == null || paint.shader is! EngineImageShader,
        'ImageShader not supported yet');
    renderStrategy.hasArbitraryPaint = true;
    renderStrategy.hasImageElements = true;
    _didDraw = true;
    final double left = offset.dx;
    final double top = offset.dy;
    final PaintDrawImage command = PaintDrawImage(image, offset, paint.paintData);
    _paintBounds.growLTRB(
        left, top, left + image.width, top + image.height, command);
    _commands.add(command);
  }

  void drawPicture(ui.Picture picture) {
    assert(!_recordingEnded);
    final EnginePicture enginePicture = picture as EnginePicture;
    if (enginePicture.recordingCanvas == null) {
      // No contents / nothing to draw.
      return;
    }
    final RecordingCanvas pictureRecording = enginePicture.recordingCanvas!;
    if (pictureRecording._didDraw == true) {
      _didDraw = true;
    }
    renderStrategy.merge(pictureRecording.renderStrategy);
    // Need to save to make sure we don't pick up leftover clips and
    // transforms from running commands in picture.
    save();
    _commands.addAll(pictureRecording._commands);
    restore();
    if (pictureRecording._pictureBounds != null) {
      _paintBounds.growBounds(pictureRecording._pictureBounds!);
    }
  }

  void drawImageRect(
      ui.Image image, ui.Rect src, ui.Rect dst, SurfacePaint paint) {
    assert(!_recordingEnded);
    assert(paint.shader == null || paint.shader is! EngineImageShader,
        'ImageShader not supported yet');
    renderStrategy.hasArbitraryPaint = true;
    renderStrategy.hasImageElements = true;
    _didDraw = true;
    final PaintDrawImageRect command =
        PaintDrawImageRect(image, src, dst, paint.paintData);
    _paintBounds.grow(dst, command);
    _commands.add(command);
  }

  void drawParagraph(ui.Paragraph paragraph, ui.Offset offset) {
    assert(!_recordingEnded);
    final CanvasParagraph engineParagraph = paragraph as CanvasParagraph;
    if (!engineParagraph.isLaidOut) {
      // Ignore non-laid out paragraphs. This matches Flutter's behavior.
      return;
    }

    _didDraw = true;
    if (engineParagraph.hasArbitraryPaint) {
      renderStrategy.hasArbitraryPaint = true;
    }
    renderStrategy.hasParagraphs = true;
    final PaintDrawParagraph command =
        PaintDrawParagraph(engineParagraph, offset);

    final ui.Rect paragraphBounds = engineParagraph.paintBounds;
    _paintBounds.growLTRB(
      offset.dx + paragraphBounds.left,
      offset.dy + paragraphBounds.top,
      offset.dx + paragraphBounds.right,
      offset.dy + paragraphBounds.bottom,
      command,
    );

    _commands.add(command);
  }

  void drawShadow(ui.Path path, ui.Color color, double elevation,
      bool transparentOccluder) {
    assert(!_recordingEnded);
    renderStrategy.hasArbitraryPaint = true;
    _didDraw = true;
    final ui.Rect shadowRect =
        computePenumbraBounds(path.getBounds(), elevation);
    final PaintDrawShadow command = PaintDrawShadow(
        path as SurfacePath, color, elevation, transparentOccluder);
    _paintBounds.grow(shadowRect, command);
    _commands.add(command);
  }

  void drawVertices(
      SurfaceVertices vertices, ui.BlendMode blendMode, SurfacePaint paint) {
    assert(!_recordingEnded);
    renderStrategy.hasArbitraryPaint = true;
    _didDraw = true;
    final PaintDrawVertices command =
        PaintDrawVertices(vertices, blendMode, paint.paintData);
    _growPaintBoundsByPoints(vertices.positions, 0, paint, command);
    _commands.add(command);
  }

  void drawRawPoints(
      ui.PointMode pointMode, Float32List points, SurfacePaint paint) {
    assert(!_recordingEnded);
    renderStrategy.hasArbitraryPaint = true;
    _didDraw = true;
    final PaintDrawPoints command =
        PaintDrawPoints(pointMode, points, paint.paintData);
    _growPaintBoundsByPoints(points, paint.strokeWidth, paint, command);
    _commands.add(command);
  }

  void _growPaintBoundsByPoints(Float32List points, double thickness,
      SurfacePaint paint, DrawCommand command) {
    double minValueX, maxValueX, minValueY, maxValueY;
    minValueX = maxValueX = points[0];
    minValueY = maxValueY = points[1];
    final int len = points.length;
    for (int i = 2; i < len; i += 2) {
      final double x = points[i];
      final double y = points[i + 1];
      if (x.isNaN || y.isNaN) {
        // Follows skia implementation that sets bounds to empty
        // and aborts.
        return;
      }
      minValueX = math.min(minValueX, x);
      maxValueX = math.max(maxValueX, x);
      minValueY = math.min(minValueY, y);
      maxValueY = math.max(maxValueY, y);
    }
    final double distance = thickness / 2.0;
    final double paintSpread = _getPaintSpread(paint);
    _paintBounds.growLTRB(
      minValueX - distance - paintSpread,
      minValueY - distance - paintSpread,
      maxValueX + distance + paintSpread,
      maxValueY + distance + paintSpread,
      command,
    );
  }

  int _saveCount = 1;
  int get saveCount => _saveCount;

  /// Prints the commands recorded by this canvas to the console.
  void debugDumpCommands() {
    print('/' * 40 + ' CANVAS COMMANDS ' + '/' * 40);
    _commands.forEach(print);
    print('/' * 37 + ' END OF CANVAS COMMANDS ' + '/' * 36);
  }
}

abstract class PaintCommand {
  const PaintCommand();

  void apply(EngineCanvas canvas);
}

/// A [PaintCommand] that affect pixels on the screen (unlike, for example, the
/// [SaveCommand]).
abstract class DrawCommand extends PaintCommand {
  /// Whether the command is completely clipped out of the picture.
  bool isClippedOut = false;

  /// The left bound of the graphic produced by this command in picture-global
  /// coordinates.
  double leftBound = double.negativeInfinity;

  /// The top bound of the graphic produced by this command in picture-global
  /// coordinates.
  double topBound = double.negativeInfinity;

  /// The right bound of the graphic produced by this command in picture-global
  /// coordinates.
  double rightBound = double.infinity;

  /// The bottom bound of the graphic produced by this command in
  /// picture-global coordinates.
  double bottomBound = double.infinity;

  /// Whether this command intersects with the [clipRect].
  bool isInvisible(ui.Rect clipRect) {
    if (isClippedOut) {
      return true;
    }

    // Check top and bottom first because vertical scrolling is more common
    // than horizontal scrolling.
    return bottomBound < clipRect.top ||
        topBound > clipRect.bottom ||
        rightBound < clipRect.left ||
        leftBound > clipRect.right;
  }
}

class PaintSave extends PaintCommand {
  const PaintSave();

  @override
  void apply(EngineCanvas canvas) {
    canvas.save();
  }

  @override
  String toString() {
    if (assertionsEnabled) {
      return 'save()';
    } else {
      return super.toString();
    }
  }
}

class PaintRestore extends PaintCommand {
  const PaintRestore();

  @override
  void apply(EngineCanvas canvas) {
    canvas.restore();
  }

  @override
  String toString() {
    if (assertionsEnabled) {
      return 'restore()';
    } else {
      return super.toString();
    }
  }
}

class PaintTranslate extends PaintCommand {
  final double dx;
  final double dy;

  PaintTranslate(this.dx, this.dy);

  @override
  void apply(EngineCanvas canvas) {
    canvas.translate(dx, dy);
  }

  @override
  String toString() {
    if (assertionsEnabled) {
      return 'translate($dx, $dy)';
    } else {
      return super.toString();
    }
  }
}

class PaintScale extends PaintCommand {
  final double sx;
  final double sy;

  PaintScale(this.sx, this.sy);

  @override
  void apply(EngineCanvas canvas) {
    canvas.scale(sx, sy);
  }

  @override
  String toString() {
    if (assertionsEnabled) {
      return 'scale($sx, $sy)';
    } else {
      return super.toString();
    }
  }
}

class PaintRotate extends PaintCommand {
  final double radians;

  PaintRotate(this.radians);

  @override
  void apply(EngineCanvas canvas) {
    canvas.rotate(radians);
  }

  @override
  String toString() {
    if (assertionsEnabled) {
      return 'rotate($radians)';
    } else {
      return super.toString();
    }
  }
}

class PaintTransform extends PaintCommand {
  final Float32List matrix4;

  PaintTransform(this.matrix4);

  @override
  void apply(EngineCanvas canvas) {
    canvas.transform(matrix4);
  }

  @override
  String toString() {
    if (assertionsEnabled) {
      return 'transform(Matrix4.fromFloat32List(Float32List.fromList(<double>[${matrix4.join(', ')}])))';
    } else {
      return super.toString();
    }
  }
}

class PaintSkew extends PaintCommand {
  final double sx;
  final double sy;

  PaintSkew(this.sx, this.sy);

  @override
  void apply(EngineCanvas canvas) {
    canvas.skew(sx, sy);
  }

  @override
  String toString() {
    if (assertionsEnabled) {
      return 'skew($sx, $sy)';
    } else {
      return super.toString();
    }
  }
}

class PaintClipRect extends DrawCommand {
  final ui.Rect rect;
  final ui.ClipOp clipOp;

  PaintClipRect(this.rect, this.clipOp);

  @override
  void apply(EngineCanvas canvas) {
    canvas.clipRect(rect, clipOp);
  }

  @override
  String toString() {
    if (assertionsEnabled) {
      return 'clipRect($rect)';
    } else {
      return super.toString();
    }
  }
}

class PaintClipRRect extends DrawCommand {
  final ui.RRect rrect;

  PaintClipRRect(this.rrect);

  @override
  void apply(EngineCanvas canvas) {
    canvas.clipRRect(rrect);
  }

  @override
  String toString() {
    if (assertionsEnabled) {
      return 'clipRRect($rrect)';
    } else {
      return super.toString();
    }
  }
}

class PaintClipPath extends DrawCommand {
  final SurfacePath path;

  PaintClipPath(this.path);

  @override
  void apply(EngineCanvas canvas) {
    canvas.clipPath(path);
  }

  @override
  String toString() {
    if (assertionsEnabled) {
      return 'clipPath($path)';
    } else {
      return super.toString();
    }
  }
}

class PaintDrawColor extends DrawCommand {
  final ui.Color color;
  final ui.BlendMode blendMode;

  PaintDrawColor(this.color, this.blendMode);

  @override
  void apply(EngineCanvas canvas) {
    canvas.drawColor(color, blendMode);
  }

  @override
  String toString() {
    if (assertionsEnabled) {
      return 'drawColor($color, $blendMode)';
    } else {
      return super.toString();
    }
  }
}

class PaintDrawLine extends DrawCommand {
  final ui.Offset p1;
  final ui.Offset p2;
  final SurfacePaintData paint;

  PaintDrawLine(this.p1, this.p2, this.paint);

  @override
  void apply(EngineCanvas canvas) {
    canvas.drawLine(p1, p2, paint);
  }

  @override
  String toString() {
    if (assertionsEnabled) {
      return 'drawLine($p1, $p2, $paint)';
    } else {
      return super.toString();
    }
  }
}

class PaintDrawPaint extends DrawCommand {
  final SurfacePaintData paint;

  PaintDrawPaint(this.paint);

  @override
  void apply(EngineCanvas canvas) {
    canvas.drawPaint(paint);
  }

  @override
  String toString() {
    if (assertionsEnabled) {
      return 'drawPaint($paint)';
    } else {
      return super.toString();
    }
  }
}

class PaintDrawVertices extends DrawCommand {
  final ui.Vertices vertices;
  final ui.BlendMode blendMode;
  final SurfacePaintData paint;
  PaintDrawVertices(this.vertices, this.blendMode, this.paint);

  @override
  void apply(EngineCanvas canvas) {
    canvas.drawVertices(vertices as SurfaceVertices, blendMode, paint);
  }

  @override
  String toString() {
    if (assertionsEnabled) {
      return 'drawVertices($vertices, $blendMode, $paint)';
    } else {
      return super.toString();
    }
  }
}

class PaintDrawPoints extends DrawCommand {
  final Float32List points;
  final ui.PointMode pointMode;
  final SurfacePaintData paint;
  PaintDrawPoints(this.pointMode, this.points, this.paint);

  @override
  void apply(EngineCanvas canvas) {
    canvas.drawPoints(pointMode, points, paint);
  }

  @override
  String toString() {
    if (assertionsEnabled) {
      return 'drawPoints($pointMode, $points, $paint)';
    } else {
      return super.toString();
    }
  }
}

class PaintDrawRect extends DrawCommand {
  final ui.Rect rect;
  final SurfacePaintData paint;

  PaintDrawRect(this.rect, this.paint);

  @override
  void apply(EngineCanvas canvas) {
    canvas.drawRect(rect, paint);
  }

  @override
  String toString() {
    if (assertionsEnabled) {
      return 'drawRect($rect, $paint)';
    } else {
      return super.toString();
    }
  }
}

class PaintDrawRRect extends DrawCommand {
  final ui.RRect rrect;
  final SurfacePaintData paint;

  PaintDrawRRect(this.rrect, this.paint);

  @override
  void apply(EngineCanvas canvas) {
    canvas.drawRRect(rrect, paint);
  }

  @override
  String toString() {
    if (assertionsEnabled) {
      return 'drawRRect($rrect, $paint)';
    } else {
      return super.toString();
    }
  }
}

class PaintDrawDRRect extends DrawCommand {
  final ui.RRect outer;
  final ui.RRect inner;
  final SurfacePaintData paint;
  ui.Path? path;
  PaintDrawDRRect(this.outer, this.inner, this.paint) {
    path = ui.Path()
      ..fillType = ui.PathFillType.evenOdd
      ..addRRect(outer)
      ..addRRect(inner)
      ..close();
  }

  @override
  void apply(EngineCanvas canvas) {
    paint.style ??= ui.PaintingStyle.fill;
    canvas.drawPath(path!, paint);
  }

  @override
  String toString() {
    if (assertionsEnabled) {
      return 'drawDRRect($outer, $inner, $paint)';
    } else {
      return super.toString();
    }
  }
}

class PaintDrawOval extends DrawCommand {
  final ui.Rect rect;
  final SurfacePaintData paint;

  PaintDrawOval(this.rect, this.paint);

  @override
  void apply(EngineCanvas canvas) {
    canvas.drawOval(rect, paint);
  }

  @override
  String toString() {
    if (assertionsEnabled) {
      return 'drawOval($rect, $paint)';
    } else {
      return super.toString();
    }
  }
}

class PaintDrawCircle extends DrawCommand {
  final ui.Offset c;
  final double radius;
  final SurfacePaintData paint;

  PaintDrawCircle(this.c, this.radius, this.paint);

  @override
  void apply(EngineCanvas canvas) {
    canvas.drawCircle(c, radius, paint);
  }

  @override
  String toString() {
    if (assertionsEnabled) {
      return 'drawCircle($c, $radius, $paint)';
    } else {
      return super.toString();
    }
  }
}

class PaintDrawPath extends DrawCommand {
  final SurfacePath path;
  final SurfacePaintData paint;

  PaintDrawPath(this.path, this.paint);

  @override
  void apply(EngineCanvas canvas) {
    canvas.drawPath(path, paint);
  }

  @override
  String toString() {
    if (assertionsEnabled) {
      return 'drawPath($path, $paint)';
    } else {
      return super.toString();
    }
  }
}

class PaintDrawShadow extends DrawCommand {
  PaintDrawShadow(
      this.path, this.color, this.elevation, this.transparentOccluder);

  final SurfacePath path;
  final ui.Color color;
  final double elevation;
  final bool transparentOccluder;

  @override
  void apply(EngineCanvas canvas) {
    canvas.drawShadow(path, color, elevation, transparentOccluder);
  }

  @override
  String toString() {
    if (assertionsEnabled) {
      return 'drawShadow($path, $color, $elevation, $transparentOccluder)';
    } else {
      return super.toString();
    }
  }
}

class PaintDrawImage extends DrawCommand {
  final ui.Image image;
  final ui.Offset offset;
  final SurfacePaintData paint;

  PaintDrawImage(this.image, this.offset, this.paint);

  @override
  void apply(EngineCanvas canvas) {
    canvas.drawImage(image, offset, paint);
  }

  @override
  String toString() {
    if (assertionsEnabled) {
      return 'drawImage($image, $offset, $paint)';
    } else {
      return super.toString();
    }
  }
}

class PaintDrawImageRect extends DrawCommand {
  final ui.Image image;
  final ui.Rect src;
  final ui.Rect dst;
  final SurfacePaintData paint;

  PaintDrawImageRect(this.image, this.src, this.dst, this.paint);

  @override
  void apply(EngineCanvas canvas) {
    canvas.drawImageRect(image, src, dst, paint);
  }

  @override
  String toString() {
    if (assertionsEnabled) {
      return 'drawImageRect($image, $src, $dst, $paint)';
    } else {
      return super.toString();
    }
  }
}

class PaintDrawParagraph extends DrawCommand {
  final CanvasParagraph paragraph;
  final ui.Offset offset;

  PaintDrawParagraph(this.paragraph, this.offset);

  @override
  void apply(EngineCanvas canvas) {
    canvas.drawParagraph(paragraph, offset);
  }

  @override
  String toString() {
    if (assertionsEnabled) {
      return 'DrawParagraph(${paragraph.toPlainText()}, $offset)';
    } else {
      return super.toString();
    }
  }
}

class Subpath {
  double startX = 0.0;
  double startY = 0.0;
  double currentX = 0.0;
  double currentY = 0.0;

  final List<PathCommand> commands;

  Subpath(this.startX, this.startY) : commands = <PathCommand>[];

  Subpath shift(ui.Offset offset) {
    final Subpath result = Subpath(startX + offset.dx, startY + offset.dy)
      ..currentX = currentX + offset.dx
      ..currentY = currentY + offset.dy;

    for (final PathCommand command in commands) {
      result.commands.add(command.shifted(offset));
    }

    return result;
  }

  @override
  String toString() {
    if (assertionsEnabled) {
      return 'Subpath(${commands.join(', ')})';
    } else {
      return super.toString();
    }
  }
}

abstract class PathCommand {
  const PathCommand();

  PathCommand shifted(ui.Offset offset);

  /// Transform the command and add to targetPath.
  void transform(Float32List matrix4, SurfacePath targetPath);

  /// Helper method for implementing transforms.
  static ui.Offset _transformOffset(double x, double y, Float32List matrix4) =>
      ui.Offset((matrix4[0] * x) + (matrix4[4] * y) + matrix4[12],
          (matrix4[1] * x) + (matrix4[5] * y) + matrix4[13]);
}

class MoveTo extends PathCommand {
  final double x;
  final double y;

  const MoveTo(this.x, this.y);

  @override
  MoveTo shifted(ui.Offset offset) {
    return MoveTo(x + offset.dx, y + offset.dy);
  }

  @override
  void transform(Float32List matrix4, ui.Path targetPath) {
    final ui.Offset offset = PathCommand._transformOffset(x, y, matrix4);
    targetPath.moveTo(offset.dx, offset.dy);
  }

  @override
  String toString() {
    if (assertionsEnabled) {
      return 'MoveTo($x, $y)';
    } else {
      return super.toString();
    }
  }
}

class LineTo extends PathCommand {
  final double x;
  final double y;

  const LineTo(this.x, this.y);

  @override
  LineTo shifted(ui.Offset offset) {
    return LineTo(x + offset.dx, y + offset.dy);
  }

  @override
  void transform(Float32List matrix4, ui.Path targetPath) {
    final ui.Offset offset = PathCommand._transformOffset(x, y, matrix4);
    targetPath.lineTo(offset.dx, offset.dy);
  }

  @override
  String toString() {
    if (assertionsEnabled) {
      return 'LineTo($x, $y)';
    } else {
      return super.toString();
    }
  }
}

class Ellipse extends PathCommand {
  final double x;
  final double y;
  final double radiusX;
  final double radiusY;
  final double rotation;
  final double startAngle;
  final double endAngle;
  final bool anticlockwise;

  const Ellipse(this.x, this.y, this.radiusX, this.radiusY, this.rotation,
      this.startAngle, this.endAngle, this.anticlockwise);

  @override
  Ellipse shifted(ui.Offset offset) {
    return Ellipse(x + offset.dx, y + offset.dy, radiusX, radiusY, rotation,
        startAngle, endAngle, anticlockwise);
  }

  @override
  void transform(Float32List matrix4, SurfacePath targetPath) {
    final ui.Path bezierPath = ui.Path();
    _drawArcWithBezier(
        x,
        y,
        radiusX,
        radiusY,
        rotation,
        startAngle,
        anticlockwise ? startAngle - endAngle : endAngle - startAngle,
        matrix4,
        bezierPath);
    targetPath.addPathWithMode(bezierPath, 0, 0, matrix4, SPathAddPathMode.kAppend);
  }

  void _drawArcWithBezier(
      double centerX,
      double centerY,
      double radiusX,
      double radiusY,
      double rotation,
      double startAngle,
      double sweep,
      Float32List matrix4,
      ui.Path targetPath) {
    double ratio = sweep.abs() / (math.pi / 2.0);
    if ((1.0 - ratio).abs() < 0.0000001) {
      ratio = 1.0;
    }
    final int segments = math.max(ratio.ceil(), 1);
    final double anglePerSegment = sweep / segments;
    double angle = startAngle;
    for (int segment = 0; segment < segments; segment++) {
      _drawArcSegment(targetPath, centerX, centerY, radiusX, radiusY, rotation,
          angle, anglePerSegment, segment == 0, matrix4);
      angle += anglePerSegment;
    }
  }

  void _drawArcSegment(
      ui.Path path,
      double centerX,
      double centerY,
      double radiusX,
      double radiusY,
      double rotation,
      double startAngle,
      double sweep,
      bool startPath,
      Float32List matrix4) {
    final double s = 4 / 3 * math.tan(sweep / 4);

    // Rotate unit vector to startAngle and endAngle to use for computing start
    // and end points of segment.
    final double x1 = math.cos(startAngle);
    final double y1 = math.sin(startAngle);
    final double endAngle = startAngle + sweep;
    final double x2 = math.cos(endAngle);
    final double y2 = math.sin(endAngle);

    // Compute scaled curve control points.
    final double cpx1 = (x1 - y1 * s) * radiusX;
    final double cpy1 = (y1 + x1 * s) * radiusY;
    final double cpx2 = (x2 + y2 * s) * radiusX;
    final double cpy2 = (y2 - x2 * s) * radiusY;

    final double endPointX = centerX + x2 * radiusX;
    final double endPointY = centerY + y2 * radiusY;

    final double rotationRad = rotation * math.pi / 180.0;
    final double cosR = math.cos(rotationRad);
    final double sinR = math.sin(rotationRad);
    if (startPath) {
      final double scaledX1 = x1 * radiusX;
      final double scaledY1 = y1 * radiusY;
      if (rotation == 0.0) {
        path.moveTo(centerX + scaledX1, centerY + scaledY1);
      } else {
        final double rotatedStartX = (scaledX1 * cosR) + (scaledY1 * sinR);
        final double rotatedStartY = (scaledY1 * cosR) - (scaledX1 * sinR);
        path.moveTo(centerX + rotatedStartX, centerY + rotatedStartY);
      }
    }
    if (rotation == 0.0) {
      path.cubicTo(centerX + cpx1, centerY + cpy1, centerX + cpx2,
          centerY + cpy2, endPointX, endPointY);
    } else {
      final double rotatedCpx1 = centerX + (cpx1 * cosR) + (cpy1 * sinR);
      final double rotatedCpy1 = centerY + (cpy1 * cosR) - (cpx1 * sinR);
      final double rotatedCpx2 = centerX + (cpx2 * cosR) + (cpy2 * sinR);
      final double rotatedCpy2 = centerY + (cpy2 * cosR) - (cpx2 * sinR);
      final double rotatedEndX = centerX +
          ((endPointX - centerX) * cosR) +
          ((endPointY - centerY) * sinR);
      final double rotatedEndY = centerY +
          ((endPointY - centerY) * cosR) -
          ((endPointX - centerX) * sinR);
      path.cubicTo(rotatedCpx1, rotatedCpy1, rotatedCpx2, rotatedCpy2,
          rotatedEndX, rotatedEndY);
    }
  }

  @override
  String toString() {
    if (assertionsEnabled) {
      return 'Ellipse($x, $y, $radiusX, $radiusY)';
    } else {
      return super.toString();
    }
  }
}

class QuadraticCurveTo extends PathCommand {
  final double x1;
  final double y1;
  final double x2;
  final double y2;

  const QuadraticCurveTo(this.x1, this.y1, this.x2, this.y2);

  @override
  QuadraticCurveTo shifted(ui.Offset offset) {
    return QuadraticCurveTo(
        x1 + offset.dx, y1 + offset.dy, x2 + offset.dx, y2 + offset.dy);
  }

  @override
  void transform(Float32List matrix4, ui.Path targetPath) {
    final double m0 = matrix4[0];
    final double m1 = matrix4[1];
    final double m4 = matrix4[4];
    final double m5 = matrix4[5];
    final double m12 = matrix4[12];
    final double m13 = matrix4[13];
    final double transformedX1 = (m0 * x1) + (m4 * y1) + m12;
    final double transformedY1 = (m1 * x1) + (m5 * y1) + m13;
    final double transformedX2 = (m0 * x2) + (m4 * y2) + m12;
    final double transformedY2 = (m1 * x2) + (m5 * y2) + m13;
    targetPath.quadraticBezierTo(
        transformedX1, transformedY1, transformedX2, transformedY2);
  }

  @override
  String toString() {
    if (assertionsEnabled) {
      return 'QuadraticCurveTo($x1, $y1, $x2, $y2)';
    } else {
      return super.toString();
    }
  }
}

class BezierCurveTo extends PathCommand {
  final double x1;
  final double y1;
  final double x2;
  final double y2;
  final double x3;
  final double y3;

  const BezierCurveTo(this.x1, this.y1, this.x2, this.y2, this.x3, this.y3);

  @override
  BezierCurveTo shifted(ui.Offset offset) {
    return BezierCurveTo(x1 + offset.dx, y1 + offset.dy, x2 + offset.dx,
        y2 + offset.dy, x3 + offset.dx, y3 + offset.dy);
  }

  @override
  void transform(Float32List matrix4, ui.Path targetPath) {
    final double s0 = matrix4[0];
    final double s1 = matrix4[1];
    final double s4 = matrix4[4];
    final double s5 = matrix4[5];
    final double s12 = matrix4[12];
    final double s13 = matrix4[13];
    final double transformedX1 = (s0 * x1) + (s4 * y1) + s12;
    final double transformedY1 = (s1 * x1) + (s5 * y1) + s13;
    final double transformedX2 = (s0 * x2) + (s4 * y2) + s12;
    final double transformedY2 = (s1 * x2) + (s5 * y2) + s13;
    final double transformedX3 = (s0 * x3) + (s4 * y3) + s12;
    final double transformedY3 = (s1 * x3) + (s5 * y3) + s13;
    targetPath.cubicTo(transformedX1, transformedY1, transformedX2,
        transformedY2, transformedX3, transformedY3);
  }

  @override
  String toString() {
    if (assertionsEnabled) {
      return 'BezierCurveTo($x1, $y1, $x2, $y2, $x3, $y3)';
    } else {
      return super.toString();
    }
  }
}

class RectCommand extends PathCommand {
  final double x;
  final double y;
  final double width;
  final double height;

  const RectCommand(this.x, this.y, this.width, this.height);

  @override
  RectCommand shifted(ui.Offset offset) {
    return RectCommand(x + offset.dx, y + offset.dy, width, height);
  }

  @override
  void transform(Float32List matrix4, ui.Path targetPath) {
    final double s0 = matrix4[0];
    final double s1 = matrix4[1];
    final double s4 = matrix4[4];
    final double s5 = matrix4[5];
    final double s12 = matrix4[12];
    final double s13 = matrix4[13];
    final double transformedX1 = (s0 * x) + (s4 * y) + s12;
    final double transformedY1 = (s1 * x) + (s5 * y) + s13;
    final double x2 = x + width;
    final double y2 = y + height;
    final double transformedX2 = (s0 * x2) + (s4 * y) + s12;
    final double transformedY2 = (s1 * x2) + (s5 * y) + s13;
    final double transformedX3 = (s0 * x2) + (s4 * y2) + s12;
    final double transformedY3 = (s1 * x2) + (s5 * y2) + s13;
    final double transformedX4 = (s0 * x) + (s4 * y2) + s12;
    final double transformedY4 = (s1 * x) + (s5 * y2) + s13;
    if (transformedY1 == transformedY2 &&
        transformedY3 == transformedY4 &&
        transformedX1 == transformedX4 &&
        transformedX2 == transformedX3) {
      // It is still a rectangle.
      targetPath.addRect(ui.Rect.fromLTRB(
          transformedX1, transformedY1, transformedX3, transformedY3));
    } else {
      targetPath.moveTo(transformedX1, transformedY1);
      targetPath.lineTo(transformedX2, transformedY2);
      targetPath.lineTo(transformedX3, transformedY3);
      targetPath.lineTo(transformedX4, transformedY4);
      targetPath.close();
    }
  }

  @override
  String toString() {
    if (assertionsEnabled) {
      return 'Rect($x, $y, $width, $height)';
    } else {
      return super.toString();
    }
  }
}

class RRectCommand extends PathCommand {
  final ui.RRect rrect;

  const RRectCommand(this.rrect);

  @override
  RRectCommand shifted(ui.Offset offset) {
    return RRectCommand(rrect.shift(offset));
  }

  @override
  void transform(Float32List matrix4, SurfacePath targetPath) {
    final ui.Path roundRectPath = ui.Path();
    RRectToPathRenderer(roundRectPath).render(rrect);
    targetPath.addPathWithMode(roundRectPath, 0, 0, matrix4, SPathAddPathMode.kAppend);
  }

  @override
  String toString() {
    if (assertionsEnabled) {
      return '$rrect';
    } else {
      return super.toString();
    }
  }
}

class CloseCommand extends PathCommand {
  @override
  CloseCommand shifted(ui.Offset offset) {
    return this;
  }

  @override
  void transform(Float32List matrix4, ui.Path targetPath) {
    targetPath.close();
  }

  @override
  String toString() {
    if (assertionsEnabled) {
      return 'Close()';
    } else {
      return super.toString();
    }
  }
}

class _PaintBounds {
  // Bounds of maximum area that is paintable by canvas ops.
  final ui.Rect maxPaintBounds;

  bool _didPaintInsideClipArea = false;
  // Bounds of actually painted area. If _left is not set, reported paintBounds
  // should be empty since growLTRB calls were outside active clipping
  // region.
  double _left = double.maxFinite;
  double _top = double.maxFinite;
  double _right = -double.maxFinite;
  double _bottom = -double.maxFinite;

  // Stack of transforms.
  final List<Matrix4> _transforms = <Matrix4>[];
  // Stack of clip bounds.
  final List<ui.Rect?> _clipStack = <ui.Rect?>[];
  bool _currentMatrixIsIdentity = true;
  Matrix4 _currentMatrix = Matrix4.identity();
  bool _clipRectInitialized = false;
  double _currentClipLeft = 0.0,
      _currentClipTop = 0.0,
      _currentClipRight = 0.0,
      _currentClipBottom = 0.0;

  _PaintBounds(this.maxPaintBounds);

  void translate(double dx, double dy) {
    if (dx != 0.0 || dy != 0.0) {
      _currentMatrixIsIdentity = false;
    }
    _currentMatrix.translate(dx, dy);
  }

  void scale(double sx, double sy) {
    if (sx != 1.0 || sy != 1.0) {
      _currentMatrixIsIdentity = false;
    }
    _currentMatrix.scale(sx, sy, 1.0);
  }

  void rotateZ(double radians) {
    if (radians != 0.0) {
      _currentMatrixIsIdentity = false;
    }
    _currentMatrix.rotateZ(radians);
  }

  void transform(Float32List matrix4) {
    final Matrix4 m4 = Matrix4.fromFloat32List(matrix4);
    _currentMatrix.multiply(m4);
    _currentMatrixIsIdentity = _currentMatrix.isIdentity();
  }

  void skew(double sx, double sy) {
    _currentMatrixIsIdentity = false;

    // DO NOT USE Matrix4.skew(sx, sy)! It treats sx and sy values as radians,
    // but in our case they are transform matrix values.
    final Matrix4 skewMatrix = Matrix4.identity();
    final Float32List storage = skewMatrix.storage;
    storage[1] = sy;
    storage[4] = sx;
    _currentMatrix.multiply(skewMatrix);
  }

  static final Float32List _tempRectData = Float32List(4);

  void clipRect(final ui.Rect rect, DrawCommand command) {
    double left = rect.left;
    double top = rect.top;
    double right = rect.right;
    double bottom = rect.bottom;

    // If we have an active transform, calculate screen relative clipping
    // rectangle and union with current clipping rectangle.
    if (!_currentMatrixIsIdentity) {
      _tempRectData[0] = left;
      _tempRectData[1] = top;
      _tempRectData[2] = right;
      _tempRectData[3] = bottom;

      transformLTRB(_currentMatrix, _tempRectData);
      left = _tempRectData[0];
      top = _tempRectData[1];
      right = _tempRectData[2];
      bottom = _tempRectData[3];
    }

    if (!_clipRectInitialized) {
      _currentClipLeft = left;
      _currentClipTop = top;
      _currentClipRight = right;
      _currentClipBottom = bottom;
      _clipRectInitialized = true;
    } else {
      if (left > _currentClipLeft) {
        _currentClipLeft = left;
      }
      if (top > _currentClipTop) {
        _currentClipTop = top;
      }
      if (right < _currentClipRight) {
        _currentClipRight = right;
      }
      if (bottom < _currentClipBottom) {
        _currentClipBottom = bottom;
      }
    }
    if (_currentClipLeft >= _currentClipRight ||
        _currentClipTop >= _currentClipBottom) {
      command.isClippedOut = true;
    } else {
      command.leftBound = _currentClipLeft;
      command.topBound = _currentClipTop;
      command.rightBound = _currentClipRight;
      command.bottomBound = _currentClipBottom;
    }
  }

  ui.Rect? getDestinationClipBounds() {
    if (!_clipRectInitialized) {
      return null;
    } else {
      return ui.Rect.fromLTRB(
        _currentClipLeft,
        _currentClipTop,
        _currentClipRight,
        _currentClipBottom,
      );
    }
  }

  /// Grow painted area to include given rectangle.
  void grow(ui.Rect r, DrawCommand command) {
    growLTRB(r.left, r.top, r.right, r.bottom, command);
  }

  /// Grow painted area to include given rectangle and precompute
  /// clipped out state for command.
  void growLTRB(double left, double top, double right, double bottom,
      DrawCommand command) {
    if (left == right || top == bottom) {
      command.isClippedOut = true;
      return;
    }

    double transformedPointLeft = left;
    double transformedPointTop = top;
    double transformedPointRight = right;
    double transformedPointBottom = bottom;

    if (!_currentMatrixIsIdentity) {
      _tempRectData[0] = left;
      _tempRectData[1] = top;
      _tempRectData[2] = right;
      _tempRectData[3] = bottom;

      transformLTRB(_currentMatrix, _tempRectData);
      transformedPointLeft = _tempRectData[0];
      transformedPointTop = _tempRectData[1];
      transformedPointRight = _tempRectData[2];
      transformedPointBottom = _tempRectData[3];
    }

    if (_clipRectInitialized) {
      if (transformedPointLeft >= _currentClipRight) {
        command.isClippedOut = true;
        return;
      }
      if (transformedPointRight <= _currentClipLeft) {
        command.isClippedOut = true;
        return;
      }
      if (transformedPointTop >= _currentClipBottom) {
        command.isClippedOut = true;
        return;
      }
      if (transformedPointBottom <= _currentClipTop) {
        command.isClippedOut = true;
        return;
      }
      if (transformedPointLeft < _currentClipLeft) {
        transformedPointLeft = _currentClipLeft;
      }
      if (transformedPointRight > _currentClipRight) {
        transformedPointRight = _currentClipRight;
      }
      if (transformedPointTop < _currentClipTop) {
        transformedPointTop = _currentClipTop;
      }
      if (transformedPointBottom > _currentClipBottom) {
        transformedPointBottom = _currentClipBottom;
      }
    }

    command.leftBound = transformedPointLeft;
    command.topBound = transformedPointTop;
    command.rightBound = transformedPointRight;
    command.bottomBound = transformedPointBottom;

    if (_didPaintInsideClipArea) {
      _left = math.min(
          math.min(_left, transformedPointLeft), transformedPointRight);
      _right = math.max(
          math.max(_right, transformedPointLeft), transformedPointRight);
      _top =
          math.min(math.min(_top, transformedPointTop), transformedPointBottom);
      _bottom = math.max(
          math.max(_bottom, transformedPointTop), transformedPointBottom);
    } else {
      _left = math.min(transformedPointLeft, transformedPointRight);
      _right = math.max(transformedPointLeft, transformedPointRight);
      _top = math.min(transformedPointTop, transformedPointBottom);
      _bottom = math.max(transformedPointTop, transformedPointBottom);
    }
    _didPaintInsideClipArea = true;
  }

  /// Grow painted area to include given rectangle.
  void growBounds(ui.Rect bounds) {
    final double left = bounds.left;
    final double top = bounds.top;
    final double right = bounds.right;
    final double bottom = bounds.bottom;
    if (left == right || top == bottom) {
      return;
    }

    double transformedPointLeft = left;
    double transformedPointTop = top;
    double transformedPointRight = right;
    double transformedPointBottom = bottom;

    if (!_currentMatrixIsIdentity) {
      _tempRectData[0] = left;
      _tempRectData[1] = top;
      _tempRectData[2] = right;
      _tempRectData[3] = bottom;

      transformLTRB(_currentMatrix, _tempRectData);
      transformedPointLeft = _tempRectData[0];
      transformedPointTop = _tempRectData[1];
      transformedPointRight = _tempRectData[2];
      transformedPointBottom = _tempRectData[3];
    }

    if (_didPaintInsideClipArea) {
      _left = math.min(
          math.min(_left, transformedPointLeft), transformedPointRight);
      _right = math.max(
          math.max(_right, transformedPointLeft), transformedPointRight);
      _top =
          math.min(math.min(_top, transformedPointTop), transformedPointBottom);
      _bottom = math.max(
          math.max(_bottom, transformedPointTop), transformedPointBottom);
    } else {
      _left = math.min(transformedPointLeft, transformedPointRight);
      _right = math.max(transformedPointLeft, transformedPointRight);
      _top = math.min(transformedPointTop, transformedPointBottom);
      _bottom = math.max(transformedPointTop, transformedPointBottom);
    }
    _didPaintInsideClipArea = true;
  }

  void saveTransformsAndClip() {
    _transforms.add(_currentMatrix.clone());
    _clipStack.add(_clipRectInitialized
        ? ui.Rect.fromLTRB(_currentClipLeft, _currentClipTop, _currentClipRight,
            _currentClipBottom)
        : null);
  }

  void restoreTransformsAndClip() {
    _currentMatrix = _transforms.removeLast();
    final ui.Rect? clipRect = _clipStack.removeLast();
    if (clipRect != null) {
      _currentClipLeft = clipRect.left;
      _currentClipTop = clipRect.top;
      _currentClipRight = clipRect.right;
      _currentClipBottom = clipRect.bottom;
      _clipRectInitialized = true;
    } else if (_clipRectInitialized) {
      _clipRectInitialized = false;
    }
  }

  ui.Rect computeBounds() {
    if (!_didPaintInsideClipArea) {
      return ui.Rect.zero;
    }

    // The framework may send us NaNs in the case when it attempts to invert an
    // infinitely size rect.
    final double maxLeft = maxPaintBounds.left.isNaN
        ? double.negativeInfinity
        : maxPaintBounds.left;
    final double maxRight =
        maxPaintBounds.right.isNaN ? double.infinity : maxPaintBounds.right;
    final double maxTop =
        maxPaintBounds.top.isNaN ? double.negativeInfinity : maxPaintBounds.top;
    final double maxBottom =
        maxPaintBounds.bottom.isNaN ? double.infinity : maxPaintBounds.bottom;

    final double left = math.min(_left, _right);
    final double right = math.max(_left, _right);
    final double top = math.min(_top, _bottom);
    final double bottom = math.max(_top, _bottom);

    if (right < maxLeft || bottom < maxTop) {
      // Computed and max bounds do not intersect.
      return ui.Rect.zero;
    }

    return ui.Rect.fromLTRB(
      math.max(left, maxLeft),
      math.max(top, maxTop),
      math.min(right, maxRight),
      math.min(bottom, maxBottom),
    );
  }

  @override
  String toString() {
    if (assertionsEnabled) {
      final ui.Rect bounds = computeBounds();
      return '_PaintBounds($bounds of size ${bounds.size})';
    } else {
      return super.toString();
    }
  }
}

/// Computes the length of the visual effect caused by paint parameters, such
/// as blur and stroke width.
///
/// This paint spread should be taken into accound when estimating bounding
/// boxes for paint operations that apply the paint.
double _getPaintSpread(SurfacePaint paint) {
  double spread = 0.0;
  final ui.MaskFilter? maskFilter = paint.maskFilter;
  if (maskFilter != null) {
    // Multiply by 2 because the sigma is the standard deviation rather than
    // the length of the blur.
    // See also: https://developer.mozilla.org/en-US/docs/Web/CSS/filter-function/blur
    spread += maskFilter.webOnlySigma * 2.0;
  }
  if (paint.strokeWidth != 0) {
    // The multiplication by sqrt(2) is to account for line joints that
    // meet at 90-degree angle. Division by 2 is because only half of the
    // stroke is sticking out of the original shape. The other half is
    // inside the shape.
    const double sqrtOfTwoDivByTwo = 0.70710678118;
    spread += paint.strokeWidth * sqrtOfTwoDivByTwo;
  }
  return spread;
}

/// Contains metrics collected by recording canvas to provide data for
/// rendering heuristics (canvas use vs DOM).
class RenderStrategy {
  /// Whether paint commands contain image elements.
  bool hasImageElements = false;

  /// Whether paint commands contain paragraphs.
  bool hasParagraphs = false;

  /// Whether paint commands are doing arbitrary operations
  /// not expressible via pure DOM elements.
  ///
  /// This is used to decide whether to use simplified DomCanvas.
  bool hasArbitraryPaint = false;

  /// Whether commands are executed within a shadermask or color filter.
  ///
  /// Webkit doesn't apply filters to canvas elements in its child
  /// element tree. When this is set to true, we prevent canvas usage in
  /// bitmap canvas and instead render using dom primitives and svg only.
  bool isInsideSvgFilterTree = false;

  RenderStrategy();

  /// Merges render strategy settings from a child recording.
  void merge(RenderStrategy childStrategy) {
    hasImageElements |= childStrategy.hasImageElements;
    hasParagraphs |= childStrategy.hasParagraphs;
    hasArbitraryPaint |= childStrategy.hasArbitraryPaint;
  }
}
