// 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:async';
import 'dart:math' as math;

import 'package:newton/newton.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';

import 'colors.dart';
import 'debug.dart';
import 'icon.dart';
import 'icons.dart';
import 'icon_theme.dart';
import 'icon_theme_data.dart';
import 'ink_well.dart';
import 'material.dart';
import 'theme.dart';

typedef void TabSelectedIndexChanged(int selectedIndex);
typedef void TabLayoutChanged(Size size, List<double> widths);

// See https://www.google.com/design/spec/components/tabs.html#tabs-specs
const double _kTabHeight = 46.0;
const double _kTextAndIconTabHeight = 72.0;
const double _kTabIndicatorHeight = 2.0;
const double _kMinTabWidth = 72.0;
const double _kMaxTabWidth = 264.0;
const EdgeDims _kTabLabelPadding = const EdgeDims.symmetric(horizontal: 12.0);
const double _kTabBarScrollDrag = 0.025;
const Duration _kTabBarScroll = const Duration(milliseconds: 300);

// The scrollOffset (velocity) provided to fling() is pixels/ms, and the
// tolerance velocity is pixels/sec.
final double _kMinFlingVelocity = kPixelScrollTolerance.velocity / 2000.0;

class _TabBarParentData extends ContainerBoxParentDataMixin<RenderBox> { }

