// Copyright 2016 The Chromium 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 'package:flutter/foundation.dart';

import 'package:vector_math/vector_math_64.dart';

import 'box.dart';
import 'object.dart';

/// A context in which a [FlowDelegate] paints.
///
/// Provides information about the current size of the container and the
/// children and a mechanism for painting children.
///
/// See also:
///
///  * [FlowDelegate]
///  * [Flow]
///  * [RenderFlow]
abstract class FlowPaintingContext {
  /// The size of the container in which the children can be painted.
  Size get size;

  /// The number of children available to paint.
  int get childCount;

  /// The size of the [i]th child.
  ///
  /// If [i] is negative or exceeds [childCount], returns null.
  Size getChildSize(int i);

  /// Paint the [i]th child using the given transform.
  ///
  /// The child will be painted in a coordinate system that concatenates the
  /// container's coordinate system with the given transform. The origin of the
  /// parent's coordinate system is the upper left corner of the parent, with
  /// x increasing rightward and y increasing downward.
  ///
  /// The container will clip the children to its bounds.
  void paintChild(int i, { Matrix4 transform, double opacity = 1.0 });
}

/// A delegate that controls the appearance of a flow layout.
///
/// Flow layouts are optimized for moving children around the screen using
/// transformation matrices. For optimal performance, construct the
/// [FlowDelegate] with an [Animation] that ticks whenever the delegate wishes
/// to change the transformation matrices for the children and avoid rebuilding
/// the [Flow] widget itself every animation frame.
///
/// See also:
///
///  * [Flow]
///  * [RenderFlow]
abstract class FlowDelegate {
  /// The flow will repaint whenever [repaint] notifies its listeners.
  const FlowDelegate({ Listenable repaint }) : _repaint = repaint;

  final Listenable _repaint;

  /// Override to control the size of the container for the children.
  ///
  /// By default, the flow will be as large as possible. If this function
  /// returns a size that does not respect the given constraints, the size will
  /// be adjusted to be as close to the returned size as possible while still
  /// respecting the constraints.
  ///
  /// If this function depends on information other than the given constraints,
  /// override [shouldRelayout] to indicate when when the container should
  /// relayout.
  Size getSize(BoxConstraints constraints) => constraints.biggest;

  /// Override to control the layout constraints given to each child.
  ///
  /// By default, the children will receive the given constraints, which are the
  /// constraints used to size the container. The children need
  /// not respect the given constraints, but they are required to respect the
  /// returned constraints. For example, the incoming constraints might require
  /// the container to have a width of exactly 100.0 and a height of exactly
  /// 100.0, but this function might give the children looser constraints that
  /// let them be larger or smaller than 100.0 by 100.0.
  ///
  /// If this function depends on information other than the given constraints,
  /// override [shouldRelayout] to indicate when when the container should
  /// relayout.
  BoxConstraints getConstraintsForChild(int i, BoxConstraints constraints) => constraints;

  /// Override to paint the children of the flow.
  ///
  /// Children can be painted in any order, but each child can be painted at
  /// most once. Although the container clips the children to its own bounds, it
  /// is more efficient to skip painting a child altogether rather than having
  /// it paint entirely outside the container's clip.
  ///
  /// To paint a child, call [FlowPaintingContext.paintChild] on the given
  /// [FlowPaintingContext] (the `context` argument). The given context is valid
  /// only within the scope of this function call and contains information (such
  /// as the size of the container) that is useful for picking transformation
  /// matrices for the children.
  ///
  /// If this function depends on information other than the given context,
  /// override [shouldRepaint] to indicate when when the container should
  /// relayout.
  void paintChildren(FlowPaintingContext context);

  /// Override this method to return true when the children need to be laid out.
  /// This should compare the fields of the current delegate and the given
  /// oldDelegate and return true if the fields are such that the layout would
  /// be different.
  bool shouldRelayout(covariant FlowDelegate oldDelegate) => false;

  /// Override this method to return true when the children need to be
  /// repainted. This should compare the fields of the current delegate and the
  /// given oldDelegate and return true if the fields are such that
  /// paintChildren would act differently.
  ///
  /// The delegate can also trigger a repaint if the delegate provides the
  /// repaint animation argument to this object's constructor and that animation
  /// ticks. Triggering a repaint using this animation-based mechanism is more
  /// efficient than rebuilding the [Flow] widget to change its delegate.
  ///
  /// The flow container might repaint even if this function returns false, for
  /// example if layout triggers painting (e.g., if [shouldRelayout] returns
  /// true).
  bool shouldRepaint(covariant FlowDelegate oldDelegate);

