// 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' as math;

import 'package:flutter/foundation.dart' show clampDouble;
import 'package:flutter/gestures.dart' show DragStartBehavior;
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart' show ViewportOffset;


// BUILDER DELEGATE ---

final TwoDimensionalChildBuilderDelegate builderDelegate = TwoDimensionalChildBuilderDelegate(
  maxXIndex: 5,
  maxYIndex: 5,
  builder: (BuildContext context, ChildVicinity vicinity) {
    return Container(
      key: ValueKey<ChildVicinity>(vicinity),
      color: vicinity.xIndex.isEven && vicinity.yIndex.isEven
        ? Colors.amber[100]
        : (vicinity.xIndex.isOdd && vicinity.yIndex.isOdd
        ? Colors.blueAccent[100]
        : null),
      height: 200,
      width: 200,
      child: Center(child: Text('R${vicinity.xIndex}:C${vicinity.yIndex}')),
    );
  }
);

// Creates a simple 2D table of 200x200 squares with a builder delegate.
Widget simpleBuilderTest({
  Axis mainAxis = Axis.vertical,
  bool? primary,
  ScrollableDetails? verticalDetails,
  ScrollableDetails? horizontalDetails,
  TwoDimensionalChildBuilderDelegate? delegate,
  double? cacheExtent,
  DiagonalDragBehavior? diagonalDrag,
  Clip? clipBehavior,
  String? restorationID,
  bool useCacheExtent = false,
  bool applyDimensions = true,
  bool forgetToLayoutChild = false,
  bool setLayoutOffset = true,
}) {
  return MaterialApp(
    theme: ThemeData(useMaterial3: false),
    restorationScopeId: restorationID,
    home: Scaffold(
      body: SimpleBuilderTableView(
        mainAxis: mainAxis,
        verticalDetails: verticalDetails ?? const ScrollableDetails.vertical(),
        horizontalDetails: horizontalDetails ?? const ScrollableDetails.horizontal(),
        cacheExtent: cacheExtent,
        useCacheExtent: useCacheExtent,
        diagonalDragBehavior: diagonalDrag ?? DiagonalDragBehavior.none,
        clipBehavior: clipBehavior ?? Clip.hardEdge,
        delegate: delegate ?? builderDelegate,
        applyDimensions: applyDimensions,
        forgetToLayoutChild: forgetToLayoutChild,
        setLayoutOffset: setLayoutOffset,
      ),
    ),
  );
}

class SimpleBuilderTableView extends TwoDimensionalScrollView {
  const SimpleBuilderTableView({
    super.key,
    super.primary,
    super.mainAxis = Axis.vertical,
    super.verticalDetails = const ScrollableDetails.vertical(),
    super.horizontalDetails = const ScrollableDetails.horizontal(),
    required TwoDimensionalChildBuilderDelegate delegate,
    super.cacheExtent,
    super.diagonalDragBehavior = DiagonalDragBehavior.none,
    super.dragStartBehavior = DragStartBehavior.start,
    super.keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual,
    super.clipBehavior = Clip.hardEdge,
    this.useCacheExtent = false,
    this.applyDimensions = true,
    this.forgetToLayoutChild = false,
    this.setLayoutOffset = true,
  }) : super(delegate: delegate);

  // Piped through for testing in RenderTwoDimensionalViewport
  final bool useCacheExtent;
  final bool applyDimensions;
  final bool forgetToLayoutChild;
  final bool setLayoutOffset;

  @override
  Widget buildViewport(BuildContext context, ViewportOffset verticalOffset, ViewportOffset horizontalOffset) {
    return SimpleBuilderTableViewport(
      horizontalOffset: horizontalOffset,
      horizontalAxisDirection: horizontalDetails.direction,
      verticalOffset: verticalOffset,
      verticalAxisDirection: verticalDetails.direction,
      mainAxis: mainAxis,
      delegate: delegate as TwoDimensionalChildBuilderDelegate,
      cacheExtent: cacheExtent,
      clipBehavior: clipBehavior,
      useCacheExtent: useCacheExtent,
      applyDimensions: applyDimensions,
      forgetToLayoutChild: forgetToLayoutChild,
      setLayoutOffset: setLayoutOffset,
    );
  }
}

class SimpleBuilderTableViewport extends TwoDimensionalViewport {
  const SimpleBuilderTableViewport({
    super.key,
    required super.verticalOffset,
    required super.verticalAxisDirection,
    required super.horizontalOffset,
    required super.horizontalAxisDirection,
    required TwoDimensionalChildBuilderDelegate delegate,
    required super.mainAxis,
    super.cacheExtent,
    super.clipBehavior = Clip.hardEdge,
    this.useCacheExtent = false,
    this.applyDimensions = true,
    this.forgetToLayoutChild = false,
    this.setLayoutOffset = true,
  }) : super(delegate: delegate);

