import 'dart:math' as math;
import 'dart:ui' show lerpDouble;

import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart';

class SliverTransitionGridDelegateWithCrossAxisCount extends SliverGridDelegate {
  const SliverTransitionGridDelegateWithCrossAxisCount({
    @required this.crossAxisCount,
    this.mainAxisSpacing = 0.0,
    this.crossAxisSpacing = 0.0,
    this.childAspectRatio = 1.0,
  })  : assert(crossAxisCount != null && crossAxisCount > 0),
        assert(mainAxisSpacing != null && mainAxisSpacing >= 0),
        assert(crossAxisSpacing != null && crossAxisSpacing >= 0),
        assert(childAspectRatio != null && childAspectRatio > 0);

  /// The number of children in the cross axis.
  final double crossAxisCount;

  /// The number of logical pixels between each child along the main axis.
  final double mainAxisSpacing;

  /// The number of logical pixels between each child along the cross axis.
  final double crossAxisSpacing;

  /// The ratio of the cross-axis to the main-axis extent of each child.
  final double childAspectRatio;

  @override
  SliverGridLayout getLayout(SliverConstraints constraints) {
    final t = crossAxisCount - crossAxisCount.truncateToDouble();
    return SliverTransitionGridTileLayout(
      current: _buildSettings(constraints, crossAxisCount),
      floor: t != 0 ? _buildSettings(constraints, crossAxisCount.floorToDouble()) : null,
      ceil: t != 0 ? _buildSettings(constraints, crossAxisCount.ceilToDouble()) : null,
      t: t,
      reverseCrossAxis: axisDirectionIsReversed(constraints.crossAxisDirection),
    );
  }

  SliverTransitionGridTileLayoutSettings _buildSettings(SliverConstraints constraints, double crossAxisCount) {
    final usableCrossAxisExtent = constraints.crossAxisExtent - crossAxisSpacing * (crossAxisCount - 1);
    final childCrossAxisExtent = usableCrossAxisExtent / crossAxisCount;
    final childMainAxisExtent = childCrossAxisExtent / childAspectRatio;
    final current = SliverTransitionGridTileLayoutSettings(
      crossAxisCount: crossAxisCount,
      mainAxisStride: childMainAxisExtent + mainAxisSpacing,
      crossAxisStride: childCrossAxisExtent + crossAxisSpacing,
      childMainAxisExtent: childMainAxisExtent,
      childCrossAxisExtent: childCrossAxisExtent,
    );
    return current;
  }

  @override
  bool shouldRelayout(SliverTransitionGridDelegateWithCrossAxisCount oldDelegate) {
    return oldDelegate.crossAxisCount != crossAxisCount || oldDelegate.mainAxisSpacing != mainAxisSpacing || oldDelegate.crossAxisSpacing != crossAxisSpacing || oldDelegate.childAspectRatio != childAspectRatio;
  }
}

class SliverTransitionGridTileLayoutSettings {
  final double crossAxisCount;

  /// The number of pixels from the leading edge of one tile to the leading edge
  /// of the next tile in the main axis.
  final double mainAxisStride;

  /// The number of pixels from the leading edge of one tile to the leading edge
  /// of the next tile in the cross axis.
  final double crossAxisStride;

  /// The number of pixels from the leading edge of one tile to the trailing
  /// edge of the same tile in the main axis.
  final double childMainAxisExtent;

  /// The number of pixels from the leading edge of one tile to the trailing
  /// edge of the same tile in the cross axis.
  final double childCrossAxisExtent;

  const SliverTransitionGridTileLayoutSettings({
    @required this.crossAxisCount,
    @required this.mainAxisStride,
    @required this.crossAxisStride,
    @required this.childMainAxisExtent,
    @required this.childCrossAxisExtent,
  })  : assert(crossAxisCount != null && crossAxisCount > 0),
        assert(mainAxisStride != null && mainAxisStride >= 0),
        assert(crossAxisStride != null && crossAxisStride >= 0),
        assert(childMainAxisExtent != null && childMainAxisExtent >= 0),
        assert(childCrossAxisExtent != null && childCrossAxisExtent >= 0);
}

