// Copyright 2014 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 'package:flutter/material.dart';

import '../common.dart';

// Various tests to verify that the Opacity layer propagates the opacity to various
// combinations of children that can apply it themselves.
// See https://github.com/flutter/flutter/issues/75697
class OpacityPeepholePage extends StatelessWidget {
  const OpacityPeepholePage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Opacity Peephole tests')),
      body: ListView(
        key: const Key(kOpacityScrollableName),
        children: <Widget>[
          for (OpacityPeepholeCase variant in allOpacityPeepholeCases)
            ElevatedButton(
              key: Key(variant.route),
              child: Text(variant.name),
              onPressed: () {
                Navigator.pushNamed(context, variant.route);
              },
            ),
        ],
      ),
    );
  }
}

typedef ValueBuilder = Widget Function(double v);
typedef AnimationBuilder = Widget Function(Animation<double> animation);

double _opacity(double v) => v * 0.5 + 0.25;
int _red(double v) => (v * 255).round();
int _green(double v) => _red(1 - v);
int _blue(double v) => 0;

class OpacityPeepholeCase {
  OpacityPeepholeCase.forValue({required String route, required String name, required ValueBuilder builder})
      : this.forAnimation(
    route: route,
    name: name,
    builder: (Animation<double> animation) => AnimatedBuilder(
      animation: animation,
      builder: (BuildContext context, Widget? child) => builder(animation.value),
    ),
  );

  OpacityPeepholeCase.forAnimation({required this.route, required this.name, required AnimationBuilder builder})
      : animationBuilder = builder;

  final String route;
  final String name;
  final AnimationBuilder animationBuilder;

  Widget buildPage(BuildContext context) {
    return VariantPage(variant: this);
  }
}