class _RenderTabBar extends RenderBox with
    ContainerRenderObjectMixin<RenderBox, _TabBarParentData>,
    RenderBoxContainerDefaultsMixin<RenderBox, _TabBarParentData> {

  _RenderTabBar(this.onLayoutChanged);

  int _selectedIndex;
  int get selectedIndex => _selectedIndex;
  void set selectedIndex(int value) {
    if (_selectedIndex != value) {
      _selectedIndex = value;
      markNeedsPaint();
    }
  }

  Color _indicatorColor;
  Color get indicatorColor => _indicatorColor;
  void set indicatorColor(Color value) {
    if (_indicatorColor != value) {
      _indicatorColor = value;
      markNeedsPaint();
    }
  }

  Rect _indicatorRect;
  Rect get indicatorRect => _indicatorRect;
  void set indicatorRect(Rect value) {
    if (_indicatorRect != value) {
      _indicatorRect = value;
      markNeedsPaint();
    }
  }

  bool _textAndIcons;
  bool get textAndIcons => _textAndIcons;
  void set textAndIcons(bool value) {
    if (_textAndIcons != value) {
      _textAndIcons = value;
      markNeedsLayout();
    }
  }

  bool _isScrollable;
  bool get isScrollable => _isScrollable;
  void set isScrollable(bool value) {
    if (_isScrollable != value) {
      _isScrollable = value;
      markNeedsLayout();
    }
  }

  void setupParentData(RenderBox child) {
    if (child.parentData is! _TabBarParentData)
      child.parentData = new _TabBarParentData();
  }

  double getMinIntrinsicWidth(BoxConstraints constraints) {
    BoxConstraints widthConstraints =
        new BoxConstraints(maxWidth: constraints.maxWidth, maxHeight: constraints.maxHeight);

    double maxWidth = 0.0;
    RenderBox child = firstChild;
    while (child != null) {
      maxWidth = math.max(maxWidth, child.getMinIntrinsicWidth(widthConstraints));
      final _TabBarParentData childParentData = child.parentData;
      child = childParentData.nextSibling;
    }
    double width = isScrollable ? maxWidth : maxWidth * childCount;
    return constraints.constrainWidth(width);
  }

  double getMaxIntrinsicWidth(BoxConstraints constraints) {
    BoxConstraints widthConstraints =
        new BoxConstraints(maxWidth: constraints.maxWidth, maxHeight: constraints.maxHeight);

    double maxWidth = 0.0;
    RenderBox child = firstChild;
    while (child != null) {
      maxWidth = math.max(maxWidth, child.getMaxIntrinsicWidth(widthConstraints));
      final _TabBarParentData childParentData = child.parentData;
      child = childParentData.nextSibling;
    }
    double width = isScrollable ? maxWidth : maxWidth * childCount;
    return constraints.constrainWidth(width);
  }

  double get _tabHeight => textAndIcons ? _kTextAndIconTabHeight : _kTabHeight;
  double get _tabBarHeight => _tabHeight + _kTabIndicatorHeight;

  double _getIntrinsicHeight(BoxConstraints constraints) => constraints.constrainHeight(_tabBarHeight);

  double getMinIntrinsicHeight(BoxConstraints constraints) => _getIntrinsicHeight(constraints);

  double getMaxIntrinsicHeight(BoxConstraints constraints) => _getIntrinsicHeight(constraints);

  void layoutFixedWidthTabs() {
    double tabWidth = size.width / childCount;
    BoxConstraints tabConstraints =
      new BoxConstraints.tightFor(width: tabWidth, height: _tabHeight);
    double x = 0.0;
    RenderBox child = firstChild;
    while (child != null) {
      child.layout(tabConstraints);
      final _TabBarParentData childParentData = child.parentData;
      childParentData.offset = new Offset(x, 0.0);
      x += tabWidth;
      child = childParentData.nextSibling;
    }
  }

  double layoutScrollableTabs() {
    BoxConstraints tabConstraints = new BoxConstraints(
      minWidth: _kMinTabWidth,
      maxWidth: _kMaxTabWidth,
      minHeight: _tabHeight,
      maxHeight: _tabHeight
    );
    double x = 0.0;
    RenderBox child = firstChild;
    while (child != null) {
      child.layout(tabConstraints, parentUsesSize: true);
      final _TabBarParentData childParentData = child.parentData;
      childParentData.offset = new Offset(x, 0.0);
      x += child.size.width;
      child = childParentData.nextSibling;
    }
    return x;
  }

  Size layoutSize;
  List<double> layoutWidths;
  TabLayoutChanged onLayoutChanged;

  void reportLayoutChangedIfNeeded() {
    assert(onLayoutChanged != null);
    List<double> widths = new List<double>(childCount);
    if (!isScrollable && childCount > 0) {
      double tabWidth = size.width / childCount;
      widths.fillRange(0, widths.length, tabWidth);
    } else if (isScrollable) {
      RenderBox child = firstChild;
      int childIndex = 0;
      while (child != null) {
        widths[childIndex++] = child.size.width;
        final _TabBarParentData childParentData = child.parentData;
        child = childParentData.nextSibling;
      }
      assert(childIndex == widths.length);
    }
    if (size != layoutSize || widths != layoutWidths) {
      layoutSize = size;
      layoutWidths = widths;
      onLayoutChanged(layoutSize, layoutWidths);
    }
  }

  void performLayout() {
    assert(constraints is BoxConstraints);
    if (childCount == 0)
      return;

    if (isScrollable) {
      double tabBarWidth = layoutScrollableTabs();
      size = constraints.constrain(new Size(tabBarWidth, _tabBarHeight));
    } else {
      size = constraints.constrain(new Size(constraints.maxWidth, _tabBarHeight));
      layoutFixedWidthTabs();
    }

    if (onLayoutChanged != null)
      reportLayoutChangedIfNeeded();
  }

  bool hitTestChildren(HitTestResult result, { Point position }) {
    return defaultHitTestChildren(result, position: position);
  }

  void _paintIndicator(Canvas canvas, RenderBox selectedTab, Offset offset) {
    if (indicatorColor == null)
      return;

    if (indicatorRect != null) {
      canvas.drawRect(indicatorRect.shift(offset), new Paint()..color = indicatorColor);
      return;
    }

    final Size size = new Size(selectedTab.size.width, _kTabIndicatorHeight);
    final _TabBarParentData selectedTabParentData = selectedTab.parentData;
    final Point point = new Point(
      selectedTabParentData.offset.dx,
      _tabBarHeight - _kTabIndicatorHeight
    );
    canvas.drawRect((point + offset) & size, new Paint()..color = indicatorColor);
  }

  void paint(PaintingContext context, Offset offset) {
    int index = 0;
    RenderBox child = firstChild;
    while (child != null) {
      final _TabBarParentData childParentData = child.parentData;
      context.paintChild(child, childParentData.offset + offset);
      if (index++ == selectedIndex)
        _paintIndicator(context.canvas, child, offset);
      child = childParentData.nextSibling;
    }
  }
}

