// Copyright 2015 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 'dart:math' as math;

import 'package:flutter/rendering.dart';

import 'framework.dart';
import 'basic.dart';

typedef List<Widget> ListBuilder(BuildContext context, int startIndex, int count);

abstract class _ViewportBase extends RenderObjectWidget {
  _ViewportBase({
    Key key,
    this.builder,
    this.itemsWrap: false,
    this.itemCount,
    this.direction: Axis.vertical,
    this.startOffset: 0.0,
    this.overlayPainter
  }) : super(key: key);

  final ListBuilder builder;
  final bool itemsWrap;
  final int itemCount;
  final Axis direction;
  final double startOffset;
  final Painter overlayPainter;

  // we don't pass constructor arguments to the RenderBlockViewport() because until
  // we know our children, the constructor arguments we could give have no effect
  RenderBlockViewport createRenderObject() => new RenderBlockViewport();

  bool isLayoutDifferentThan(_ViewportBase oldWidget) {
    // changing the builder doesn't imply the layout changed
    return itemsWrap != oldWidget.itemsWrap ||
           itemCount != oldWidget.itemCount ||
           direction != oldWidget.direction ||
           startOffset != oldWidget.startOffset;
  }
}

abstract class _ViewportBaseElement<T extends _ViewportBase> extends RenderObjectElement<T> {
  _ViewportBaseElement(T widget) : super(widget);

  List<Element> _children = const <Element>[];
  int _layoutFirstIndex;
  int _layoutItemCount;

  RenderBlockViewport get renderObject => super.renderObject;

  void visitChildren(ElementVisitor visitor) {
    if (_children == null)
      return;
    for (Element child in _children)
      visitor(child);
  }

  void mount(Element parent, dynamic newSlot) {
    super.mount(parent, newSlot);
    renderObject.callback = layout;
    renderObject.totalExtentCallback = getTotalExtent;
    renderObject.minCrossAxisExtentCallback = getMinCrossAxisExtent;
    renderObject.maxCrossAxisExtentCallback = getMaxCrossAxisExtent;
    renderObject.overlayPainter = widget.overlayPainter;
  }

  void unmount() {
    renderObject.callback = null;
    renderObject.totalExtentCallback = null;
    renderObject.minCrossAxisExtentCallback = null;
    renderObject.maxCrossAxisExtentCallback = null;
    renderObject.overlayPainter = null;
    super.unmount();
  }

  void update(T newWidget) {
    bool needLayout = newWidget.isLayoutDifferentThan(widget);
    super.update(newWidget);
    // TODO(abarth): Don't we need to update overlayPainter here?
    if (needLayout)
      renderObject.markNeedsLayout();
    else
      _updateChildren();
  }

  void reinvokeBuilders() {
    _updateChildren();
  }

  void layout(BoxConstraints constraints);

  void _updateChildren() {
    assert(_layoutFirstIndex != null);
    assert(_layoutItemCount != null);
    List<Widget> newWidgets;
    if (_layoutItemCount > 0)
      newWidgets = widget.builder(this, _layoutFirstIndex, _layoutItemCount).map((Widget widget) {
        return new RepaintBoundary(key: new ValueKey<Key>(widget.key), child: widget);
      }).toList();
    else
      newWidgets = <Widget>[];
    _children = updateChildren(_children, newWidgets);
  }

  double getTotalExtent(BoxConstraints constraints);

  double getMinCrossAxisExtent(BoxConstraints constraints) {
    return 0.0;
  }

  double getMaxCrossAxisExtent(BoxConstraints constraints) {
    if (widget.direction == Axis.vertical)
      return constraints.maxWidth;
    return constraints.maxHeight;
  }

  void insertChildRenderObject(RenderObject child, Element slot) {
    RenderObject nextSibling = slot?.renderObject;
    renderObject.add(child, before: nextSibling);
  }

