// 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:async';
import 'dart:convert';
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 'package:web_engine_tester/golden_tester.dart';

import '../common/fake_asset_manager.dart';
import '../common/test_initialization.dart';
import 'utils.dart';

const String kGlitchShaderSksl = r'''
{
  "sksl": "// This SkSL shader is autogenerated by spirv-cross.\n\nfloat4 flutter_FragCoord;\n\nuniform vec2 uResolution;\nuniform float uTime;\nuniform shader uTex;\nuniform half2 uTex_size;\n\nvec4 oColor;\n\nvec2 FLT_flutter_local_FlutterFragCoord()\n{\n    return flutter_FragCoord.xy;\n}\n\nfloat FLT_flutter_local_cubicPulse(float c, float w, inout float x)\n{\n    x = abs(x - c);\n    if (x > w)\n    {\n        return 0.0;\n    }\n    x /= w;\n    return 1.0 - ((x * x) * (3.0 - (2.0 * x)));\n}\n\nfloat FLT_flutter_local_twoSin(inout float x)\n{\n    x = (6.4899997711181640625 * x) - 0.64999997615814208984375;\n    float t = ((-0.699999988079071044921875) * sin(6.80000019073486328125 * x)) + (1.39999997615814208984375 * sin(2.900000095367431640625 * x));\n    t = (t / 4.099999904632568359375) + 0.5;\n    return t;\n}\n\nfloat FLT_flutter_local_hash_1d(float v)\n{\n    float u = 50.0 * sin(v * 3000.0);\n    return (2.0 * fract((2.0 * u) * u)) - 1.0;\n}\n\nvoid FLT_main()\n{\n    vec2 uv = vec2(FLT_flutter_local_FlutterFragCoord()) / uResolution;\n    float param = 0.5;\n    float param_1 = 0.0500000007450580596923828125;\n    float param_2 = fract(uTime / 4.0);\n    float _127 = FLT_flutter_local_cubicPulse(param, param_1, param_2);\n    float t_2 = _127;\n    float param_3 = fract(uTime / 5.0);\n    float _134 = FLT_flutter_local_twoSin(param_3);\n    float t_1 = _134;\n    float glitchScale = mix(0.0, 8.0, t_1 + t_2);\n    float aberrationSize = mix(0.0, 5.0, t_1 + t_2);\n    float param_4 = uv.y;\n    float h = FLT_flutter_local_hash_1d(param_4);\n    float hs = sign(h);\n    h = max(h, 0.0);\n    h *= h;\n    h = floor(h + float(0.5)) * hs;\n    uv += (vec2(h * glitchScale, 0.0) / uResolution);\n    vec2 redOffset = vec2(aberrationSize, 0.0) / uResolution;\n    vec2 greenOffset = vec2(0.0) / uResolution;\n    vec2 blueOffset = vec2(-aberrationSize, 0.0) / uResolution;\n    vec2 redUv = uv + redOffset;\n    vec2 greenUv = uv + greenOffset;\n    vec2 blueUv = uv + blueOffset;\n    vec2 ra = uTex.eval(uTex_size *  redUv).xw;\n    vec2 ga = uTex.eval(uTex_size *  greenUv).yw;\n    vec2 ba = uTex.eval(uTex_size *  blueUv).zw;\n    ra.x /= ra.y;\n    ga.x /= ga.y;\n    ba.x /= ba.y;\n    float alpha = max(ra.y, max(ga.y, ba.y));\n    oColor = vec4(ra.x, ga.x, ba.x, 1.0) * alpha;\n}\n\nhalf4 main(float2 iFragCoord)\n{\n      flutter_FragCoord = float4(iFragCoord, 0, 0);\n      FLT_main();\n      return oColor;\n}\n",
  "stage": 1,
  "target_platform": 2,
  "uniforms": [
    {
      "array_elements": 0,
      "bit_width": 32,
      "columns": 1,
      "location": 0,
      "name": "uResolution",
      "rows": 2,
      "type": 10
    },
    {
      "array_elements": 0,
      "bit_width": 32,
      "columns": 1,
      "location": 1,
      "name": "uTime",
      "rows": 1,
      "type": 10
    },
    {
      "array_elements": 0,
      "bit_width": 0,
      "columns": 1,
      "location": 2,
      "name": "uTex",
      "rows": 1,
      "type": 12
    }
  ]
}
''';

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

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

  late FakeAssetScope assetScope;
  setUp(() {
    assetScope = fakeAssetManager.pushAssetScope();
    assetScope.setAsset(
      'glitch_shader',
      ByteData.sublistView(utf8.encode(kGlitchShaderSksl))
    );
  });

  tearDown(() {
    fakeAssetManager.popAssetScope(assetScope);
  });

  const ui.Rect drawRegion = ui.Rect.fromLTWH(0, 0, 300, 300);
  const ui.Rect imageRegion = ui.Rect.fromLTWH(0, 0, 150, 150);

  // Emits a set of rendering tests for an image
  // `imageGenerator` should produce an image that is 150x150 pixels.
  void emitImageTests(String name, Future<ui.Image> Function() imageGenerator) {
    group(name, () {
      late ui.Image image;
      setUp(() async {
        image = await imageGenerator();
      });

      tearDown(() {
        image.dispose();
      });

      test('drawImage', () async {
        final ui.Image image = await imageGenerator();

        final ui.PictureRecorder recorder = ui.PictureRecorder();
        final ui.Canvas canvas = ui.Canvas(recorder, drawRegion);
        canvas.drawImage(image, const ui.Offset(100, 100), ui.Paint());

        await drawPictureUsingCurrentRenderer(recorder.endRecording());

        await matchGoldenFile('${name}_canvas_drawImage.png', region: drawRegion);
      });

      test('drawImageRect', () async {
        final ui.Image image = await imageGenerator();

        final ui.PictureRecorder recorder = ui.PictureRecorder();
        final ui.Canvas canvas = ui.Canvas(recorder, drawRegion);
        canvas.drawImageRect(
          image,
          const ui.Rect.fromLTRB(50, 50, 100, 100),
          const ui.Rect.fromLTRB(100, 100, 150, 150),
          ui.Paint()
        );

        await drawPictureUsingCurrentRenderer(recorder.endRecording());

        await matchGoldenFile('${name}_canvas_drawImageRect.png', region: drawRegion);
      });

      test('drawImageNine', () async {
        final ui.Image image = await imageGenerator();

        final ui.PictureRecorder recorder = ui.PictureRecorder();
        final ui.Canvas canvas = ui.Canvas(recorder, drawRegion);
        canvas.drawImageNine(
          image,
          const ui.Rect.fromLTRB(50, 50, 100, 100),
          drawRegion,
          ui.Paint()
        );

        await drawPictureUsingCurrentRenderer(recorder.endRecording());

        await matchGoldenFile('${name}_canvas_drawImageNine.png', region: drawRegion);
      });

      test('image_shader_cubic_rotated', () async {
        final ui.Image image = await imageGenerator();

        final Float64List matrix = Matrix4.rotationZ(pi / 6).toFloat64();
        final ui.ImageShader shader = ui.ImageShader(
          image,
          ui.TileMode.repeated,
          ui.TileMode.repeated,
          matrix,
          filterQuality: ui.FilterQuality.high,
        );
        final ui.PictureRecorder recorder = ui.PictureRecorder();
        final ui.Canvas canvas = ui.Canvas(recorder, drawRegion);
        canvas.drawOval(
          const ui.Rect.fromLTRB(0, 50, 300, 250),
          ui.Paint()..shader = shader
        );

        await drawPictureUsingCurrentRenderer(recorder.endRecording());
        await matchGoldenFile('${name}_image_shader_cubic_rotated.png', region: drawRegion);
      });

      test('fragment_shader_sampler', () async {
        final ui.Image image = await imageGenerator();

        final ui.FragmentProgram program = await renderer.createFragmentProgram('glitch_shader');
        final ui.FragmentShader shader = program.fragmentShader();

        // Resolution
        shader.setFloat(0, 300);
        shader.setFloat(1, 300);

        // Time
        shader.setFloat(2, 2);

        // Image
        shader.setImageSampler(0, image);

        final ui.PictureRecorder recorder = ui.PictureRecorder();
        final ui.Canvas canvas = ui.Canvas(recorder, drawRegion);
        canvas.drawCircle(const ui.Offset(150, 150), 100, ui.Paint()..shader = shader);

        await drawPictureUsingCurrentRenderer(recorder.endRecording());

        await matchGoldenFile('${name}_fragment_shader_sampler.png', region: drawRegion);
      }, skip: isHtml); // HTML doesn't support fragment shaders

      test('toByteData_rgba', () async {
        final ui.Image image = await imageGenerator();

        final ByteData? rgbaData = await image.toByteData();
        expect(rgbaData, isNotNull);
        expect(rgbaData!.lengthInBytes, isNonZero);
      });

      test('toByteData_png', () async {
        final ui.Image image = await imageGenerator();

        final ByteData? pngData = await image.toByteData(format: ui.ImageByteFormat.png);
        expect(pngData, isNotNull);
        expect(pngData!.lengthInBytes, isNonZero);
      }, skip: isHtml); // https://github.com/flutter/flutter/issues/126611
    });
  }

  emitImageTests('picture_toImage', () {
    final ui.PictureRecorder recorder = ui.PictureRecorder();
    final ui.Canvas canvas = ui.Canvas(recorder, imageRegion);
    for (int y = 0; y < 15; y++) {
      for (int x = 0; x < 15; x++) {
        final ui.Offset center = ui.Offset(x * 10 + 5, y * 10 + 5);
        final ui.Color color = ui.Color.fromRGBO(
          (center.dx * 256 / 150).round(),
          (center.dy * 256 / 150).round(), 0, 1);
        canvas.drawCircle(center, 5, ui.Paint()..color = color);
      }
    }
    return recorder.endRecording().toImage(150, 150);
  });

  Uint8List generatePixelData(
    int width,
    int height,
    ui.Color Function(double, double) generator
  ) {
    final Uint8List data = Uint8List(width * height * 4);
    int outputIndex = 0;
    for (int y = 0; y < height; y++) {
      for (int x = 0; x < width; x++) {
        final ui.Color pixelColor = generator(
          (2.0 * x / width) - 1.0,
          (2.0 * y / height) - 1.0,
        );
        data[outputIndex++] = pixelColor.red;
        data[outputIndex++] = pixelColor.green;
        data[outputIndex++] = pixelColor.blue;
        data[outputIndex++] = pixelColor.alpha;
      }
    }
    return data;
  }

  emitImageTests('decodeImageFromPixels_unscaled', () {
    final Uint8List pixels = generatePixelData(150, 150, (double x, double y) {
      final double r = sqrt(x * x + y * y);
      final double theta = atan2(x, y);
      return ui.Color.fromRGBO(
        (255 * (sin(r * 10.0) + 1.0) / 2.0).round(),
        (255 * (sin(theta * 10.0) + 1.0) / 2.0).round(),
        0,
        1,
      );
    });
    final Completer<ui.Image> completer = Completer<ui.Image>();
    ui.decodeImageFromPixels(pixels, 150, 150, ui.PixelFormat.rgba8888, completer.complete);
    return completer.future;
  });

  // https://github.com/flutter/flutter/issues/126603
  if (!isHtml) {
    emitImageTests('decodeImageFromPixels_scaled', () {
      final Uint8List pixels = generatePixelData(50, 50, (double x, double y) {
        final double r = sqrt(x * x + y * y);
        final double theta = atan2(x, y);
        return ui.Color.fromRGBO(
          (255 * (sin(r * 10.0) + 1.0) / 2.0).round(),
          (255 * (sin(theta * 10.0) + 1.0) / 2.0).round(),
          0,
          1,
        );
      });
      final Completer<ui.Image> completer = Completer<ui.Image>();
      ui.decodeImageFromPixels(
        pixels,
        50,
        50,
        ui.PixelFormat.rgba8888,
        completer.complete,
        targetWidth: 150,
        targetHeight: 150,
      );
      return completer.future;
    });
  }

  emitImageTests('codec_uri', () async {
    final ui.Codec codec = await renderer.instantiateImageCodecFromUrl(
      Uri(path: '/test_images/mandrill_128.png')
    );
    expect(codec.frameCount, 1);

    final ui.FrameInfo info = await codec.getNextFrame();
    return info.image;
  });

  emitImageTests('codec_list_resized', () async {
    final ByteBuffer data = await httpFetchByteBuffer('/test_images/mandrill_128.png');
    final ui.Codec codec = await renderer.instantiateImageCodec(
      data.asUint8List(),
      targetWidth: 150,
      targetHeight: 150,
    );
    expect(codec.frameCount, 1);

    final ui.FrameInfo info = await codec.getNextFrame();
    return info.image;
  });
}