class _TabBarWrapper extends MultiChildRenderObjectWidget {
  _TabBarWrapper({
    Key key,
    List<Widget> children,
    this.selectedIndex,
    this.indicatorColor,
    this.indicatorRect,
    this.textAndIcons,
    this.isScrollable: false,
    this.onLayoutChanged
  }) : super(key: key, children: children);

  final int selectedIndex;
  final Color indicatorColor;
  final Rect indicatorRect;
  final bool textAndIcons;
  final bool isScrollable;
  final TabLayoutChanged onLayoutChanged;

  _RenderTabBar createRenderObject(BuildContext context) {
    _RenderTabBar result = new _RenderTabBar(onLayoutChanged);
    updateRenderObject(context, result);
    return result;
  }

  void updateRenderObject(BuildContext context, _RenderTabBar renderObject) {
    renderObject
      ..selectedIndex = selectedIndex
      ..indicatorColor = indicatorColor
      ..indicatorRect = indicatorRect
      ..textAndIcons = textAndIcons
      ..isScrollable = isScrollable
      ..onLayoutChanged = onLayoutChanged;
  }
}

typedef Widget TabLabelIconBuilder(BuildContext context, Color color);

/// Each TabBar tab can display either a title [text], an icon, or both. An icon
/// can be specified by either the icon or iconBuilder parameters. In either case
/// the icon will occupy a 24x24 box above the title text. If iconBuilder is
/// specified its color parameter is the color that an ordinary icon would have
/// been drawn with. The color reflects that tab's selection state.
class TabLabel {
  const TabLabel({ this.text, this.icon, this.iconBuilder });

  final String text;
  final IconData icon;
  final TabLabelIconBuilder iconBuilder;
}

class _Tab extends StatelessComponent {
  _Tab({
    Key key,
    this.onSelected,
    this.label,
    this.color
  }) : super(key: key) {
    assert(label.text != null || label.icon != null || label.iconBuilder != null);
  }

  final VoidCallback onSelected;
  final TabLabel label;
  final Color color;

  Widget _buildLabelText() {
    assert(label.text != null);
    TextStyle style = new TextStyle(color: color);
    return new Text(label.text, style: style);
  }

  Widget _buildLabelIcon(BuildContext context) {
    assert(label.icon != null || label.iconBuilder != null);
    if (label.icon != null) {
      return new Icon(icon: label.icon, color: color);
    } else {
      return new SizedBox(
        width: 24.0,
        height: 24.0,
        child: label.iconBuilder(context, color)
      );
    }
  }

  Widget build(BuildContext context) {
    assert(debugCheckHasMaterial(context));
    Widget labelContent;
    if (label.icon == null && label.iconBuilder == null) {
      labelContent = _buildLabelText();
    } else if (label.text == null) {
      labelContent = _buildLabelIcon(context);
    } else {
      labelContent = new Column(
        children: <Widget>[
          new Container(
            child: _buildLabelIcon(context),
            margin: const EdgeDims.only(bottom: 10.0)
          ),
          _buildLabelText()
        ],
        justifyContent: FlexJustifyContent.center,
        alignItems: FlexAlignItems.center
      );
    }

    Container centeredLabel = new Container(
      child: new Center(child: labelContent, widthFactor: 1.0, heightFactor: 1.0),
      constraints: new BoxConstraints(minWidth: _kMinTabWidth),
      padding: _kTabLabelPadding
    );

    return new InkWell(
      onTap: onSelected,
      child: centeredLabel
    );
  }

  void debugFillDescription(List<String> description) {
    super.debugFillDescription(description);
    description.add('$label');
  }
}

class _TabsScrollBehavior extends BoundedBehavior {
  _TabsScrollBehavior();

  bool isScrollable = true;

  Simulation createFlingScrollSimulation(double position, double velocity) {
    if (!isScrollable)
      return null;

    double velocityPerSecond = velocity * 1000.0;
    return new BoundedFrictionSimulation(
      _kTabBarScrollDrag, position, velocityPerSecond, minScrollOffset, maxScrollOffset
    );
  }

  double applyCurve(double scrollOffset, double scrollDelta) {
    return (isScrollable) ? super.applyCurve(scrollOffset, scrollDelta) : 0.0;
  }
}

abstract class TabBarSelectionAnimationListener {
  void handleStatusChange(AnimationStatus status);
  void handleProgressChange();
  void handleSelectionDeactivate();
}