  void moveChildRenderObject(RenderObject child, Element slot) {
    assert(child.parent == renderObject);
    RenderObject nextSibling = slot?.renderObject;
    renderObject.move(child, before: nextSibling);
  }

  void removeChildRenderObject(RenderObject child) {
    assert(child.parent == renderObject);
    renderObject.remove(child);
  }

}

class HomogeneousViewport extends _ViewportBase {
  HomogeneousViewport({
    Key key,
    ListBuilder builder,
    bool itemsWrap: false,
    int itemCount, // optional, but you cannot shrink-wrap this class or otherwise use its intrinsic dimensions if you don't specify it
    Axis direction: Axis.vertical,
    double startOffset: 0.0,
    Painter overlayPainter,
    this.itemExtent // required, must be non-zero
  }) : super(
    key: key,
    builder: builder,
    itemsWrap: itemsWrap,
    itemCount: itemCount,
    direction: direction,
    startOffset: startOffset,
    overlayPainter: overlayPainter
  ) {
    assert(itemExtent != null);
    assert(itemExtent > 0);
  }

  final double itemExtent;

  _HomogeneousViewportElement createElement() => new _HomogeneousViewportElement(this);

  bool isLayoutDifferentThan(HomogeneousViewport oldWidget) {
    return itemExtent != oldWidget.itemExtent || super.isLayoutDifferentThan(oldWidget);
  }
}

class _HomogeneousViewportElement extends _ViewportBaseElement<HomogeneousViewport> {
  _HomogeneousViewportElement(HomogeneousViewport widget) : super(widget);

  void layout(BoxConstraints constraints) {
    // We enter a build scope (meaning that markNeedsBuild() is forbidden)
    // because we are in the middle of layout and if we allowed people to set
    // state, they'd expect to have that state reflected immediately, which, if
    // we were to try to honour it, would potentially result in assertions
    // because you can't normally mutate the render object tree during layout.
    // (If there were a way to limit these writes to descendants of this, it'd
    // be ok because we are exempt from that assert since we are still actively
    // doing our own layout.)
    BuildableElement.lockState(() {
      double mainAxisExtent = widget.direction == Axis.vertical ? constraints.maxHeight : constraints.maxWidth;
      double offset;
      if (widget.startOffset <= 0.0 && !widget.itemsWrap) {
        _layoutFirstIndex = 0;
        offset = -widget.startOffset;
      } else {
        _layoutFirstIndex = (widget.startOffset / widget.itemExtent).floor();
        offset = -(widget.startOffset % widget.itemExtent);
      }
      if (mainAxisExtent < double.INFINITY) {
        _layoutItemCount = ((mainAxisExtent - offset) / widget.itemExtent).ceil();
        if (widget.itemCount != null && !widget.itemsWrap)
          _layoutItemCount = math.min(_layoutItemCount, widget.itemCount - _layoutFirstIndex);
      } else {
        assert(() {
          'This HomogeneousViewport has no specified number of items (meaning it has infinite items), ' +
          'and has been placed in an unconstrained environment where all items can be rendered. ' +
          'It is most likely that you have placed your HomogeneousViewport (which is an internal ' +
          'component of several scrollable widgets) inside either another scrolling box, a flexible ' +
          'box (Row, Column), or a Stack, without giving it a specific size.';
          return widget.itemCount != null;
        });
        _layoutItemCount = widget.itemCount - _layoutFirstIndex;
      }
      _layoutItemCount = math.max(0, _layoutItemCount);
      _updateChildren();
      // Update the renderObject configuration
      renderObject.direction = widget.direction;
      renderObject.itemExtent = widget.itemExtent;
      renderObject.minExtent = getTotalExtent(null);
      renderObject.startOffset = offset;
      renderObject.overlayPainter = widget.overlayPainter;
    }, building: true);
  }

  double getTotalExtent(BoxConstraints constraints) {
    // constraints is null when called by layout() above
    return widget.itemCount != null ? widget.itemCount * widget.itemExtent : double.INFINITY;
  }
}