import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'dart:math' as math;

class Grid extends MultiChildRenderObjectWidget {
  final double mainAxisSpacing;

  final double crossAxisSpacing;

  final double? mainAxisExtent;

  final int crossAxisCount;

  final AxisDirection axisDirection;

  final TextDirection textDirection;

  const Grid({
    super.key,
    this.mainAxisSpacing = 0,
    this.crossAxisSpacing = 0,
    int? crossAxisCount,
    AxisDirection? axisDirection,
    TextDirection? textDirection,
    this.mainAxisExtent,
    List<Widget>? children,
  })  : crossAxisCount = crossAxisCount ?? 1,
        axisDirection = axisDirection ?? AxisDirection.down,
        textDirection = textDirection ?? TextDirection.ltr,
        super(children: children ?? const []);

  const Grid.baseGap({
    Key? key,
    double mainAxisSpacing = 8.0,
    double crossAxisSpacing = 8.0,
    int? crossAxisCount,
    AxisDirection? axisDirection,
    TextDirection? textDirection,
    double? mainAxisExtent,
    List<Widget>? children,
  }) : this(
          key: key,
          mainAxisSpacing: mainAxisSpacing,
          crossAxisSpacing: crossAxisSpacing,
          crossAxisCount: crossAxisCount,
          axisDirection: axisDirection,
          textDirection: textDirection,
          mainAxisExtent: mainAxisExtent,
          children: children,
        );

  @override
  RenderObject createRenderObject(BuildContext context) {
    return RenderGrid(
      textDirection: textDirection,
      crossAxisCount: crossAxisCount,
      mainAxisSpacing: mainAxisSpacing,
      crossAxisSpacing: crossAxisSpacing,
      axisDirection: axisDirection,
      mainAxisExtent: mainAxisExtent,
    );
  }

  @override
  void updateRenderObject(
    BuildContext context,
    RenderGrid renderObject,
  ) {
    renderObject
      ..mainAxisSpacing = mainAxisSpacing
      ..mainAxisExtent = mainAxisExtent
      ..crossAxisSpacing = crossAxisSpacing
      ..textDirection = textDirection
      ..axisDirection = axisDirection
      ..crossAxisCount = crossAxisCount;
  }
}