class TabBarSelection<T> extends StatefulComponent {
  TabBarSelection({
    Key key,
    this.value,
    this.values,
    this.onChanged,
    this.child
  }) : super(key: key)  {
    assert(values != null && values.length > 0);
    assert(new Set<T>.from(values).length == values.length);
    assert(value == null ? true : values.where((T e) => e == value).length == 1);
    assert(child != null);
  }

  final T value;
  List<T> values;
  final ValueChanged<T> onChanged;
  final Widget child;

  TabBarSelectionState<T> createState() => new TabBarSelectionState<T>();

  static TabBarSelectionState of(BuildContext context) {
    return context.ancestorStateOfType(const TypeMatcher<TabBarSelectionState>());
  }
}

class TabBarSelectionState<T> extends State<TabBarSelection<T>> {

  Animation<double> get animation => _controller.view;
  // Both the TabBar and TabBarView classes access _controller because they
  // alternately drive selection progress between tabs.
  final AnimationController _controller = new AnimationController(duration: _kTabBarScroll, value: 1.0);
  final Map<T, int> _valueToIndex = new Map<T, int>();

  void _initValueToIndex() {
    _valueToIndex.clear();
    int index = 0;
    for(T value in values)
      _valueToIndex[value] = index++;
  }

  void initState() {
    super.initState();
    _value = config.value ?? PageStorage.of(context)?.readState(context) ?? values.first;

    // If the selection's values have changed since the selected value was saved with
    // PageStorage.writeState() then use the default.
    if (!values.contains(_value))
      _value = values.first;

    _previousValue = _value;
    _initValueToIndex();
  }

  void didUpdateConfig(TabBarSelection oldConfig) {
    super.didUpdateConfig(oldConfig);
    if (values != oldConfig.values)
      _initValueToIndex();
  }

  void _writeValue() {
    PageStorage.of(context)?.writeState(context, _value);
  }

  List<T> get values => config.values;

  T get previousValue => _previousValue;
  T _previousValue;

  bool _valueIsChanging = false;
  bool get valueIsChanging => _valueIsChanging;

  int indexOf(T tabValue) => _valueToIndex[tabValue];
  int get index => _valueToIndex[value];
  int get previousIndex => indexOf(_previousValue);

  T get value => _value;
  T _value;
  void set value(T newValue) {
    if (newValue == _value)
      return;
    if (!_valueIsChanging)
      _previousValue = _value;
    _value = newValue;
    _writeValue();
    _valueIsChanging = true;

    // If the selected value change was triggered by a drag gesture, the current
    // value of _controller.value will reflect where the gesture ended. While
    // the drag was underway progress indicates where the indicator and TabBarView
    // scrollPosition are vis the indices of the two tabs adjacent to the selected
    // one. So 0.5 means the drag didn't move at all, 0.0 means the drag extended
    // to the beginning of the tab on the left and 1.0 likewise for the tab on the
    // right. That is unless the index of the selected value was 0 or values.length - 1.
    // In those cases progress just moves between the selected tab and the adjacent
    // one. Convert progress to reflect the fact that we're now moving between (just)
    // the previous and current selection index.

    double value;
    if (_controller.status == AnimationStatus.completed)
      value = 0.0;
    else if (_previousValue == values.first)
      value = _controller.value;
    else if (_previousValue == values.last)
      value = 1.0 - _controller.value;
    else if (previousIndex < index)
      value = (_controller.value - 0.5) * 2.0;
    else
      value = 1.0 - _controller.value * 2.0;

    _controller
      ..value = value
      ..forward().then((_) {
        if (_controller.value == 1.0) {
          if (config.onChanged != null)
            config.onChanged(_value);
          _valueIsChanging = false;
        }
      });
  }

  final List<TabBarSelectionAnimationListener> _animationListeners = <TabBarSelectionAnimationListener>[];

  void registerAnimationListener(TabBarSelectionAnimationListener listener) {
    _animationListeners.add(listener);
    _controller
      ..addStatusListener(listener.handleStatusChange)
      ..addListener(listener.handleProgressChange);
  }

  void unregisterAnimationListener(TabBarSelectionAnimationListener listener) {
    _animationListeners.remove(listener);
    _controller
      ..removeStatusListener(listener.handleStatusChange)
      ..removeListener(listener.handleProgressChange);
  }

