// 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 'basic.dart';
import 'framework.dart';

import 'package:flutter/rendering.dart';

typedef void ExtentsChangedCallback(double contentExtent, double containerExtent);

abstract class VirtualViewport extends RenderObjectWidget {
  double get startOffset;
  ScrollDirection get scrollDirection;
  Iterable<Widget> get children;
}

abstract class VirtualViewportElement<T extends VirtualViewport> extends RenderObjectElement<T> {
  VirtualViewportElement(T widget) : super(widget);

  int get materializedChildBase;
  int get materializedChildCount;
  double get startOffsetBase;
  double get startOffsetLimit;

  double get paintOffset => -(widget.startOffset - startOffsetBase);

  List<Element> _materializedChildren = const <Element>[];

  RenderVirtualViewport get renderObject => super.renderObject;

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

  void mount(Element parent, dynamic newSlot) {
    super.mount(parent, newSlot);
    _iterator = null;
    _widgets = <Widget>[];
    renderObject.callback = layout;
    updateRenderObject();
  }

  void unmount() {
    renderObject.callback = null;
    super.unmount();
  }

  void update(T newWidget) {
    if (widget.children != newWidget.children) {
      _iterator = null;
      _widgets = <Widget>[];
    }
    super.update(newWidget);
    updateRenderObject();
    if (!renderObject.needsLayout)
      _materializeChildren();
  }

  void _updatePaintOffset() {
    switch (widget.scrollDirection) {
      case ScrollDirection.vertical:
        renderObject.paintOffset = new Offset(0.0, paintOffset);
        break;
      case ScrollDirection.horizontal:
        renderObject.paintOffset = new Offset(paintOffset, 0.0);
        break;
    }
  }

  void updateRenderObject() {
    renderObject.virtualChildCount = widget.children.length;

    if (startOffsetBase != null) {
      _updatePaintOffset();

      // If we don't already need layout, we need to request a layout if the
      // viewport has shifted to expose new children.
      if (!renderObject.needsLayout) {
        if (startOffsetBase != null && widget.startOffset < startOffsetBase)
          renderObject.markNeedsLayout();
        else if (startOffsetLimit != null && widget.startOffset > startOffsetLimit)
          renderObject.markNeedsLayout();
      }
    }
  }

  void layout(BoxConstraints constraints) {
    assert(startOffsetBase != null);
    assert(startOffsetLimit != null);
    _updatePaintOffset();
    BuildableElement.lockState(_materializeChildren, building: true);
  }

  Iterator<Widget> _iterator;
  List<Widget> _widgets;

  void _populateWidgets(int limit) {
    if (limit <= _widgets.length)
      return;
    if (widget.children is List<Widget>) {
      _widgets = widget.children;
      return;
    }
    _iterator ??= widget.children.iterator;
    while (_widgets.length < limit) {
      bool moved = _iterator.moveNext();
      assert(moved);
      Widget current = _iterator.current;
      assert(current != null);
      _widgets.add(current);
    }
  }

  void _materializeChildren() {
    int base = materializedChildBase;
    int count = materializedChildCount;
    int length = renderObject.virtualChildCount;
    assert(base != null);
    assert(count != null);
    _populateWidgets(base < 0 ? length : math.min(length, base + count));
    List<Widget> newWidgets = new List<Widget>(count);
    for (int i = 0; i < count; ++i) {
      int childIndex = base + i;
      Widget child = _widgets[(childIndex % length).abs()];
      Key key = new ValueKey(child.key ?? childIndex);
      newWidgets[i] = new RepaintBoundary(key: key, child: child);
    }
    _materializedChildren = updateChildren(_materializedChildren, newWidgets);
  }

  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);
  }
}