class RenderGrid extends RenderBox
    with
        ContainerRenderObjectMixin<RenderBox, GridParentData>,
        RenderBoxContainerDefaultsMixin<RenderBox, GridParentData> {
  RenderGrid({
    required double mainAxisSpacing,
    required double crossAxisSpacing,
    required int crossAxisCount,
    required AxisDirection axisDirection,
    required TextDirection textDirection,
    double? mainAxisExtent,
  })  : _crossAxisCount = crossAxisCount,
        _crossAxisSpacing = crossAxisSpacing,
        _mainAxisSpacing = mainAxisSpacing,
        _axisDirection = axisDirection,
        _textDirection = textDirection,
        _mainAxisExtent = mainAxisExtent;

  int _crossAxisCount;

  int get crossAxisCount => _crossAxisCount;

  set crossAxisCount(int value) {
    if (_crossAxisCount != value) {
      _crossAxisCount = value;
      markNeedsLayout();
    }
  }

  double? _mainAxisExtent;

  double? get mainAxisExtent => _mainAxisExtent;

  set mainAxisExtent(double? value) {
    if (_mainAxisExtent != value) {
      _mainAxisExtent = value;
      markNeedsLayout();
    }
  }

  double _mainAxisSpacing;

  double get mainAxisSpacing => _mainAxisSpacing;

  set mainAxisSpacing(double value) {
    if (_mainAxisSpacing != value) {
      _mainAxisSpacing = value;
      markNeedsLayout();
    }
  }

  double _crossAxisSpacing;

  double get crossAxisSpacing => _crossAxisSpacing;

  set crossAxisSpacing(double value) {
    if (_crossAxisSpacing != value) {
      _crossAxisSpacing = value;
      markNeedsLayout();
    }
  }

  AxisDirection _axisDirection;

  AxisDirection get axisDirection => _axisDirection;

  set axisDirection(AxisDirection value) {
    if (_axisDirection != value) {
      _axisDirection = value;
      markNeedsLayout();
    }
  }

  TextDirection _textDirection;

  TextDirection get textDirection => _textDirection;

  set textDirection(TextDirection value) {
    if (_textDirection != value) {
      _textDirection = value;
      markNeedsLayout();
    }
  }

  Axis get mainAxis => axisDirectionToAxis(_axisDirection);

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

  @override
  void performLayout() {
    final requestedSize = _computeSize(constraints: constraints);
    size = constraints.constrain(requestedSize);
    _hasOverflow = size != requestedSize;
  }

  @override
  bool hitTestChildren(BoxHitTestResult result, {required Offset position}) {
    return defaultHitTestChildren(result, position: position);
  }

  @override
  void paint(PaintingContext context, Offset offset) {
    if (_hasOverflow) {
      context.pushClipRect(
        needsCompositing,
        offset,
        Offset.zero & size,
        defaultPaint,
      );
    } else {
      defaultPaint(context, offset);
    }
  }

  GridParentData _getParentData(RenderBox child) {
    return child.parentData as GridParentData;
  }

  void _layoutChild(
    RenderBox child,
    BoxConstraints constraints, {
    bool parentUsesSize = false,
  }) {
    child.layout(constraints, parentUsesSize: parentUsesSize);
  }

  int _computeCrossAxisCellCount(
    GridParentData childParentData,
    int crossAxisCount,
  ) {
    return math.min(
      childParentData.crossAxisCellCount ?? 1,
      crossAxisCount,
    );
  }

  Size _computeSize({
    required BoxConstraints constraints,
  }) {
    final crossAxisExtent = mainAxis == Axis.vertical
        ? constraints.maxWidth
        : constraints.maxHeight;
    final stride = (crossAxisExtent + crossAxisSpacing) / crossAxisCount;
    final offsets = List.filled(crossAxisCount, 0.0);
    RenderBox? child = firstChild;
    while (child != null) {
      final childParentData = _getParentData(child);
      final crossAxisCellCount = _computeCrossAxisCellCount(
        childParentData,
        crossAxisCount,
      );
      final crossAxisExtent = stride * crossAxisCellCount - crossAxisSpacing;
      final shouldFitContent = childParentData.mainAxisCellCount == null;

      double mainAxisExtent = 0;

      if (shouldFitContent) {
        final childConstraints = mainAxis == Axis.vertical
            ? BoxConstraints.tightFor(width: crossAxisExtent)
            : BoxConstraints.tightFor(height: crossAxisExtent);
        _layoutChild(child, childConstraints, parentUsesSize: true);
        mainAxisExtent =
            mainAxis == Axis.vertical ? child.size.height : child.size.width;
      } else {
        final mainAxisCellCount = childParentData.mainAxisCellCount ?? 1;
        mainAxisExtent = (this.mainAxisExtent ?? stride) * mainAxisCellCount -
            mainAxisSpacing;
        childParentData.realMainAxisExtent = mainAxisExtent;
        final childSize = mainAxis == Axis.vertical
            ? Size(crossAxisExtent, mainAxisExtent)
            : Size(mainAxisExtent, crossAxisExtent);
        final childConstraints = BoxConstraints.tight(childSize);
        _layoutChild(child, childConstraints);
      }
      final origin = _getOrigin(offsets, crossAxisCellCount);
      final mainAxisOffset = origin.mainAxisOffset;
      final crossAxisOffset = origin.crossAxisIndex * stride;
      final offset = mainAxis == Axis.vertical
          ? Offset(crossAxisOffset, mainAxisOffset)
          : Offset(mainAxisOffset, crossAxisOffset);
      childParentData.offset = offset;

      final nextOffset = mainAxisOffset + mainAxisExtent + mainAxisSpacing;

      for (int i = 0; i < crossAxisCellCount; i++) {
        offsets[origin.crossAxisIndex + i] = nextOffset;
      }
      child = childAfter(child);
    }
    final mainAxisExtent = offsets.reduce(math.max) - mainAxisSpacing;

    if (axisDirectionIsReversed(axisDirection)) {
      child = firstChild;
      while (child != null) {
        final childParentData = _getParentData(child);
        final offset = childParentData.offset;
        final crossAxisOffset = offset.getCrossAxisOffset(mainAxis);
        final mainAxisOffset = mainAxisExtent -
            offset.getMainAxisOffset(mainAxis) -
            childParentData.realMainAxisExtent!;
        final newOffset = mainAxis == Axis.vertical
            ? Offset(crossAxisOffset, mainAxisOffset)
            : Offset(mainAxisOffset, crossAxisOffset);
        childParentData.offset = newOffset;
        child = childAfter(child);
      }
    }

    if (mainAxis == Axis.vertical && textDirection == TextDirection.rtl) {
      child = firstChild;
      while (child != null) {
        final childParentData = _getParentData(child);
        final crossAxisCellCount = crossAxisCount;
        final crossAxisCellExtent =
            stride * crossAxisCellCount - crossAxisSpacing;
        final offset = childParentData.offset;
        final crossAxisOffset =
            crossAxisExtent - offset.dx - crossAxisCellExtent;
        final mainAxisOffset = offset.dy;
        final newOffset = Offset(crossAxisOffset, mainAxisOffset);
        childParentData.offset = newOffset;
        child = childAfter(child);
      }
    }

    return mainAxis == Axis.vertical
        ? Size(crossAxisExtent, mainAxisExtent)
        : Size(mainAxisExtent, crossAxisExtent);
  }

  bool _hasOverflow = false;
}