  // Piped through for testing in RenderTwoDimensionalViewport
  final bool useCacheExtent;
  final bool applyDimensions;
  final bool forgetToLayoutChild;
  final bool setLayoutOffset;

  @override
  RenderTwoDimensionalViewport createRenderObject(BuildContext context) {
    return RenderSimpleBuilderTableViewport(
      horizontalOffset: horizontalOffset,
      horizontalAxisDirection: horizontalAxisDirection,
      verticalOffset: verticalOffset,
      verticalAxisDirection: verticalAxisDirection,
      mainAxis: mainAxis,
      delegate: delegate as TwoDimensionalChildBuilderDelegate,
      childManager: context as TwoDimensionalChildManager,
      cacheExtent: cacheExtent,
      clipBehavior: clipBehavior,
      useCacheExtent: useCacheExtent,
      applyDimensions: applyDimensions,
      forgetToLayoutChild: forgetToLayoutChild,
      setLayoutOffset: setLayoutOffset,
    );
  }

  @override
  void updateRenderObject(BuildContext context, RenderSimpleBuilderTableViewport renderObject) {
    renderObject
      ..horizontalOffset = horizontalOffset
      ..horizontalAxisDirection = horizontalAxisDirection
      ..verticalOffset = verticalOffset
      ..verticalAxisDirection = verticalAxisDirection
      ..mainAxis = mainAxis
      ..delegate = delegate
      ..cacheExtent = cacheExtent
      ..clipBehavior = clipBehavior;
  }
}

class RenderSimpleBuilderTableViewport extends RenderTwoDimensionalViewport {
  RenderSimpleBuilderTableViewport({
    required super.horizontalOffset,
    required super.horizontalAxisDirection,
    required super.verticalOffset,
    required super.verticalAxisDirection,
    required TwoDimensionalChildBuilderDelegate delegate,
    required super.mainAxis,
    required super.childManager,
    super.cacheExtent,
    super.clipBehavior = Clip.hardEdge,
    this.applyDimensions = true,
    this.setLayoutOffset = true,
    this.useCacheExtent = false,
    this.forgetToLayoutChild = false,
  }) : super(delegate: delegate);

  // These are to test conditions to validate subclass implementations after
  // layoutChildSequence
  final bool applyDimensions;
  final bool setLayoutOffset;
  final bool useCacheExtent;
  final bool forgetToLayoutChild;

  RenderBox? testGetChildFor(ChildVicinity vicinity) => getChildFor(vicinity);

  @override
  TestExtendedParentData parentDataOf(RenderBox child) {
    return child.parentData! as TestExtendedParentData;
  }

  @override
  void setupParentData(RenderBox child) {
    if (child.parentData is! TestExtendedParentData) {
      child.parentData = TestExtendedParentData();
    }
  }

  @override
  void layoutChildSequence() {
    // Really simple table implementation for testing.
    // Every child is 200x200 square
    final double horizontalPixels = horizontalOffset.pixels;
    final double verticalPixels = verticalOffset.pixels;
    final double viewportWidth = viewportDimension.width + (useCacheExtent ? cacheExtent : 0.0);
    final double viewportHeight = viewportDimension.height + (useCacheExtent ? cacheExtent : 0.0);
    final TwoDimensionalChildBuilderDelegate builderDelegate = delegate as TwoDimensionalChildBuilderDelegate;

    final int maxRowIndex;
    final int maxColumnIndex;
    maxRowIndex = builderDelegate.maxYIndex ?? 5;
    maxColumnIndex = builderDelegate.maxXIndex ?? 5;

    final int leadingColumn = math.max((horizontalPixels / 200).floor(), 0);
    final int leadingRow = math.max((verticalPixels / 200).floor(), 0);
    final int trailingColumn = math.min(
      ((horizontalPixels + viewportWidth) / 200).ceil(),
      maxColumnIndex,
    );
    final int trailingRow = math.min(
      ((verticalPixels + viewportHeight) / 200).ceil(),
      maxRowIndex,
    );

    double xLayoutOffset = (leadingColumn * 200) - horizontalOffset.pixels;
    for (int column = leadingColumn; column <= trailingColumn; column++) {
      double yLayoutOffset = (leadingRow * 200) - verticalOffset.pixels;
      for (int row = leadingRow; row <= trailingRow; row++) {
        final ChildVicinity vicinity = ChildVicinity(xIndex: column, yIndex: row);
        final RenderBox child = buildOrObtainChildFor(vicinity)!;
        if (!forgetToLayoutChild) {
          child.layout(constraints.tighten(width: 200.0, height: 200.0));
        }

        if (setLayoutOffset) {
          parentDataOf(child).layoutOffset = Offset(xLayoutOffset, yLayoutOffset);
        }
        yLayoutOffset += 200;
      }
      xLayoutOffset += 200;
    }
    if (applyDimensions) {
      final double verticalExtent = 200 * (maxRowIndex + 1);
      verticalOffset.applyContentDimensions(
        0.0,
        clampDouble(verticalExtent - viewportDimension.height, 0.0, double.infinity),
      );
      final double horizontalExtent = 200 * (maxColumnIndex + 1);
      horizontalOffset.applyContentDimensions(
        0.0,
        clampDouble(horizontalExtent - viewportDimension.width, 0.0, double.infinity),
      );
    }
  }
}

