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

import 'package:test/bootstrap/browser.dart';
import 'package:test/test.dart';
import 'package:ui/src/engine.dart';
import 'package:ui/ui.dart' as ui;

import '../common/matchers.dart';
import '../common/test_initialization.dart';

void main() {
  internalBootstrapBrowserTest(() => testMain);
}

Future<void> testMain() async {
  setUpUnitTests(
    setUpTestViewDimensions: false,
  );

  final bool deviceClipRoundsOut = renderer is! HtmlRenderer;
  runCanvasTests(deviceClipRoundsOut: deviceClipRoundsOut);
}

void runCanvasTests({required bool deviceClipRoundsOut}) {
  setUp(() {
    EngineSemanticsOwner.debugResetSemantics();
  });

  group('ui.Canvas transform tests', () {
    void transformsClose(Float64List value, Float64List expected) {
      expect(expected.length, equals(16));
      expect(value.length, equals(16));
      for (int r = 0; r < 4; r++) {
        for (int c = 0; c < 4; c++) {
          expect(value[r*4 + c], within(from: expected[r*4 + c]));
        }
      }
    }

    void transformsNotClose(Float64List value, Float64List expected) {
      // We check the lengths here even though [transformsClose] will
      // check them so that the [TestFailure] we catch below can only
      // be due to a difference in matrix values.
      expect(expected.length, equals(16));
      expect(value.length, equals(16));
      try {
        transformsClose(value, expected);
      } on TestFailure {
        return;
      }
      throw TestFailure('transforms were too close to equal'); // ignore: only_throw_errors
    }

    test('ui.Canvas.translate affects canvas.getTransform', () {
      final ui.PictureRecorder recorder = ui.PictureRecorder();
      final ui.Canvas canvas = ui.Canvas(recorder);
      canvas.translate(12, 14.5);
      final Float64List matrix = Matrix4.translationValues(12, 14.5, 0).toFloat64();
      final Float64List curMatrix = canvas.getTransform();
      transformsClose(curMatrix, matrix);
      canvas.translate(10, 10);
      final Float64List newCurMatrix = canvas.getTransform();
      transformsNotClose(newCurMatrix, matrix);
      transformsClose(curMatrix, matrix);
    });

    test('ui.Canvas.scale affects canvas.getTransform', () {
      final ui.PictureRecorder recorder = ui.PictureRecorder();
      final ui.Canvas canvas = ui.Canvas(recorder);
      canvas.scale(12, 14.5);
      final Float64List matrix = Matrix4.diagonal3Values(12, 14.5, 1).toFloat64();
      final Float64List curMatrix = canvas.getTransform();
      transformsClose(curMatrix, matrix);
      canvas.scale(10, 10);
      final Float64List newCurMatrix = canvas.getTransform();
      transformsNotClose(newCurMatrix, matrix);
      transformsClose(curMatrix, matrix);
    });

    test('Canvas.rotate affects canvas.getTransform', () async {
      final ui.PictureRecorder recorder = ui.PictureRecorder();
      final ui.Canvas canvas = ui.Canvas(recorder);
      canvas.rotate(pi);
      final Float64List matrix = Matrix4.rotationZ(pi).toFloat64();
      final Float64List curMatrix = canvas.getTransform();
      transformsClose(curMatrix, matrix);
      canvas.rotate(pi / 2);
      final Float64List newCurMatrix = canvas.getTransform();
      transformsNotClose(newCurMatrix, matrix);
      transformsClose(curMatrix, matrix);
    });

    test('Canvas.skew affects canvas.getTransform', () async {
      final ui.PictureRecorder recorder = ui.PictureRecorder();
      final ui.Canvas canvas = ui.Canvas(recorder);
      canvas.skew(12, 14.5);
      final Float64List matrix = (Matrix4.identity()..setEntry(0, 1, 12)..setEntry(1, 0, 14.5)).toFloat64();
      final Float64List curMatrix = canvas.getTransform();
      transformsClose(curMatrix, matrix);
      canvas.skew(10, 10);
      final Float64List newCurMatrix = canvas.getTransform();
      transformsNotClose(newCurMatrix, matrix);
      transformsClose(curMatrix, matrix);
    });

    test('Canvas.transform affects canvas.getTransform', () async {
      final ui.PictureRecorder recorder = ui.PictureRecorder();
      final ui.Canvas canvas = ui.Canvas(recorder);
      final Float64List matrix = (Matrix4.identity()..translate(12.0, 14.5)..scale(12.0, 14.5)).toFloat64();
      canvas.transform(matrix);
      final Float64List curMatrix = canvas.getTransform();
      transformsClose(curMatrix, matrix);
      canvas.translate(10, 10);
      final Float64List newCurMatrix = canvas.getTransform();
      transformsNotClose(newCurMatrix, matrix);
      transformsClose(curMatrix, matrix);
    });
  });

  void rectsClose(ui.Rect value, ui.Rect expected) {
    expect(value.left,   closeTo(expected.left,   1e-6));
    expect(value.top,    closeTo(expected.top,    1e-6));
    expect(value.right,  closeTo(expected.right,  1e-6));
    expect(value.bottom, closeTo(expected.bottom, 1e-6));
  }

  void rectsNotClose(ui.Rect value, ui.Rect expected) {
    try {
      rectsClose(value, expected);
    } on TestFailure {
      return;
    }
    throw TestFailure('transforms were too close to equal'); // ignore: only_throw_errors
  }

  group('ui.Canvas clip tests', () {
    test('Canvas.clipRect affects canvas.getClipBounds', () async {
      final ui.PictureRecorder recorder = ui.PictureRecorder();
      final ui.Canvas canvas = ui.Canvas(recorder, const ui.Rect.fromLTRB(0, 0, 100, 100));
      const ui.Rect clipRawBounds = ui.Rect.fromLTRB(10.2, 11.3, 20.4, 25.7);
      const ui.Rect clipExpandedBounds = ui.Rect.fromLTRB(10, 11, 21, 26);
      final ui.Rect clipDestBounds = deviceClipRoundsOut ? clipExpandedBounds : clipRawBounds;
      canvas.clipRect(clipRawBounds);

      // Save initial return values for testing restored values
      final ui.Rect initialLocalBounds = canvas.getLocalClipBounds();
      final ui.Rect initialDestinationBounds = canvas.getDestinationClipBounds();
      rectsClose(initialLocalBounds, clipExpandedBounds);
      rectsClose(initialDestinationBounds, clipDestBounds);

      canvas.save();
      canvas.clipRect(const ui.Rect.fromLTRB(0, 0, 15, 15));
      // Both clip bounds have changed
      rectsNotClose(canvas.getLocalClipBounds(), clipExpandedBounds);
      rectsNotClose(canvas.getDestinationClipBounds(), clipDestBounds);
      // Previous return values have not changed
      rectsClose(initialLocalBounds, clipExpandedBounds);
      rectsClose(initialDestinationBounds, clipDestBounds);
      canvas.restore();

      // save/restore returned the values to their original values
      expect(canvas.getLocalClipBounds(), initialLocalBounds);
      expect(canvas.getDestinationClipBounds(), initialDestinationBounds);

      canvas.save();
      canvas.scale(2, 2);
      const ui.Rect scaledExpandedBounds = ui.Rect.fromLTRB(5, 5.5, 10.5, 13);
      rectsClose(canvas.getLocalClipBounds(), scaledExpandedBounds);
      // Destination bounds are unaffected by transform
      rectsClose(canvas.getDestinationClipBounds(), clipDestBounds);
      canvas.restore();

      // save/restore returned the values to their original values
      expect(canvas.getLocalClipBounds(), initialLocalBounds);
      expect(canvas.getDestinationClipBounds(), initialDestinationBounds);
    });

    test('Canvas.clipRRect affects canvas.getClipBounds', () async {
      final ui.PictureRecorder recorder = ui.PictureRecorder();
      final ui.Canvas canvas = ui.Canvas(recorder, const ui.Rect.fromLTRB(0, 0, 100, 100));
      const ui.Rect clipRawBounds = ui.Rect.fromLTRB(10.2, 11.3, 20.4, 25.7);
      const ui.Rect clipExpandedBounds = ui.Rect.fromLTRB(10, 11, 21, 26);
      final ui.Rect clipDestBounds = deviceClipRoundsOut ? clipExpandedBounds : clipRawBounds;
      final ui.RRect clip = ui.RRect.fromRectAndRadius(clipRawBounds, const ui.Radius.circular(3));
      canvas.clipRRect(clip);

      // Save initial return values for testing restored values
      final ui.Rect initialLocalBounds = canvas.getLocalClipBounds();
      final ui.Rect initialDestinationBounds = canvas.getDestinationClipBounds();
      rectsClose(initialLocalBounds, clipExpandedBounds);
      rectsClose(initialDestinationBounds, clipDestBounds);

      canvas.save();
      canvas.clipRect(const ui.Rect.fromLTRB(0, 0, 15, 15));
      // Both clip bounds have changed
      rectsNotClose(canvas.getLocalClipBounds(), clipExpandedBounds);
      rectsNotClose(canvas.getDestinationClipBounds(), clipDestBounds);
      // Previous return values have not changed
      rectsClose(initialLocalBounds, clipExpandedBounds);
      rectsClose(initialDestinationBounds, clipDestBounds);
      canvas.restore();

      // save/restore returned the values to their original values
      expect(canvas.getLocalClipBounds(), initialLocalBounds);
      expect(canvas.getDestinationClipBounds(), initialDestinationBounds);

      canvas.save();
      canvas.scale(2, 2);
      const ui.Rect scaledExpandedBounds = ui.Rect.fromLTRB(5, 5.5, 10.5, 13);
      rectsClose(canvas.getLocalClipBounds(), scaledExpandedBounds);
      // Destination bounds are unaffected by transform
      rectsClose(canvas.getDestinationClipBounds(), clipDestBounds);
      canvas.restore();

      // save/restore returned the values to their original values
      expect(canvas.getLocalClipBounds(), initialLocalBounds);
      expect(canvas.getDestinationClipBounds(), initialDestinationBounds);
    });

    test('Canvas.clipPath affects canvas.getClipBounds', () async {
      final ui.PictureRecorder recorder = ui.PictureRecorder();
      final ui.Canvas canvas = ui.Canvas(recorder, const ui.Rect.fromLTRB(0, 0, 100, 100));
      const ui.Rect clipRawBounds = ui.Rect.fromLTRB(10.2, 11.3, 20.4, 25.7);
      const ui.Rect clipExpandedBounds = ui.Rect.fromLTRB(10, 11, 21, 26);
      final ui.Rect clipDestBounds = deviceClipRoundsOut ? clipExpandedBounds : clipRawBounds;
      final ui.Path clip = ui.Path()..addRect(clipRawBounds)..addOval(clipRawBounds);
      canvas.clipPath(clip);

      // Save initial return values for testing restored values
      final ui.Rect initialLocalBounds = canvas.getLocalClipBounds();
      final ui.Rect initialDestinationBounds = canvas.getDestinationClipBounds();
      rectsClose(initialLocalBounds, clipExpandedBounds);
      rectsClose(initialDestinationBounds, clipDestBounds);

      canvas.save();
      canvas.clipRect(const ui.Rect.fromLTRB(0, 0, 15, 15));
      // Both clip bounds have changed
      rectsNotClose(canvas.getLocalClipBounds(), clipExpandedBounds);
      rectsNotClose(canvas.getDestinationClipBounds(), clipDestBounds);
      // Previous return values have not changed
      rectsClose(initialLocalBounds, clipExpandedBounds);
      rectsClose(initialDestinationBounds, clipDestBounds);
      canvas.restore();

      // save/restore returned the values to their original values
      expect(canvas.getLocalClipBounds(), initialLocalBounds);
      expect(canvas.getDestinationClipBounds(), initialDestinationBounds);

      canvas.save();
      canvas.scale(2, 2);
      const ui.Rect scaledExpandedBounds = ui.Rect.fromLTRB(5, 5.5, 10.5, 13);
      rectsClose(canvas.getLocalClipBounds(), scaledExpandedBounds);
      // Destination bounds are unaffected by transform
      rectsClose(canvas.getDestinationClipBounds(), clipDestBounds);
      canvas.restore();

      // save/restore returned the values to their original values
      expect(canvas.getLocalClipBounds(), initialLocalBounds);
      expect(canvas.getDestinationClipBounds(), initialDestinationBounds);
    });

    test('Canvas.clipRect(diff) does not affect canvas.getClipBounds', () async {
      final ui.PictureRecorder recorder = ui.PictureRecorder();
      final ui.Canvas canvas = ui.Canvas(recorder, const ui.Rect.fromLTRB(0, 0, 100, 100));
      const ui.Rect clipRawBounds = ui.Rect.fromLTRB(10.2, 11.3, 20.4, 25.7);
      const ui.Rect clipExpandedBounds = ui.Rect.fromLTRB(10, 11, 21, 26);
      final ui.Rect clipDestBounds = deviceClipRoundsOut ? clipExpandedBounds : clipRawBounds;
      canvas.clipRect(clipRawBounds);

      // Save initial return values for testing restored values
      final ui.Rect initialLocalBounds = canvas.getLocalClipBounds();
      final ui.Rect initialDestinationBounds = canvas.getDestinationClipBounds();
      rectsClose(initialLocalBounds, clipExpandedBounds);
      rectsClose(initialDestinationBounds, clipDestBounds);

      canvas.clipRect(const ui.Rect.fromLTRB(0, 0, 15, 15), clipOp: ui.ClipOp.difference);
      expect(canvas.getLocalClipBounds(), initialLocalBounds);
      expect(canvas.getDestinationClipBounds(), initialDestinationBounds);
    });
  });

  group('RestoreToCount function tests', () {
    test('RestoreToCount can work', () async {
      final ui.PictureRecorder recorder = ui.PictureRecorder();
      final ui.Canvas canvas = ui.Canvas(recorder);
      canvas.save();
      canvas.save();
      canvas.save();
      canvas.save();
      canvas.save();
      expect(canvas.getSaveCount(), 6);
      canvas.restoreToCount(2);
      expect(canvas.getSaveCount(), 2);
      canvas.restore();
      expect(canvas.getSaveCount(), 1);
    });

    test('RestoreToCount count less than 1, the stack should be reset', () async {
      final ui.PictureRecorder recorder = ui.PictureRecorder();
      final ui.Canvas canvas = ui.Canvas(recorder);
      canvas.save();
      canvas.save();
      canvas.save();
      canvas.save();
      canvas.save();
      expect(canvas.getSaveCount(), equals(6));
      canvas.restoreToCount(0);
      expect(canvas.getSaveCount(), equals(1));
    });

    test('RestoreToCount count greater than current [getSaveCount]', () async {
      final ui.PictureRecorder recorder = ui.PictureRecorder();
      final ui.Canvas canvas = ui.Canvas(recorder);
      canvas.save();
      canvas.save();
      canvas.save();
      canvas.save();
      canvas.save();
      expect(canvas.getSaveCount(), equals(6));
      canvas.restoreToCount(canvas.getSaveCount() + 1);
      expect(canvas.getSaveCount(), equals(6));
    });
  });
}