class SliverTransitionGridTileLayout extends SliverGridLayout {
  /// Creates a layout that uses equally sized and spaced tiles.
  ///
  /// All of the arguments must not be null and must not be negative. The
  /// `crossAxisCount` argument must be greater than zero.
  const SliverTransitionGridTileLayout({
    @required this.current,
    this.floor,
    this.ceil,
    this.t = 0,
    @required this.reverseCrossAxis,
  }) : assert(reverseCrossAxis != null);

  final SliverTransitionGridTileLayoutSettings current, floor, ceil;
  final double t;

  /// Whether the children should be placed in the opposite order of increasing
  /// coordinates in the cross axis.
  ///
  /// For example, if the cross axis is horizontal, the children are placed from
  /// left to right when [reverseCrossAxis] is false and from right to left when
  /// [reverseCrossAxis] is true.
  ///
  /// Typically set to the return value of [axisDirectionIsReversed] applied to
  /// the [SliverConstraints.crossAxisDirection].
  final bool reverseCrossAxis;

  @override
  int getMinChildIndexForScrollOffset(double scrollOffset) {
    final settings = t == 0 ? current : floor;
    final index = settings.mainAxisStride > 0.0 ? (settings.crossAxisCount * (scrollOffset ~/ settings.mainAxisStride)).floor() : 0;
    return index;
  }

  @override
  int getMaxChildIndexForScrollOffset(double scrollOffset) {
    final settings = t == 0 ? current : floor;
    if (settings.mainAxisStride > 0.0) {
      final mainAxisCount = (scrollOffset / settings.mainAxisStride).ceil();
      final index = math.max(0, settings.crossAxisCount * mainAxisCount - 1).ceil();
      return index;
    }
    return 0;
  }

  double _getScrollOffset(int index, SliverTransitionGridTileLayoutSettings settings) {
    return (index ~/ settings.crossAxisCount) * settings.mainAxisStride;
  }

  double _getCrossAxisOffset(int index, SliverTransitionGridTileLayoutSettings settings) {
    final crossAxisStart = (index % settings.crossAxisCount) * settings.crossAxisStride;
    if (reverseCrossAxis) {
      return settings.crossAxisCount * settings.crossAxisStride - crossAxisStart - settings.childCrossAxisExtent - (settings.crossAxisStride - settings.childCrossAxisExtent);
    }
    return crossAxisStart;
  }

  @override
  SliverGridGeometry getGeometryForChildIndex(int index) {
    return SliverGridGeometry(
      scrollOffset: t == 0 ? _getScrollOffset(index, current) : lerpDouble(_getScrollOffset(index, floor), _getScrollOffset(index, ceil), t),
      crossAxisOffset: t == 0 ? _getCrossAxisOffset(index, current) : lerpDouble(_getCrossAxisOffset(index, floor), _getCrossAxisOffset(index, ceil), t),
      mainAxisExtent: current.childMainAxisExtent,
      crossAxisExtent: current.childCrossAxisExtent,
    );
  }

  @override
  double computeMaxScrollOffset(int childCount) {
    assert(childCount != null);

    if (t != 0) {
      final index = childCount - 1;
      final maxScrollOffset = lerpDouble(_getScrollOffset(index, floor), _getScrollOffset(index, ceil), t) + current.mainAxisStride;
      return maxScrollOffset;
    }

    final mainAxisCount = ((childCount - 1) ~/ current.crossAxisCount) + 1;
    final mainAxisSpacing = current.mainAxisStride - current.childMainAxisExtent;
    final maxScrollOffset = current.mainAxisStride * mainAxisCount - mainAxisSpacing;
    return maxScrollOffset;
  }
}
