// 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.

/// @docImport 'package:flutter/widgets.dart';
library;

import 'dart:math' as math;

import 'package:flutter/foundation.dart';

import 'box.dart';
import 'layer.dart';
import 'object.dart';
import 'sliver.dart';
import 'sliver_fixed_extent_list.dart';
import 'sliver_multi_box_adaptor.dart';

/// Represents the animation of the children of a parent [TreeSliverNode] that
/// are animating into or out of view.
///
/// The `fromIndex` and `toIndex` are identify the animating children following
/// the parent, with the `value` representing the status of the current
/// animation. The value of `toIndex` is inclusive, meaning the child at that
/// index is included in the animating segment.
///
/// Provided to [RenderTreeSliver] as part of
/// [RenderTreeSliver.activeAnimations] by [TreeSliver] to properly offset
/// animating children.
typedef TreeSliverNodesAnimation = ({int fromIndex, int toIndex, double value});

/// Used to pass information down to [RenderTreeSliver].
class TreeSliverNodeParentData extends SliverMultiBoxAdaptorParentData {
  /// The depth of the node, used by [RenderTreeSliver] to offset children by
  /// by the [TreeSliverIndentationType].
  int depth = 0;
}

/// The style of indentation for [TreeSliverNode]s in a [TreeSliver], as
/// handled by [RenderTreeSliver].
///
/// {@template flutter.rendering.TreeSliverIndentationType}
/// By default, the indentation is handled by [RenderTreeSliver]. Child nodes
/// are offset by the indentation specified by
/// [TreeSliverIndentationType.value] in the cross axis of the viewport. This
/// means the space allotted to the indentation will not be part of the space
/// made available to the Widget returned by [TreeSliver.treeNodeBuilder].
///
/// Alternatively, the indentation can be implemented in
/// [TreeSliver.treeNodeBuilder], with the depth of the given tree row
/// accessed by [TreeSliverNode.depth]. This allows for more customization in
/// building tree rows, such as filling the indented area with decorations or
/// ink effects.
///
/// {@tool dartpad}
/// This example shows a highly customized [TreeSliver] configured to
/// [TreeSliverIndentationType.none]. This allows the indentation to be handled
/// by the developer in [TreeSliver.treeNodeBuilder], where a decoration is
/// used to fill the indented space.
///
/// ** See code in examples/api/lib/widgets/sliver/sliver_tree.1.dart **
/// {@end-tool}
///
/// {@endtemplate}
class TreeSliverIndentationType {
  const TreeSliverIndentationType._internal(double value) : _value = value;

  /// The number of pixels by which [TreeSliverNode]s will be offset according
  /// to their [TreeSliverNode.depth].
  double get value => _value;
  final double _value;

  /// The default indentation of child [TreeSliverNode]s in a [TreeSliver].
  ///
  /// Child nodes will be offset by 10 pixels for each level in the tree.
  static const TreeSliverIndentationType standard = TreeSliverIndentationType._internal(10.0);

  /// Configures no offsetting of child nodes in a [TreeSliver].
  ///
  /// Useful if the indentation is implemented in the
  /// [TreeSliver.treeNodeBuilder] instead for more customization options.
  ///
  /// Child nodes will not be offset in the tree.
  static const TreeSliverIndentationType none = TreeSliverIndentationType._internal(0.0);

  /// Configures a custom offset for indenting child nodes in a
  /// [TreeSliver].
  ///
  /// Child nodes will be offset by the provided number of pixels in the tree.
  /// The [value] must be a non negative number.
  static TreeSliverIndentationType custom(double value) {
    assert(value >= 0.0);
    return TreeSliverIndentationType._internal(value);
  }
}

// Used during paint to delineate animating portions of the tree.
typedef _PaintSegment = ({int leadingIndex, int trailingIndex});