  void deactivate() {
    _controller.stop();
    for (TabBarSelectionAnimationListener listener in _animationListeners.toList()) {
      listener.handleSelectionDeactivate();
      unregisterAnimationListener(listener);
    }
    assert(_animationListeners.isEmpty);
    _writeValue();
  }

  Widget build(BuildContext context) {
    return config.child;
  }
}

/// Displays a horizontal row of tabs, one per label. If isScrollable is
/// true then each tab is as wide as needed for its label and the entire
/// [TabBar] is scrollable. Otherwise each tab gets an equal share of the
/// available space. A [TabBarSelection] widget ancestor must have been
/// built to enable saving and monitoring the selected tab.
///
/// Tabs must always have an ancestor Material object.
class TabBar<T> extends Scrollable {
  TabBar({
    Key key,
    this.labels,
    this.isScrollable: false
  }) : super(key: key, scrollDirection: Axis.horizontal);

  final Map<T, TabLabel> labels;
  final bool isScrollable;

  _TabBarState createState() => new _TabBarState();
}

class _TabBarState<T> extends ScrollableState<TabBar<T>> implements TabBarSelectionAnimationListener {

  TabBarSelectionState _selection;
  bool _valueIsChanging = false;

  void _initSelection(TabBarSelectionState<T> selection) {
    _selection?.unregisterAnimationListener(this);
    _selection = selection;
    _selection?.registerAnimationListener(this);
  }

  void initState() {
    super.initState();
    scrollBehavior.isScrollable = config.isScrollable;
    _initSelection(TabBarSelection.of(context));
  }

  void didUpdateConfig(TabBar oldConfig) {
    super.didUpdateConfig(oldConfig);
    if (!config.isScrollable)
      scrollTo(0.0);
  }

  void dispose() {
    _selection?.unregisterAnimationListener(this);
    super.dispose();
  }

  void handleSelectionDeactivate() {
    _selection = null;
  }

  void handleStatusChange(AnimationStatus status) {
    if (config.labels.length == 0)
      return;

    if (_valueIsChanging && status == AnimationStatus.completed) {
      _valueIsChanging = false;
      _indicatorTween
        ..begin = _tabIndicatorRect(math.max(0, _selection.index - 1))
        ..end = _tabIndicatorRect(math.min(config.labels.length - 1, _selection.index + 1));
      setState(() {
        _indicatorRect = _tabIndicatorRect(_selection.index);
      });
    }
  }

  void handleProgressChange() {
    if (config.labels.length == 0 || _selection == null)
      return;

    if (!_valueIsChanging && _selection.valueIsChanging) {
      if (config.isScrollable)
        scrollTo(_centeredTabScrollOffset(_selection.index), duration: _kTabBarScroll);
      _indicatorTween
        ..begin = _indicatorRect ?? _tabIndicatorRect(_selection.previousIndex)
        ..end = _tabIndicatorRect(_selection.index);
      _valueIsChanging = true;
    }
    Rect oldRect = _indicatorRect;
    double t = _selection.animation.value;
    if (_valueIsChanging) {
      // When _valueIsChanging is true, we're animating based on a ticker and
      // want to curve the animation. When _valueIsChanging is false, we're
      // animating based on a pointer event and want linear feedback. It's
      // possible we should move this curve into the selection animation.
      t = Curves.ease.transform(t);
    }
    // TODO(abarth): If we've never gone through handleStatusChange before, we
    // might not have set up our _indicatorTween yet.
    _indicatorRect = _indicatorTween.lerp(t);
    if (oldRect != _indicatorRect)
      setState(() { });
  }

  Size _viewportSize = Size.zero;
  Size _tabBarSize;
  List<double> _tabWidths;
  Rect _indicatorRect;
  RectTween _indicatorTween = new RectTween();

  Rect _tabRect(int tabIndex) {
    assert(_tabBarSize != null);
    assert(_tabWidths != null);
    assert(tabIndex >= 0 && tabIndex < _tabWidths.length);
    double tabLeft = 0.0;
    if (tabIndex > 0)
      tabLeft = _tabWidths.take(tabIndex).reduce((double sum, double width) => sum + width);
    final double tabTop = 0.0;
    final double tabBottom = _tabBarSize.height - _kTabIndicatorHeight;
    final double tabRight = tabLeft + _tabWidths[tabIndex];
    return new Rect.fromLTRB(tabLeft, tabTop, tabRight, tabBottom);
  }