// LIST DELEGATE ---
final List<List<Widget>> children = List<List<Widget>>.generate(
  100,
  (int xIndex) {
    return List<Widget>.generate(
      100,
      (int yIndex) {
        return Container(
          color: xIndex.isEven && yIndex.isEven
            ? Colors.amber[100]
            : (xIndex.isOdd && yIndex.isOdd
              ? Colors.blueAccent[100]
              : null),
          height: 200,
          width: 200,
          child: Center(child: Text('R$xIndex:C$yIndex')),
        );
      },
    );
  },
);

// Builds a simple 2D table of 200x200 squares with a list delegate.
Widget simpleListTest({
  Axis mainAxis = Axis.vertical,
  bool? primary,
  ScrollableDetails? verticalDetails,
  ScrollableDetails? horizontalDetails,
  TwoDimensionalChildListDelegate? delegate,
  double? cacheExtent,
  DiagonalDragBehavior? diagonalDrag,
  Clip? clipBehavior,
}) {
  return MaterialApp(
    theme: ThemeData(useMaterial3: true),
    home: Scaffold(
      body: SimpleListTableView(
        mainAxis: mainAxis,
        verticalDetails: verticalDetails ?? const ScrollableDetails.vertical(),
        horizontalDetails: horizontalDetails ?? const ScrollableDetails.horizontal(),
        cacheExtent: cacheExtent,
        diagonalDragBehavior: diagonalDrag ?? DiagonalDragBehavior.none,
        clipBehavior: clipBehavior ?? Clip.hardEdge,
        delegate: delegate ?? TwoDimensionalChildListDelegate(children: children),
      ),
    ),
  );
}

class SimpleListTableView extends TwoDimensionalScrollView {
  const SimpleListTableView({
    super.key,
    super.primary,
    super.mainAxis = Axis.vertical,
    super.verticalDetails = const ScrollableDetails.vertical(),
    super.horizontalDetails = const ScrollableDetails.horizontal(),
    required TwoDimensionalChildListDelegate delegate,
    super.cacheExtent,
    super.diagonalDragBehavior = DiagonalDragBehavior.none,
    super.dragStartBehavior = DragStartBehavior.start,
    super.keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual,
    super.clipBehavior = Clip.hardEdge,
  }) : super(delegate: delegate);

  @override
  Widget buildViewport(BuildContext context, ViewportOffset verticalOffset, ViewportOffset horizontalOffset) {
    return SimpleListTableViewport(
      horizontalOffset: horizontalOffset,
      horizontalAxisDirection: horizontalDetails.direction,
      verticalOffset: verticalOffset,
      verticalAxisDirection: verticalDetails.direction,
      mainAxis: mainAxis,
      delegate: delegate as TwoDimensionalChildListDelegate,
      cacheExtent: cacheExtent,
      clipBehavior: clipBehavior,
    );
  }
}

class SimpleListTableViewport extends TwoDimensionalViewport {
  const SimpleListTableViewport({
    super.key,
    required super.verticalOffset,
    required super.verticalAxisDirection,
    required super.horizontalOffset,
    required super.horizontalAxisDirection,
    required TwoDimensionalChildListDelegate delegate,
    required super.mainAxis,
    super.cacheExtent,
    super.clipBehavior = Clip.hardEdge,
  }) : super(delegate: delegate);

  @override
  RenderTwoDimensionalViewport createRenderObject(BuildContext context) {
    return RenderSimpleListTableViewport(
      horizontalOffset: horizontalOffset,
      horizontalAxisDirection: horizontalAxisDirection,
      verticalOffset: verticalOffset,
      verticalAxisDirection: verticalAxisDirection,
      mainAxis: mainAxis,
      delegate: delegate as TwoDimensionalChildListDelegate,
      childManager: context as TwoDimensionalChildManager,
      cacheExtent: cacheExtent,
      clipBehavior: clipBehavior,
    );
  }