/// A sliver that places multiple [TreeSliverNode]s in a linear array along the
/// main access, while staggering nodes that are animating into and out of view.
///
/// The extent of each child node is determined by the [itemExtentBuilder].
///
/// See also:
///
///   * [TreeSliver], the widget that creates and manages this render
///     object.
class RenderTreeSliver extends RenderSliverVariedExtentList {
  /// Creates the render object that lays out the [TreeSliverNode]s of a
  /// [TreeSliver].
  RenderTreeSliver({
    required super.childManager,
    required super.itemExtentBuilder,
    required Map<UniqueKey, TreeSliverNodesAnimation> activeAnimations,
    required double indentation,
  }) : _activeAnimations = activeAnimations,
       _indentation = indentation;

  // TODO(Piinks): There are some opportunities to cache even further as far as
  // extents and layout offsets when using itemExtentBuilder from the super
  // class as we do here. I want to yak shave that in a separate change.

  /// The currently active [TreeSliverNode] animations.
  ///
  /// Since the index of animating nodes can change at any time, the unique key
  /// is used to track an animation of nodes across frames.
  Map<UniqueKey, TreeSliverNodesAnimation> get activeAnimations => _activeAnimations;
  Map<UniqueKey, TreeSliverNodesAnimation> _activeAnimations;
  set activeAnimations(Map<UniqueKey, TreeSliverNodesAnimation> value) {
    if (_activeAnimations == value) {
      return;
    }
    _activeAnimations = value;
    markNeedsLayout();
  }

  /// The number of pixels by which child nodes will be offset in the cross axis
  /// based on their [TreeSliverNodeParentData.depth].
  ///
  /// If zero, can alternatively offset children in
  /// [TreeSliver.treeNodeBuilder] for more options to customize the
  /// indented space.
  double get indentation => _indentation;
  double _indentation;
  set indentation(double value) {
    if (_indentation == value) {
      return;
    }
    assert(indentation >= 0.0);
    _indentation = value;
    markNeedsLayout();
  }

  // Maps the index of parents to the animation key of their children.
  final Map<int, UniqueKey> _animationLeadingIndices = <int, UniqueKey>{};
  // Maps the key of child node animations to the fixed distance they are
  // traversing during the animation. Determined at the start of the animation.
  final Map<UniqueKey, double> _animationOffsets = <UniqueKey, double>{};
  void _updateAnimationCache() {
    _animationLeadingIndices.clear();
    _activeAnimations.forEach((UniqueKey key, TreeSliverNodesAnimation animation) {
      _animationLeadingIndices[animation.fromIndex - 1] = key;
    });
    // Remove any stored offsets or clip layers that are no longer actively
    // animating.
    _animationOffsets.removeWhere((UniqueKey key, _) => !_activeAnimations.keys.contains(key));
    _clipHandles.removeWhere((UniqueKey key, LayerHandle<ClipRectLayer> handle) {
      if (!_activeAnimations.keys.contains(key)) {
        handle.layer = null;
        return true;
      }
      return false;
    });
  }

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

  @override
  void dispose() {
    _clipHandles.removeWhere((UniqueKey key, LayerHandle<ClipRectLayer> handle) {
      handle.layer = null;
      return true;
    });
    super.dispose();
  }

  // TODO(Piinks): This should be made a public getter on the super class.
  // Multiple subclasses are making use of it now, yak shave that refactor
  // separately.
  late SliverLayoutDimensions _currentLayoutDimensions;

  @override
  void performLayout() {
    assert(
      constraints.axisDirection == AxisDirection.down,
      'TreeSliver is only supported in Viewports with an AxisDirection.down. '
      'The current axis direction is: ${constraints.axisDirection}.',
    );
    _updateAnimationCache();
    _currentLayoutDimensions = SliverLayoutDimensions(
      scrollOffset: constraints.scrollOffset,
      precedingScrollExtent: constraints.precedingScrollExtent,
      viewportMainAxisExtent: constraints.viewportMainAxisExtent,
      crossAxisExtent: constraints.crossAxisExtent,
    );
    super.performLayout();
  }