  Rect _tabIndicatorRect(int tabIndex) {
    Rect r = _tabRect(tabIndex);
    return new Rect.fromLTRB(r.left, r.bottom, r.right, r.bottom + _kTabIndicatorHeight);
  }

  ScrollBehavior createScrollBehavior() => new _TabsScrollBehavior();
  _TabsScrollBehavior get scrollBehavior => super.scrollBehavior;

  double _centeredTabScrollOffset(int tabIndex) {
    double viewportWidth = scrollBehavior.containerExtent;
    Rect tabRect = _tabRect(tabIndex);
    return (tabRect.left + tabRect.width / 2.0 - viewportWidth / 2.0)
      .clamp(scrollBehavior.minScrollOffset, scrollBehavior.maxScrollOffset);
  }

  void _handleTabSelected(int tabIndex) {
    if (_selection != null && tabIndex != _selection.index)
      setState(() {
        _selection.value = _selection.values[tabIndex];
      });
  }

  Widget _toTab(TabLabel label, int tabIndex, Color color, Color selectedColor) {
    Color labelColor = color;
    if (_selection != null) {
      final bool isSelectedTab = tabIndex == _selection.index;
      final bool isPreviouslySelectedTab = tabIndex == _selection.previousIndex;
      labelColor = isSelectedTab ? selectedColor : color;
      if (_selection.valueIsChanging) {
        if (isSelectedTab)
          labelColor = Color.lerp(color, selectedColor, _selection.animation.value);
        else if (isPreviouslySelectedTab)
          labelColor = Color.lerp(selectedColor, color, _selection.animation.value);
      }
    }
    return new _Tab(
      onSelected: () { _handleTabSelected(tabIndex); },
      label: label,
      color: labelColor
    );
  }

  void _updateScrollBehavior() {
    scrollTo(scrollBehavior.updateExtents(
      containerExtent: config.scrollDirection == Axis.vertical ? _viewportSize.height : _viewportSize.width,
      contentExtent: _tabWidths.reduce((double sum, double width) => sum + width),
      scrollOffset: scrollOffset
    ));
  }

  void _layoutChanged(Size tabBarSize, List<double> tabWidths) {
    setState(() {
      _tabBarSize = tabBarSize;
      _tabWidths = tabWidths;
      _updateScrollBehavior();
    });
  }

  Offset _handlePaintOffsetUpdateNeeded(ViewportDimensions dimensions) {
    // We make various state changes here but don't have to do so in a
    // setState() callback because we are called during layout and all
    // we're updating is the new offset, which we are providing to the
    // render object via our return value.
    _viewportSize = dimensions.containerSize;
    _updateScrollBehavior();
    if (config.isScrollable)
      scrollTo(_centeredTabScrollOffset(_selection.index), duration: _kTabBarScroll);
    return scrollOffsetToPixelDelta(scrollOffset);
  }

  Widget buildContent(BuildContext context) {
    TabBarSelectionState<T> newSelection = TabBarSelection.of(context);
    if (_selection != newSelection)
      _initSelection(newSelection);

    assert(config.labels.isNotEmpty);
    assert(Material.of(context) != null);

    ThemeData themeData = Theme.of(context);
    Color backgroundColor = Material.of(context).color;
    Color indicatorColor = themeData.indicatorColor;
    if (indicatorColor == backgroundColor) {
      // ThemeData tries to avoid this by having indicatorColor avoid being the
      // primaryColor. However, it's possible that the tab strip is on a
      // Material that isn't the primaryColor. In that case, if the indicator
      // color ends up clashing, then this overrides it. When that happens,
      // automatic transitions of the theme will likely look ugly as the
      // indicator color suddenly snaps to white at one end, but it's not clear
      // how to avoid that any further.
      indicatorColor = Colors.white;
    }

    TextStyle textStyle = themeData.primaryTextTheme.body1;
    IconThemeData iconTheme = themeData.primaryIconTheme;
    Color textColor = themeData.primaryTextTheme.body1.color.withAlpha(0xB2); // 70% alpha

    List<Widget> tabs = <Widget>[];
    bool textAndIcons = false;
    int tabIndex = 0;
    for (TabLabel label in config.labels.values) {
      tabs.add(_toTab(label, tabIndex++, textColor, indicatorColor));
      if (label.text != null && (label.icon != null || label.iconBuilder != null))
        textAndIcons = true;
    }

    Widget contents = new IconTheme(
      data: iconTheme,
      child: new DefaultTextStyle(
        style: textStyle,
        child: new _TabBarWrapper(
          children: tabs,
          selectedIndex: _selection?.index,
          indicatorColor: indicatorColor,
          indicatorRect: _indicatorRect,
          textAndIcons: textAndIcons,
          isScrollable: config.isScrollable,
          onLayoutChanged: _layoutChanged
        )
      )
    );

    if (config.isScrollable) {
      return new Viewport(
        scrollDirection: Axis.horizontal,
        paintOffset: scrollOffsetToPixelDelta(scrollOffset),
        onPaintOffsetUpdateNeeded: _handlePaintOffsetUpdateNeeded,
        child: contents
      );
    }

    return contents;
  }
}