class GridParentData extends ContainerBoxParentData<RenderBox> {
  int? crossAxisCellCount;
  num? mainAxisCellCount;
  double? realMainAxisExtent;

  @override
  String toString() =>
      'crossAxisCellCount=$crossAxisCellCount; mainAxisCellCount=$mainAxisCellCount;';
}

class GridItem extends ParentDataWidget<GridParentData> {
  final int crossAxisCellCount;
  final num? mainAxisCellCount;

  const GridItem({
    super.key,
    required super.child,
    this.mainAxisCellCount,
    this.crossAxisCellCount = 1,
  });

  @override
  void applyParentData(RenderObject renderObject) {
    final parentData = renderObject.parentData;
    if (parentData is GridParentData) {
      bool needsLayout = false;
      if (parentData.crossAxisCellCount != crossAxisCellCount) {
        parentData.crossAxisCellCount = crossAxisCellCount;
        needsLayout = true;
      }

      if (parentData.mainAxisCellCount != mainAxisCellCount) {
        parentData.mainAxisCellCount = mainAxisCellCount;
        needsLayout = true;
      }

      if (needsLayout) {
        final targetParent = renderObject.parent;
        if (targetParent is RenderGrid) {
          targetParent.markNeedsLayout();
        }
      }
    }
  }

  @override
  Type get debugTypicalAncestorWidgetClass => GridItem;
}

class _Origin {
  final int crossAxisIndex;
  final double mainAxisOffset;

  const _Origin(this.crossAxisIndex, this.mainAxisOffset);
}

_Origin _getOrigin(List<double> offsets, int crossAxisCount) {
  var length = offsets.length;
  var origin = const _Origin(0, double.infinity);
  for (int i = 0; i < length; i++) {
    final offset = offsets[i];
    if (offset.lessOrEqual(origin.mainAxisOffset)) {
      continue;
    }
    int start = 0;
    int span = 0;
    for (int j = 0;
        span < crossAxisCount &&
            j < length &&
            length - j >= crossAxisCount - span;
        j++) {
      if (offset.lessOrEqual(offsets[j])) {
        span++;
        if (span == crossAxisCount) {
          origin = _Origin(start, offset);
        }
      } else {
        start = j + 1;
        span = 0;
      }
    }
  }
  return origin;
}

extension on double {
  lessOrEqual(double value) {
    return value < this || (value - this).abs() < precisionErrorTolerance + 1;
  }
}

extension on Offset {
  double getCrossAxisOffset(Axis direction) {
    return direction == Axis.vertical ? dx : dy;
  }

  double getMainAxisOffset(Axis direction) {
    return direction == Axis.vertical ? dy : dx;
  }
}