  @override
  int getMinChildIndexForScrollOffset(double scrollOffset, double itemExtent) {
    // itemExtent is deprecated in the super class, we ignore it because we use
    // the builder anyways.
    return _getChildIndexForScrollOffset(scrollOffset);
  }

  @override
  int getMaxChildIndexForScrollOffset(double scrollOffset, double itemExtent) {
    // itemExtent is deprecated in the super class, we ignore it because we use
    // the builder anyways.
    return _getChildIndexForScrollOffset(scrollOffset);
  }

  int _getChildIndexForScrollOffset(double scrollOffset) {
    if (scrollOffset == 0.0) {
      return 0;
    }
    var position = 0.0;
    var index = 0;
    var totalAnimationOffset = 0.0;
    double? itemExtent;
    final int? childCount = childManager.estimatedChildCount;
    while (position < scrollOffset) {
      if (childCount != null && index > childCount - 1) {
        break;
      }

      itemExtent = itemExtentBuilder(index, _currentLayoutDimensions);
      if (itemExtent == null) {
        break;
      }
      if (_animationLeadingIndices.keys.contains(index)) {
        final UniqueKey animationKey = _animationLeadingIndices[index]!;
        if (_animationOffsets[animationKey] == null) {
          // We have not computed the distance this block is traversing over the
          // lifetime of the animation.
          _computeAnimationOffsetFor(animationKey, position);
        }
        // We add the offset accounting for the animation value.
        totalAnimationOffset +=
            _animationOffsets[animationKey]! * (1 - _activeAnimations[animationKey]!.value);
      }
      position += itemExtent - totalAnimationOffset;
      ++index;
    }
    return index - 1;
  }

  void _computeAnimationOffsetFor(UniqueKey key, double position) {
    assert(_activeAnimations[key] != null);
    final double targetPosition = constraints.scrollOffset + constraints.remainingCacheExtent;
    var currentPosition = position;
    final int startingIndex = _activeAnimations[key]!.fromIndex;
    final int lastIndex = _activeAnimations[key]!.toIndex;
    var currentIndex = startingIndex;
    var totalAnimatingOffset = 0.0;
    // We animate only a portion of children that would be visible/in the cache
    // extent, unless all children would fit on the screen.
    while (currentIndex <= lastIndex && currentPosition < targetPosition) {
      final double itemExtent = itemExtentBuilder(currentIndex, _currentLayoutDimensions)!;
      totalAnimatingOffset += itemExtent;
      currentPosition += itemExtent;
      currentIndex++;
    }
    // For the life of this animation, which affects all children following
    // startingIndex (regardless of if they are a child of the triggering
    // parent), they will be offset by totalAnimatingOffset * the
    // animation value. This is because even though more children can be
    // scrolled into view, the same distance must be maintained for a smooth
    // animation.
    _animationOffsets[key] = totalAnimatingOffset;
  }

  @override
  double childCrossAxisPosition(covariant RenderObject child) {
    final parentData = child.parentData! as TreeSliverNodeParentData;
    return parentData.depth * indentation;
  }

  @override
  double indexToLayoutOffset(double itemExtent, int index) {
    // itemExtent is deprecated in the super class, we ignore it because we use
    // the builder anyways.
    var position = 0.0;
    var currentIndex = 0;
    var totalAnimationOffset = 0.0;
    double? itemExtent;
    final int? childCount = childManager.estimatedChildCount;
    while (currentIndex < index) {
      if (childCount != null && currentIndex > childCount - 1) {
        break;
      }

      itemExtent = itemExtentBuilder(currentIndex, _currentLayoutDimensions);
      if (itemExtent == null) {
        break;
      }
      if (_animationLeadingIndices.keys.contains(currentIndex)) {
        final UniqueKey animationKey = _animationLeadingIndices[currentIndex]!;
        assert(_animationOffsets[animationKey] != null);
        // We add the offset accounting for the animation value.
        totalAnimationOffset +=
            _animationOffsets[animationKey]! * (1 - _activeAnimations[animationKey]!.value);
      }
      position += itemExtent;
      currentIndex++;
    }
    return position - totalAnimationOffset;
  }