class TabBarView extends PageableList {
  TabBarView({
    Key key,
    List<Widget> children
  }) : super(
    key: key,
    scrollDirection: Axis.horizontal,
    children: children
  ) {
    assert(children != null);
    assert(children.length > 1);
  }

  _TabBarViewState createState() => new _TabBarViewState();
}

class _TabBarViewState extends PageableListState<TabBarView> implements TabBarSelectionAnimationListener {

  TabBarSelectionState _selection;
  List<Widget> _items;

  int get _tabCount => config.children.length;

  BoundedBehavior _boundedBehavior;

  ExtentScrollBehavior get scrollBehavior {
    _boundedBehavior ??= new BoundedBehavior();
    return _boundedBehavior;
  }

  void _initSelection(TabBarSelectionState selection) {
    _selection = selection;
    if (_selection != null) {
      _selection.registerAnimationListener(this);
      _updateItemsAndScrollBehavior();
    }
  }

  void initState() {
    super.initState();
    _initSelection(TabBarSelection.of(context));
  }

  void didUpdateConfig(TabBarView oldConfig) {
    super.didUpdateConfig(oldConfig);
    if (_selection != null && config.children != oldConfig.children)
      _updateItemsForSelectedIndex(_selection.index);
  }

  void dispose() {
    _selection?.unregisterAnimationListener(this);
    super.dispose();
  }

  void handleSelectionDeactivate() {
    _selection = null;
  }

  void _updateItemsFromChildren(int first, int second, [int third]) {
    List<Widget> widgets = config.children;
    _items = <Widget>[widgets[first], widgets[second]];
    if (third != null)
      _items.add(widgets[third]);
  }

  void _updateItemsForSelectedIndex(int selectedIndex) {
    if (selectedIndex == 0) {
      _updateItemsFromChildren(0, 1);
    } else if (selectedIndex == _tabCount - 1) {
      _updateItemsFromChildren(selectedIndex - 1, selectedIndex);
    } else {
      _updateItemsFromChildren(selectedIndex - 1, selectedIndex, selectedIndex + 1);
    }
  }

  void _updateScrollBehaviorForSelectedIndex(int selectedIndex) {
    if (selectedIndex == 0) {
      scrollTo(scrollBehavior.updateExtents(contentExtent: 2.0, containerExtent: 1.0, scrollOffset: 0.0));
    } else if (selectedIndex == _tabCount - 1) {
      scrollTo(scrollBehavior.updateExtents(contentExtent: 2.0, containerExtent: 1.0, scrollOffset: 1.0));
    } else {
      scrollTo(scrollBehavior.updateExtents(contentExtent: 3.0, containerExtent: 1.0, scrollOffset: 1.0));
    }
  }

  void _updateItemsAndScrollBehavior() {
    assert(_selection != null);
    final int selectedIndex = _selection.index;
    assert(selectedIndex != null);
    _updateItemsForSelectedIndex(selectedIndex);
    _updateScrollBehaviorForSelectedIndex(selectedIndex);
  }

  void handleStatusChange(AnimationStatus status) {
  }