List<OpacityPeepholeCase> allOpacityPeepholeCases = <OpacityPeepholeCase>[
  // Tests that Opacity can hand down value to a simple child
  OpacityPeepholeCase.forValue(
    route: kOpacityPeepholeOneRectRouteName,
    name: 'One Big Rectangle',
    builder: (double v) {
      return Opacity(
        opacity: _opacity(v),
        child: Container(
          width: 300,
          height: 400,
          color: Color.fromARGB(255, _red(v), _green(v), _blue(v)),
        ),
      );
    }
  ),
  // Tests that a column of Opacity widgets can individually hand their values down to simple children
  OpacityPeepholeCase.forValue(
    route: kOpacityPeepholeColumnOfOpacityRouteName,
    name: 'Column of Opacity',
    builder: (double v) {
      return Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: <Widget>[
          for (int i = 0; i < 10; i++, v = 1 - v)
            Opacity(
              opacity: _opacity(v),
              child: Padding(
                padding: const EdgeInsets.all(5),
                child: Container(
                  width: 300,
                  height: 30,
                  color: Color.fromARGB(255, _red(v), _green(v), _blue(v)),
                ),
              ),
            ),
        ],
      );
    },
  ),
  // Tests that an Opacity can hand value down to a cached child
  OpacityPeepholeCase.forValue(
      route: kOpacityPeepholeOpacityOfCachedChildRouteName,
      name: 'Opacity of Cached Child',
      builder: (double v) {
        // ChildV starts as a constant so the same color pattern always appears and the child will be cached
        double childV = 0;
        return Opacity(
          opacity: _opacity(v),
          child: RepaintBoundary(
            child: SizedBox(
              width: 300,
              height: 400,
              child: Stack(
                children: <Widget>[
                  for (double i = 0; i < 100; i += 10, childV = 1 - childV)
                    Positioned.fromRelativeRect(
                      rect: RelativeRect.fromLTRB(i, i, i, i),
                      child: Container(
                        color: Color.fromARGB(255, _red(childV), _green(childV), _blue(childV)),
                      ),
                    ),
                ],
              ),
            ),
          ),
        );
      }
  ),
  // Tests that an Opacity can hand a value down to a Column of simple non-overlapping children
  OpacityPeepholeCase.forValue(
    route: kOpacityPeepholeOpacityOfColumnRouteName,
    name: 'Opacity of Column',
    builder: (double v) {
      return Opacity(
        opacity: _opacity(v),
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            for (int i = 0; i < 10; i++, v = 1 - v)
              Padding(
                padding: const EdgeInsets.all(5),
                // RepaintBoundary here to avoid combining children into 1 big Picture
                child: RepaintBoundary(
                  child: Container(
                    width: 300,
                    height: 30,
                    color: Color.fromARGB(255, _red(v), _green(v), _blue(v)),
                  ),
                ),
              ),
          ],
        ),
      );
    },
  ),
  // Tests that an entire grid of Opacity objects can hand their values down to their simple children
  OpacityPeepholeCase.forValue(
    route: kOpacityPeepholeGridOfOpacityRouteName,
    name: 'Grid of Opacity',
    builder: (double v) {
      double rowV = v;
      double colV = rowV;
      return Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: <Widget>[
          for (int i = 0; i < 10; i++, rowV = 1 - rowV, colV = rowV)
            Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                for (int j = 0; j < 7; j++, colV = 1 - colV)
                  Opacity(
                    opacity: _opacity(colV),
                    child: Padding(
                      padding: const EdgeInsets.all(5),
                      child: Container(
                        width: 30,
                        height: 30,
                        color: Color.fromARGB(255, _red(colV), _green(colV), _blue(colV)),
                      ),
                    ),
                  ),
              ],
            ),
        ],
      );
    },
  ),
  // tests if an Opacity can hand its value down to a 2D grid of simple non-overlapping children.
  // The success of this case would depend on the sophistication of the non-overlapping tests.
  OpacityPeepholeCase.forValue(
    route: kOpacityPeepholeOpacityOfGridRouteName,
    name: 'Opacity of Grid',
    builder: (double v) {
      double rowV = v;
      double colV = rowV;
      return Opacity(
        opacity: _opacity(v),
        child: SizedBox(
          width: 300,
          height: 400,
          child: Stack(
            children: <Widget>[
              for (int i = 0; i < 10; i++, rowV = 1 - rowV, colV = rowV)
                for (int j = 0; j < 7; j++, colV = 1 - colV)
                  Positioned.fromRect(
                    rect: Rect.fromLTWH(j * 40 + 5, i * 40 + 5, 30, 30),
                    // RepaintBoundary here to avoid combining the 70 children into a single Picture
                    child: RepaintBoundary(
                      child: Container(
                        color: Color.fromARGB(255, _red(colV), _green(colV), _blue(colV)),
                      ),
                    ),
                  ),
            ],
          ),
        ),
      );
    },
  ),
  // tests if an Opacity can hand its value down to a Column of non-overlapping rows of non-overlapping simple children.
  // This test only requires linear non-overlapping tests to succeed.
  OpacityPeepholeCase.forValue(
    route: kOpacityPeepholeOpacityOfColOfRowsRouteName,
    name: 'Opacity of Column of Rows',
    builder: (double v) {
      double rowV = v;
      double colV = v;
      return Opacity(
        opacity: _opacity(v),
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            for (int i = 0; i < 10; i++, rowV = 1 - rowV, colV = rowV)
              Padding(
                padding: const EdgeInsets.only(top: 5, bottom: 5),
                // RepaintBoundary here to separate each row into a separate layer child
                child: RepaintBoundary(
                  child: Row(
                    mainAxisAlignment: MainAxisAlignment.center,
                    children: <Widget>[
                      for (int j = 0; j < 7; j++, colV = 1 - colV)
                        Padding(
                          padding: const EdgeInsets.only(left: 5, right: 5),
                          // RepaintBoundary here to prevent the row children combining into a single Picture
                          child: RepaintBoundary(
                            child: Container(
                              width: 30,
                              height: 30,
                              color: Color.fromARGB(255, _red(colV), _green(colV), _blue(colV)),
                            ),
                          ),
                        ),
                    ],
                  ),
                ),
              ),
          ],
        ),
      );
    },
  ),
  OpacityPeepholeCase.forAnimation(
    route: kOpacityPeepholeFadeTransitionTextRouteName,
    name: 'FadeTransition text',
    builder: (Animation<double> animation) {
      return FadeTransition(
        opacity: Tween<double>(begin: 0.25, end: 0.75).animate(animation),
        child: const SizedBox(
          width: 300,
          height: 400,
          child: Center(
            child: Text('Hello, World',
              style: TextStyle(fontSize: 48),
            ),
          ),
        ),
      );
    },
  ),
  OpacityPeepholeCase.forValue(
    route: kOpacityPeepholeGridOfRectsWithAlphaRouteName,
    name: 'Grid of Rectangles with alpha',
    builder: (double v) {
      return Opacity(
        opacity: _opacity(v),
        child: SizedBox.expand(
          child: CustomPaint(
            painter: RectGridPainter((Canvas canvas, Size size) {
              const int numRows = 10;
              const int numCols = 7;
              const double rectWidth = 30;
              const double rectHeight = 30;
              final double hGap = (size.width - numCols * rectWidth) / (numCols + 1);
              final double vGap = (size.height - numRows * rectHeight) / (numRows + 1);
              final double gap = min(hGap, vGap);
              final double xOffset = (size.width - (numCols * (rectWidth + gap) - gap)) * 0.5;
              final double yOffset = (size.height - (numRows * (rectHeight + gap) - gap)) * 0.5;
              final Paint rectPaint = Paint();
              for (int r = 0; r < numRows; r++, v = 1 - v) {
                final double y = yOffset + r * (rectHeight + gap);
                double cv = v;
                for (int c = 0; c < numCols; c++, cv = 1 - cv) {
                  final double x = xOffset + c * (rectWidth + gap);
                  rectPaint.color = Color.fromRGBO(_red(cv), _green(cv), _blue(cv), _opacity(cv));
                  final Rect rect = Rect.fromLTWH(x, y, rectWidth, rectHeight);
                  canvas.drawRect(rect, rectPaint);
                }
              }
            }),
          ),
        ),
      );
    },
  ),
  OpacityPeepholeCase.forValue(
    route: kOpacityPeepholeGridOfAlphaSaveLayerRectsRouteName,
    name: 'Grid of alpha SaveLayers of Rectangles',
    builder: (double v) {
      return Opacity(
        opacity: _opacity(v),
        child: SizedBox.expand(
          child: CustomPaint(
            painter: RectGridPainter((Canvas canvas, Size size) {
              const int numRows = 10;
              const int numCols = 7;
              const double rectWidth = 30;
              const double rectHeight = 30;
              final double hGap = (size.width - numCols * rectWidth) / (numCols + 1);
              final double vGap = (size.height - numRows * rectHeight) / (numRows + 1);
              final double gap = min(hGap, vGap);
              final double xOffset = (size.width - (numCols * (rectWidth + gap) - gap)) * 0.5;
              final double yOffset = (size.height - (numRows * (rectHeight + gap) - gap)) * 0.5;
              final Paint rectPaint = Paint();
              final Paint layerPaint = Paint();
              for (int r = 0; r < numRows; r++, v = 1 - v) {
                final double y = yOffset + r * (rectHeight + gap);
                double cv = v;
                for (int c = 0; c < numCols; c++, cv = 1 - cv) {
                  final double x = xOffset + c * (rectWidth + gap);
                  rectPaint.color = Color.fromRGBO(_red(cv), _green(cv), _blue(cv), 1.0);
                  layerPaint.color = Color.fromRGBO(255, 255, 255, _opacity(cv));
                  final Rect rect = Rect.fromLTWH(x, y, rectWidth, rectHeight);
                  canvas.saveLayer(null, layerPaint);
                  canvas.drawRect(rect, rectPaint);
                  canvas.restore();
                }
              }
            }),
          ),
        ),
      );
    },
  ),
  OpacityPeepholeCase.forValue(
    route: kOpacityPeepholeColumnOfAlphaSaveLayerRowsOfRectsRouteName,
    name: 'Grid with alpha SaveLayer on Rows',
    builder: (double v) {
      return Opacity(
        opacity: _opacity(v),
        child: SizedBox.expand(
          child: CustomPaint(
            painter: RectGridPainter((Canvas canvas, Size size) {
              const int numRows = 10;
              const int numCols = 7;
              const double rectWidth = 30;
              const double rectHeight = 30;
              final double hGap = (size.width - numCols * rectWidth) / (numCols + 1);
              final double vGap = (size.height - numRows * rectHeight) / (numRows + 1);
              final double gap = min(hGap, vGap);
              final double xOffset = (size.width - (numCols * (rectWidth + gap) - gap)) * 0.5;
              final double yOffset = (size.height - (numRows * (rectHeight + gap) - gap)) * 0.5;
              final Paint rectPaint = Paint();
              final Paint layerPaint = Paint();
              for (int r = 0; r < numRows; r++, v = 1 - v) {
                final double y = yOffset + r * (rectHeight + gap);
                layerPaint.color = Color.fromRGBO(255, 255, 255, _opacity(v));
                canvas.saveLayer(null, layerPaint);
                double cv = v;
                for (int c = 0; c < numCols; c++, cv = 1 - cv) {
                  final double x = xOffset + c * (rectWidth + gap);
                  rectPaint.color = Color.fromRGBO(_red(cv), _green(cv), _blue(cv), 1.0);
                  final Rect rect = Rect.fromLTWH(x, y, rectWidth, rectHeight);
                  canvas.drawRect(rect, rectPaint);
                }
                canvas.restore();
              }
            }),
          ),
        ),
      );
    },
  ),
];

class RectGridPainter extends CustomPainter {
  RectGridPainter(this.painter);

  final void Function(Canvas canvas, Size size) painter;

  @override
  void paint(Canvas canvas, Size size) => painter(canvas, size);

  @override
  bool shouldRepaint(CustomPainter oldDelegate) => true;
}

Map<String, WidgetBuilder> opacityPeepholeRoutes = <String, WidgetBuilder>{
  for (OpacityPeepholeCase variant in allOpacityPeepholeCases)
    variant.route: variant.buildPage,
};

class VariantPage extends StatefulWidget {
  const VariantPage({super.key, required this.variant});

  final OpacityPeepholeCase variant;

  @override
  State<VariantPage> createState() => VariantPageState();
}

class VariantPageState extends State<VariantPage> with SingleTickerProviderStateMixin {
  late AnimationController _controller;

  @override
  void initState() {
    super.initState();

    _controller = AnimationController(vsync: this, duration: const Duration(seconds: 4));
    _controller.repeat(reverse: true);
  }

  @override
  void dispose() {
    _controller.dispose();

    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.variant.name),
      ),
      body: Center(
        child: widget.variant.animationBuilder(_controller),
      ),
    );
  }
}