  final Map<UniqueKey, LayerHandle<ClipRectLayer>> _clipHandles =
      <UniqueKey, LayerHandle<ClipRectLayer>>{};

  @override
  void paint(PaintingContext context, Offset offset) {
    if (firstChild == null) {
      return;
    }

    RenderBox? nextChild = firstChild;
    void paintUpTo(int index, RenderBox? startWith, PaintingContext context, Offset offset) {
      var child = startWith;
      while (child != null && indexOf(child) <= index) {
        final double mainAxisDelta = childMainAxisPosition(child);
        final parentData = child.parentData! as TreeSliverNodeParentData;
        final Offset childOffset =
            Offset(
              parentData.depth * indentation,
              parentData.layoutOffset! - constraints.scrollOffset,
            ) +
            offset;
        // If the child's visible interval (mainAxisDelta, mainAxisDelta + paintExtentOf(child))
        // does not intersect the paint extent interval (0, constraints.remainingPaintExtent), it's hidden.
        if (mainAxisDelta < constraints.remainingPaintExtent &&
            mainAxisDelta + paintExtentOf(child) > 0) {
          context.paintChild(child, childOffset);
        }
        child = childAfter(child);
      }
      nextChild = child;
    }

    if (_animationLeadingIndices.isEmpty) {
      // There are no animations running.
      paintUpTo(indexOf(lastChild!), firstChild, context, offset);
      return;
    }

    // We are animating.
    // Separate animating segments to clip for any overlap.
    int leadingIndex = indexOf(firstChild!);
    final List<int> animationIndices = _animationLeadingIndices.keys.toList()..sort();
    final paintSegments = <_PaintSegment>[];
    while (animationIndices.isNotEmpty) {
      final int trailingIndex = animationIndices.removeAt(0);
      paintSegments.add((leadingIndex: leadingIndex, trailingIndex: trailingIndex));
      leadingIndex = trailingIndex + 1;
    }
    paintSegments.add((leadingIndex: leadingIndex, trailingIndex: indexOf(lastChild!)));

    // Paint, clipping for all but the first segment.
    paintUpTo(paintSegments.removeAt(0).trailingIndex, nextChild, context, offset);
    // Paint the rest with clip layers.
    while (paintSegments.isNotEmpty) {
      final _PaintSegment segment = paintSegments.removeAt(0);

      // Rect is calculated by the trailing edge of the parent (preceding
      // leadingIndex), and the trailing edge of the trailing index. We cannot
      // rely on the leading edge of the leading index, because it is currently
      // moving.
      final int parentIndex = math.max(segment.leadingIndex - 1, 0);
      final double leadingOffset =
          indexToLayoutOffset(0.0, parentIndex) +
          (parentIndex == 0 ? 0.0 : itemExtentBuilder(parentIndex, _currentLayoutDimensions)!);
      final double trailingOffset =
          indexToLayoutOffset(0.0, segment.trailingIndex) +
          itemExtentBuilder(segment.trailingIndex, _currentLayoutDimensions)!;
      final rect = Rect.fromPoints(
        Offset(0.0, leadingOffset),
        Offset(constraints.crossAxisExtent, trailingOffset),
      );
      // We use the same animation key to keep track of the clip layer, unless
      // this is the odd man out segment.
      final UniqueKey key = _animationLeadingIndices[parentIndex]!;
      _clipHandles[key] ??= LayerHandle<ClipRectLayer>();
      _clipHandles[key]!.layer = context.pushClipRect(needsCompositing, offset, rect, (
        PaintingContext context,
        Offset offset,
      ) {
        paintUpTo(segment.trailingIndex, nextChild, context, offset);
      }, oldLayer: _clipHandles[key]!.layer);
    }
  }
}