  @override
  void updateRenderObject(BuildContext context, RenderSimpleListTableViewport renderObject) {
    renderObject
      ..horizontalOffset = horizontalOffset
      ..horizontalAxisDirection = horizontalAxisDirection
      ..verticalOffset = verticalOffset
      ..verticalAxisDirection = verticalAxisDirection
      ..mainAxis = mainAxis
      ..delegate = delegate
      ..cacheExtent = cacheExtent
      ..clipBehavior = clipBehavior;
  }
}

class RenderSimpleListTableViewport extends RenderTwoDimensionalViewport {
  RenderSimpleListTableViewport({
    required super.horizontalOffset,
    required super.horizontalAxisDirection,
    required super.verticalOffset,
    required super.verticalAxisDirection,
    required TwoDimensionalChildListDelegate delegate,
    required super.mainAxis,
    required super.childManager,
    super.cacheExtent,
    super.clipBehavior = Clip.hardEdge,
  }) : super(delegate: delegate);

  @override
  void layoutChildSequence() {
    // Really simple table implementation for testing.
    // Every child is 200x200 square
    final double horizontalPixels = horizontalOffset.pixels;
    final double verticalPixels = verticalOffset.pixels;
    final TwoDimensionalChildListDelegate listDelegate = delegate as TwoDimensionalChildListDelegate;
    final int rowCount;
    final int columnCount;
    rowCount = listDelegate.children.length;
    columnCount = listDelegate.children[0].length;

    final int leadingColumn = math.max((horizontalPixels / 200).floor(), 0);
    final int leadingRow = math.max((verticalPixels / 200).floor(), 0);
    final int trailingColumn = math.min(
      ((horizontalPixels + viewportDimension.width) / 200).ceil(),
      columnCount - 1,
    );
    final int trailingRow = math.min(
      ((verticalPixels + viewportDimension.height) / 200).ceil(),
      rowCount - 1,
    );

    double xLayoutOffset = (leadingColumn * 200) - horizontalOffset.pixels;
    for (int column = leadingColumn; column <= trailingColumn; column++) {
      double yLayoutOffset = (leadingRow * 200) - verticalOffset.pixels;
      for (int row = leadingRow; row <= trailingRow; row++) {
        final ChildVicinity vicinity = ChildVicinity(xIndex: column, yIndex: row);
        final RenderBox child = buildOrObtainChildFor(vicinity)!;
        child.layout(constraints.tighten(width: 200.0, height: 200.0));

        parentDataOf(child).layoutOffset = Offset(xLayoutOffset, yLayoutOffset);
        yLayoutOffset += 200;
      }
      xLayoutOffset += 200;
    }

    verticalOffset.applyContentDimensions(
      0.0,
      math.max(200 * rowCount - viewportDimension.height, 0.0),
    );
    horizontalOffset.applyContentDimensions(
      0,
      math.max(200 * columnCount - viewportDimension.width, 0.0),
    );
  }
}

class KeepAliveCheckBox extends StatefulWidget {
  const KeepAliveCheckBox({ super.key });

  @override
  KeepAliveCheckBoxState createState() => KeepAliveCheckBoxState();
}

class KeepAliveCheckBoxState extends State<KeepAliveCheckBox> with AutomaticKeepAliveClientMixin {
  bool checkValue = false;

  @override
  bool get wantKeepAlive => _wantKeepAlive;
  bool _wantKeepAlive = false;
  set wantKeepAlive(bool value) {
    if (_wantKeepAlive != value) {
      _wantKeepAlive = value;
      updateKeepAlive();
    }
  }

  @override
  Widget build(BuildContext context) {
    super.build(context);
    return Checkbox(
      value: checkValue,
      onChanged: (bool? value) {
        if (checkValue != value) {
          setState(() {
            checkValue = value!;
            wantKeepAlive = value;
          });
        }
      },
    );
  }
}

// TwoDimensionalViewportParentData already mixes in KeepAliveParentDataMixin,
// and so should be compatible with both the KeepAlive and
// TestParentDataWidget ParentDataWidgets.
// This ParentData is set up above as part of the
// RenderSimpleBuilderTableViewport for testing.
class TestExtendedParentData extends TwoDimensionalViewportParentData {
  int? testValue;
}

class TestParentDataWidget extends ParentDataWidget<TestExtendedParentData> {
  const TestParentDataWidget({
    super.key,
    required super.child,
    this.testValue,
  });

  final int? testValue;

  @override
  void applyParentData(RenderObject renderObject) {
    assert(renderObject.parentData is TestExtendedParentData);
    final TestExtendedParentData parentData = renderObject.parentData! as TestExtendedParentData;
    parentData.testValue = testValue;
  }

  @override
  Type get debugTypicalAncestorWidgetClass => SimpleBuilderTableViewport;
}