  void handleProgressChange() {
    if (_selection == null || !_selection.valueIsChanging)
      return;
    // The TabBar is driving the TabBarSelection animation.

    final Animation<double> animation = _selection.animation;

    if (animation.status == AnimationStatus.completed) {
      _updateItemsAndScrollBehavior();
      return;
    }

    if (animation.status != AnimationStatus.forward)
      return;

    final int selectedIndex = _selection.index;
    final int previousSelectedIndex = _selection.previousIndex;

    if (selectedIndex < previousSelectedIndex) {
      _updateItemsFromChildren(selectedIndex, previousSelectedIndex);
      scrollTo(1.0 - animation.value);
    } else {
      _updateItemsFromChildren(previousSelectedIndex, selectedIndex);
      scrollTo(animation.value);
    }
  }

  void dispatchOnScroll() {
    if (_selection == null || _selection.valueIsChanging)
      return;
    // This class is driving the TabBarSelection's animation.

    final AnimationController controller = _selection._controller;

    if (_selection.index == 0 || _selection.index == _tabCount - 1)
      controller.value = scrollOffset;
    else
      controller.value = scrollOffset / 2.0;
  }

  Future fling(double scrollVelocity) {
    if (_selection == null || _selection.valueIsChanging)
      return new Future.value();

    if (scrollVelocity.abs() > _kMinFlingVelocity) {
      final int selectionDelta = scrollVelocity.sign.truncate();
      final int targetIndex = (_selection.index + selectionDelta).clamp(0, _tabCount - 1);
      _selection.value = _selection.values[targetIndex];
      return new Future.value();
    }

    final int selectionIndex = _selection.index;
    final int settleIndex = snapScrollOffset(scrollOffset).toInt();
    if (selectionIndex > 0 && settleIndex != 1) {
      final int targetIndex = (selectionIndex + (settleIndex == 2 ? 1 : -1)).clamp(0, _tabCount - 1);
      _selection.value = _selection.values[targetIndex];
      return new Future.value();
    } else if (selectionIndex == 0 && settleIndex == 1) {
      _selection.value = _selection.values[1];
      return new Future.value();
    }
    return settleScrollOffset();
  }

  Widget buildContent(BuildContext context) {
    TabBarSelectionState newSelection = TabBarSelection.of(context);
    if (_selection != newSelection)
      _initSelection(newSelection);
    return new PageViewport(
      itemsWrap: config.itemsWrap,
      scrollDirection: config.scrollDirection,
      startOffset: scrollOffset,
      overlayPainter: config.scrollableListPainter,
      children: _items
    );
  }
}

class TabPageSelector<T> extends StatelessComponent {
  const TabPageSelector({ Key key }) : super(key: key);

  Widget _buildTabIndicator(TabBarSelectionState<T> selection, T tab, Animation animation, ColorTween selectedColor, ColorTween previousColor) {
    Color background;
    if (selection.valueIsChanging) {
      // The selection's animation is animating from previousValue to value.
      if (selection.value == tab)
        background = selectedColor.evaluate(animation);
      else if (selection.previousValue == tab)
        background = previousColor.evaluate(animation);
      else
        background = selectedColor.begin;
    } else {
      background = selection.value == tab ? selectedColor.end : selectedColor.begin;
    }
    return new Container(
      width: 12.0,
      height: 12.0,
      margin: new EdgeDims.all(4.0),
      decoration: new BoxDecoration(
        backgroundColor: background,
        border: new Border.all(color: selectedColor.end),
        shape: BoxShape.circle
      )
    );
  }

  Widget build(BuildContext context) {
    final TabBarSelectionState selection = TabBarSelection.of(context);
    final Color color = Theme.of(context).accentColor;
    final ColorTween selectedColor = new ColorTween(begin: Colors.transparent, end: color);
    final ColorTween previousColor = new ColorTween(begin: color, end: Colors.transparent);
    Animation<double> animation = new CurvedAnimation(parent: selection.animation, curve: Curves.ease);
    return new AnimatedBuilder(
      animation: animation,
      builder: (BuildContext context, Widget child) {
        return new Semantics(
          label: 'Page ${selection.index + 1} of ${selection.values.length}',
          child: new Row(
            children: selection.values.map((T tab) => _buildTabIndicator(selection, tab, animation, selectedColor, previousColor)).toList(),
            justifyContent: FlexJustifyContent.collapse
          )
        );
      }
    );
  }
}