  /// Override this method to include additional information in the
  /// debugging data printed by [debugDumpRenderTree] and friends.
  ///
  /// By default, returns the [runtimeType] of the class.
  @override
  String toString() => '$runtimeType';
}

int _getAlphaFromOpacity(double opacity) => (opacity * 255).round();

/// Parent data for use with [RenderFlow].
///
/// The [offset] property is ignored by [RenderFlow] and is always set to
/// [Offset.zero]. Children of a [RenderFlow] are positioned using a
/// transformation matrix, which is private to the [RenderFlow]. To set the
/// matrix, use the [FlowPaintingContext.paintChild] function from an override
/// of the [FlowDelegate.paintChildren] function.
class FlowParentData extends ContainerBoxParentData<RenderBox> {
  Matrix4 _transform;
}

/// Implements the flow layout algorithm.
///
/// Flow layouts are optimized for repositioning children using transformation
/// matrices.
///
/// The flow container is sized independently from the children by the
/// [FlowDelegate.getSize] function of the delegate. The children are then sized
/// independently given the constraints from the
/// [FlowDelegate.getConstraintsForChild] function.
///
/// Rather than positioning the children during layout, the children are
/// positioned using transformation matrices during the paint phase using the
/// matrices from the [FlowDelegate.paintChildren] function. The children can be
/// repositioned efficiently by simply repainting the flow.
///
/// The most efficient way to trigger a repaint of the flow is to supply a
/// repaint argument to the constructor of the [FlowDelegate]. The flow will
/// listen to this animation and repaint whenever the animation ticks, avoiding
/// both the build and layout phases of the pipeline.
///
/// See also:
///
///  * [FlowDelegate]
///  * [RenderStack]
class RenderFlow extends RenderBox
    with ContainerRenderObjectMixin<RenderBox, FlowParentData>,
         RenderBoxContainerDefaultsMixin<RenderBox, FlowParentData>
    implements FlowPaintingContext {
  /// Creates a render object for a flow layout.
  ///
  /// For optimal performance, consider using children that return true from
  /// [isRepaintBoundary].
  RenderFlow({
    List<RenderBox> children,
    @required FlowDelegate delegate
  }) : assert(delegate != null),
       _delegate = delegate {
    addAll(children);
  }

  @override
  void setupParentData(RenderBox child) {
    final ParentData childParentData = child.parentData;
    if (childParentData is FlowParentData)
      childParentData._transform = null;
    else
      child.parentData = FlowParentData();
  }

  /// The delegate that controls the transformation matrices of the children.
  FlowDelegate get delegate => _delegate;
  FlowDelegate _delegate;
  /// When the delegate is changed to a new delegate with the same runtimeType
  /// as the old delegate, this object will call the delegate's
  /// [FlowDelegate.shouldRelayout] and [FlowDelegate.shouldRepaint] functions
  /// to determine whether the new delegate requires this object to update its
  /// layout or painting.
  set delegate(FlowDelegate newDelegate) {
    assert(newDelegate != null);
    if (_delegate == newDelegate)
      return;
    final FlowDelegate oldDelegate = _delegate;
    _delegate = newDelegate;

    if (newDelegate.runtimeType != oldDelegate.runtimeType || newDelegate.shouldRelayout(oldDelegate))
      markNeedsLayout();
    else if (newDelegate.shouldRepaint(oldDelegate))
      markNeedsPaint();

    if (attached) {
      oldDelegate._repaint?.removeListener(markNeedsPaint);
      newDelegate._repaint?.addListener(markNeedsPaint);
    }
  }

  @override
  void attach(PipelineOwner owner) {
    super.attach(owner);
    _delegate._repaint?.addListener(markNeedsPaint);
  }

  @override
  void detach() {
    _delegate._repaint?.removeListener(markNeedsPaint);
    super.detach();
  }

  Size _getSize(BoxConstraints constraints) {
    assert(constraints.debugAssertIsValid());
    return constraints.constrain(_delegate.getSize(constraints));
  }

  @override
  bool get isRepaintBoundary => true;

  // TODO(ianh): It's a bit dubious to be using the getSize function from the delegate to
  // figure out the intrinsic dimensions. We really should either not support intrinsics,
  // or we should expose intrinsic delegate callbacks and throw if they're not implemented.

  @override
  double computeMinIntrinsicWidth(double height) {
    final double width = _getSize(BoxConstraints.tightForFinite(height: height)).width;
    if (width.isFinite)
      return width;
    return 0.0;
  }

  @override
  double computeMaxIntrinsicWidth(double height) {
    final double width = _getSize(BoxConstraints.tightForFinite(height: height)).width;
    if (width.isFinite)
      return width;
    return 0.0;
  }

  @override
  double computeMinIntrinsicHeight(double width) {
    final double height = _getSize(BoxConstraints.tightForFinite(width: width)).height;
    if (height.isFinite)
      return height;
    return 0.0;
  }

  @override
  double computeMaxIntrinsicHeight(double width) {
    final double height = _getSize(BoxConstraints.tightForFinite(width: width)).height;
    if (height.isFinite)
      return height;
    return 0.0;
  }

  @override
  void performLayout() {
    size = _getSize(constraints);
    int i = 0;
    _randomAccessChildren.clear();
    RenderBox child = firstChild;
    while (child != null) {
      _randomAccessChildren.add(child);
      final BoxConstraints innerConstraints = _delegate.getConstraintsForChild(i, constraints);
      child.layout(innerConstraints, parentUsesSize: true);
      final FlowParentData childParentData = child.parentData;
      childParentData.offset = Offset.zero;
      child = childParentData.nextSibling;
      i += 1;
    }
  }

  // Updated during layout. Only valid if layout is not dirty.
  final List<RenderBox> _randomAccessChildren = <RenderBox>[];

  // Updated during paint.
  final List<int> _lastPaintOrder = <int>[];

  // Only valid during paint.
  PaintingContext _paintingContext;
  Offset _paintingOffset;

  @override
  Size getChildSize(int i) {
    if (i < 0 || i >= _randomAccessChildren.length)
      return null;
    return _randomAccessChildren[i].size;
  }

  @override
  void paintChild(int i, { Matrix4 transform, double opacity = 1.0 }) {
    transform ??= Matrix4.identity();
    final RenderBox child = _randomAccessChildren[i];
    final FlowParentData childParentData = child.parentData;
    assert(() {
      if (childParentData._transform != null) {
        throw FlutterError(
          'Cannot call paintChild twice for the same child.\n'
          'The flow delegate of type ${_delegate.runtimeType} attempted to '
          'paint child $i multiple times, which is not permitted.'
        );
      }
      return true;
    }());
    _lastPaintOrder.add(i);
    childParentData._transform = transform;

    // We return after assigning _transform so that the transparent child can
    // still be hit tested at the correct location.
    if (opacity == 0.0)
      return;

    void painter(PaintingContext context, Offset offset) {
      context.paintChild(child, offset);
    }
    if (opacity == 1.0) {
      _paintingContext.pushTransform(needsCompositing, _paintingOffset, transform, painter);
    } else {
      _paintingContext.pushOpacity(_paintingOffset, _getAlphaFromOpacity(opacity), (PaintingContext context, Offset offset) {
        context.pushTransform(needsCompositing, offset, transform, painter);
      });
    }
  }

  void _paintWithDelegate(PaintingContext context, Offset offset) {
    _lastPaintOrder.clear();
    _paintingContext = context;
    _paintingOffset = offset;
    for (RenderBox child in _randomAccessChildren) {
      final FlowParentData childParentData = child.parentData;
      childParentData._transform = null;
    }
    try {
      _delegate.paintChildren(this);
    } finally {
      _paintingContext = null;
      _paintingOffset = null;
    }
  }

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

  @override
  bool hitTestChildren(HitTestResult result, { Offset position }) {
    final List<RenderBox> children = getChildrenAsList();
    for (int i = _lastPaintOrder.length - 1; i >= 0; --i) {
      final int childIndex = _lastPaintOrder[i];
      if (childIndex >= children.length)
        continue;
      final RenderBox child = children[childIndex];
      final FlowParentData childParentData = child.parentData;
      final Matrix4 transform = childParentData._transform;
      if (transform == null)
        continue;
      final Matrix4 inverse = Matrix4.zero();
      final double determinate = inverse.copyInverse(transform);
      if (determinate == 0.0) {
        // We cannot invert the transform. That means the child doesn't appear
        // on screen and cannot be hit.
        continue;
      }
      final Offset childPosition = MatrixUtils.transformPoint(inverse, position);
      if (child.hitTest(result, position: childPosition))
        return true;
    }
    return false;
  }

  @override
  void applyPaintTransform(RenderBox child, Matrix4 transform) {
    final FlowParentData childParentData = child.parentData;
    if (childParentData._transform != null)
      transform.multiply(childParentData._transform);
    super.applyPaintTransform(child, transform);
  }
}
