// 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);

  setUp(() {
    EngineSemantics.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');
    }

    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');
  }

  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);
      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, clipExpandedBounds);

      canvas.save();
      canvas.clipRect(const ui.Rect.fromLTRB(0, 0, 15, 15));
      // Both clip bounds have changed
      rectsNotClose(canvas.getLocalClipBounds(), clipExpandedBounds);
      rectsNotClose(canvas.getDestinationClipBounds(), clipExpandedBounds);
      // Previous return values have not changed
      rectsClose(initialLocalBounds, clipExpandedBounds);
      rectsClose(initialDestinationBounds, clipExpandedBounds);
      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(), clipExpandedBounds);
      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.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, clipExpandedBounds);

      canvas.save();
      canvas.clipRect(const ui.Rect.fromLTRB(0, 0, 15, 15));
      // Both clip bounds have changed
      rectsNotClose(canvas.getLocalClipBounds(), clipExpandedBounds);
      rectsNotClose(canvas.getDestinationClipBounds(), clipExpandedBounds);
      // Previous return values have not changed
      rectsClose(initialLocalBounds, clipExpandedBounds);
      rectsClose(initialDestinationBounds, clipExpandedBounds);
      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(), clipExpandedBounds);
      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.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, clipExpandedBounds);

      canvas.save();
      canvas.clipRect(const ui.Rect.fromLTRB(0, 0, 15, 15));
      // Both clip bounds have changed
      rectsNotClose(canvas.getLocalClipBounds(), clipExpandedBounds);
      rectsNotClose(canvas.getDestinationClipBounds(), clipExpandedBounds);
      // Previous return values have not changed
      rectsClose(initialLocalBounds, clipExpandedBounds);
      rectsClose(initialDestinationBounds, clipExpandedBounds);
      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(), clipExpandedBounds);
      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);
      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, clipExpandedBounds);

      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));
    });
  });
}
