// 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 'dart:sky' as sky;

import 'package:newton/newton.dart';
import 'package:sky/animation/animation_performance.dart';
import 'package:sky/animation/animated_value.dart';
import 'package:sky/animation/curves.dart';
import 'package:sky/animation/scroll_behavior.dart';
import 'package:sky/painting/text_style.dart';
import 'package:sky/rendering/box.dart';
import 'package:sky/rendering/object.dart';
import 'package:sky/theme/colors.dart' as colors;
import 'package:sky/theme/typography.dart' as typography;
import 'package:sky/widgets/basic.dart';
import 'package:sky/widgets/default_text_style.dart';
import 'package:sky/widgets/icon.dart';
import 'package:sky/widgets/ink_well.dart';
import 'package:sky/widgets/scrollable.dart';
import 'package:sky/widgets/theme.dart';
import 'package:sky/widgets/transitions.dart';
import 'package:sky/widgets/framework.dart';
import 'package:vector_math/vector_math.dart';

typedef void SelectedIndexChanged(int selectedIndex);
typedef void LayoutChanged(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 double _kRelativeMaxTabWidth = 56.0;
const EdgeDims _kTabLabelPadding = const EdgeDims.symmetric(horizontal: 12.0);
const int _kTabIconSize = 24;
const double _kTabBarScrollDrag = 0.025;
const Duration _kTabBarScroll = const Duration(milliseconds: 200);

class TabBarParentData extends BoxParentData with
    ContainerParentDataMixin<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 _backgroundColor;
  Color get backgroundColor => _backgroundColor;
  void set backgroundColor(Color value) {
    if (_backgroundColor != value) {
      _backgroundColor = 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));
      assert(child.parentData is TabBarParentData);
      child = child.parentData.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));
      assert(child.parentData is TabBarParentData);
      child = child.parentData.nextSibling;
    }
    double width = isScrollable ? maxWidth : maxWidth * childCount;
    return constraints.constrainWidth(width);
  }

  double get _tabBarHeight {
    return (textAndIcons ? _kTextAndIconTabHeight : _kTabHeight) + _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: size.height);
    double x = 0.0;
    RenderBox child = firstChild;
    while (child != null) {
      child.layout(tabConstraints);
      assert(child.parentData is TabBarParentData);
      child.parentData.position = new Point(x, 0.0);
      x += tabWidth;
      child = child.parentData.nextSibling;
    }
  }

  void layoutScrollableTabs() {
    BoxConstraints tabConstraints = new BoxConstraints(
      minWidth: _kMinTabWidth,
      maxWidth: math.min(size.width - _kRelativeMaxTabWidth, _kMaxTabWidth),
      minHeight: size.height,
      maxHeight: size.height);
    double x = 0.0;
    RenderBox child = firstChild;
    while (child != null) {
      child.layout(tabConstraints, parentUsesSize: true);
      assert(child.parentData is TabBarParentData);
      child.parentData.position = new Point(x, 0.0);
      x += child.size.width;
      child = child.parentData.nextSibling;
    }
  }

  Size layoutSize;
  List<double> layoutWidths;
  LayoutChanged 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;
        child = child.parentData.nextSibling;
      }
      assert(childIndex == widths.length);
    }
    if (size != layoutSize || widths != layoutWidths) {
      layoutSize = size;
      layoutWidths = widths;
      onLayoutChanged(layoutSize, layoutWidths);
    }
  }

  void performLayout() {
    assert(constraints is BoxConstraints);

    size = constraints.constrain(new Size(constraints.maxWidth, _tabBarHeight));
    assert(!size.isInfinite);

    if (childCount == 0)
      return;

    if (isScrollable)
      layoutScrollableTabs();
    else
      layoutFixedWidthTabs();

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

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

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

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

    var size = new Size(selectedTab.size.width, _kTabIndicatorHeight);
    var point = new Point(
      selectedTab.parentData.position.x,
      _tabBarHeight - _kTabIndicatorHeight
    );
    Rect rect = (point + offset) & size;
    canvas.drawRect(rect, new Paint()..color = indicatorColor);
  }

  void paint(PaintingCanvas canvas, Offset offset) {
    if (backgroundColor != null) {
      double width = layoutWidths != null
        ? layoutWidths.reduce((sum, width) => sum + width)
        : size.width;
      Rect rect = offset & new Size(width, size.height);
      canvas.drawRect(rect, new Paint()..color = backgroundColor);
    }
    int index = 0;
    RenderBox child = firstChild;
    while (child != null) {
      assert(child.parentData is TabBarParentData);
      canvas.paintChild(child, child.parentData.position + offset);
      if (index++ == selectedIndex)
        _paintIndicator(canvas, child, offset);
      child = child.parentData.nextSibling;
    }
  }
}

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

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

  RenderTabBar get root => super.root;
  RenderTabBar createNode() => new RenderTabBar(onLayoutChanged);

  void syncRenderObject(Widget old) {
    super.syncRenderObject(old);
    root.selectedIndex = selectedIndex;
    root.backgroundColor = backgroundColor;
    root.indicatorColor = indicatorColor;
    root.indicatorRect = indicatorRect;
    root.textAndIcons = textAndIcons;
    root.isScrollable = isScrollable;
    root.onLayoutChanged = onLayoutChanged;
  }
}

class TabLabel {
  const TabLabel({ this.text, this.icon });

  final String text;
  final String icon;
}

class Tab extends Component {
  Tab({
    Key key,
    this.label,
    this.color,
    this.selected: false,
    this.selectedColor
  }) : super(key: key) {
    assert(label.text != null || label.icon != null);
  }

  final TabLabel label;
  final Color color;
  final bool selected;
  final Color selectedColor;

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

  Widget _buildLabelIcon() {
    assert(label.icon != null);
    Color iconColor = selected ? selectedColor : color;
    sky.ColorFilter filter = new sky.ColorFilter.mode(iconColor, sky.TransferMode.srcATop);
    return new Icon(type: label.icon, size: _kTabIconSize, colorFilter: filter);
  }

  Widget build() {
    Widget labelContents;
    if (label.icon == null) {
      labelContents = _buildLabelText();
    } else if (label.text == null) {
      labelContents = _buildLabelIcon();
    } else {
      labelContents = new Flex(
        <Widget>[
          new Container(
            child: _buildLabelIcon(),
            margin: const EdgeDims.only(bottom: 10.0)
          ),
          _buildLabelText()
        ],
        justifyContent: FlexJustifyContent.center,
        alignItems: FlexAlignItems.center,
        direction: FlexDirection.vertical
      );
    }

    Container centeredLabel = new Container(
      child: new Center(child: labelContents),
      constraints: new BoxConstraints(minWidth: _kMinTabWidth),
      padding: _kTabLabelPadding
    );

    return new InkWell(child: centeredLabel);
  }
}

class _TabsScrollBehavior extends BoundedBehavior {
  _TabsScrollBehavior({ double contentsSize: 0.0, double containerSize: 0.0 })
    : super(contentsSize: contentsSize, containerSize: containerSize);

  bool isScrollable = true;

  Simulation release(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;
  }
}

class TabBar extends Scrollable {
  TabBar({
    Key key,
    this.labels,
    this.selectedIndex: 0,
    this.onChanged,
    this.isScrollable: false
  }) : super(key: key, scrollDirection: ScrollDirection.horizontal);

  Iterable<TabLabel> labels;
  int selectedIndex;
  SelectedIndexChanged onChanged;
  bool isScrollable;

  Size _tabBarSize;
  List<double> _tabWidths;
  AnimationPerformance _indicatorAnimation;

  void initState() {
    super.initState();
    _indicatorAnimation = new AnimationPerformance()
      ..duration = _kTabBarScroll
      ..variable = new AnimatedRect(null, curve: ease);
  }

  void syncFields(TabBar source) {
    super.syncFields(source);
    labels = source.labels;
    selectedIndex = source.selectedIndex;
    onChanged = source.onChanged;
    isScrollable = source.isScrollable;
    if (!isScrollable)
      scrollTo(0.0);
    scrollBehavior.isScrollable = source.isScrollable;
  }

  AnimatedRect get _indicatorRect => _indicatorAnimation.variable as AnimatedRect;

  void _startIndicatorAnimation(int fromTabIndex, int toTabIndex) {
    _indicatorRect
      ..begin = (_indicatorRect.value == null ? _tabIndicatorRect(fromTabIndex) : _indicatorRect.value)
      ..end = _tabIndicatorRect(toTabIndex);
    _indicatorAnimation
      ..progress = 0.0
      ..play();
  }

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

  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((sum, width) => sum + width);
    double tabTop = 0.0;
    double tabBottom = _tabBarSize.height -_kTabIndicatorHeight;
    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);
  }

  double _centeredTabScrollOffset(int tabIndex) {
    double viewportWidth = scrollBehavior.containerSize;
    return (_tabRect(tabIndex).left + _tabWidths[tabIndex] / 2.0 - viewportWidth / 2.0)
      .clamp(scrollBehavior.minScrollOffset, scrollBehavior.maxScrollOffset);
  }

  EventDisposition _handleTap(int tabIndex) {
    if (tabIndex != selectedIndex) {
      if (_tabWidths != null) {
        if (isScrollable)
          scrollTo(_centeredTabScrollOffset(tabIndex), duration: _kTabBarScroll);
        _startIndicatorAnimation(selectedIndex, tabIndex);
      }
      if (onChanged != null)
        onChanged(tabIndex);
      return EventDisposition.processed;
    }
    return EventDisposition.ignored;
  }

  Widget _toTab(TabLabel label, int tabIndex, Color color, Color selectedColor) {
    return new Listener(
      child: new Tab(
        label: label,
        color: color,
        selected: tabIndex == selectedIndex,
        selectedColor: selectedColor
      ),
      onGestureTap: (_) => _handleTap(tabIndex)
    );
  }

  void _layoutChanged(Size tabBarSize, List<double> tabWidths) {
    setState(() {
      _tabBarSize = tabBarSize;
      _tabWidths = tabWidths;
      scrollBehavior.containerSize = _tabBarSize.width;
      scrollBehavior.contentsSize = _tabWidths.reduce((sum, width) => sum + width);
    });
  }

  Widget buildContent() {
    assert(labels != null && labels.isNotEmpty);

    ThemeData themeData = Theme.of(this);
    Color backgroundColor = themeData.primaryColor;
    Color indicatorColor = themeData.accentColor;
    if (indicatorColor == backgroundColor) {
      indicatorColor = colors.white;
    }

    TextStyle textStyle;
    IconThemeColor iconThemeColor;
    switch (themeData.primaryColorBrightness) {
      case ThemeBrightness.light:
        textStyle = typography.black.body1;
        iconThemeColor = IconThemeColor.black;
        break;
      case ThemeBrightness.dark:
        textStyle = typography.white.body1;
        iconThemeColor = IconThemeColor.white;
        break;
    }

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

    Matrix4 transform = new Matrix4.identity();
    transform.translate(-scrollOffset, 0.0);

    return new Transform(
      transform: transform,
      child: new IconTheme(
        data: new IconThemeData(color: iconThemeColor),
        child: new DefaultTextStyle(
          style: textStyle,
          child: new BuilderTransition(
            variables: [_indicatorRect],
            direction: Direction.forward,
            performance: _indicatorAnimation,
            builder: () {
              return new TabBarWrapper(
                children: tabs,
                selectedIndex: selectedIndex,
                backgroundColor: backgroundColor,
                indicatorColor: indicatorColor,
                indicatorRect: _indicatorRect.value,
                textAndIcons: textAndIcons,
                isScrollable: isScrollable,
                onLayoutChanged: _layoutChanged
              );
            }
          )
        )
      )
    );
  }
}

class TabNavigatorView {
  TabNavigatorView({ this.label, this.builder });

  final TabLabel label;
  final Builder builder;

  Widget buildContent() {
    assert(builder != null);
    Widget content = builder();
    assert(content != null);
    return content;
  }
}

class TabNavigator extends Component {
  TabNavigator({
    Key key,
    this.views,
    this.selectedIndex: 0,
    this.onChanged,
    this.isScrollable: false
  }) : super(key: key);

  final List<TabNavigatorView> views;
  final int selectedIndex;
  final SelectedIndexChanged onChanged;
  final bool isScrollable;

  void _handleSelectedIndexChanged(int tabIndex) {
    if (onChanged != null)
      onChanged(tabIndex);
  }

  Widget build() {
    assert(views != null && views.isNotEmpty);
    assert(selectedIndex >= 0 && selectedIndex < views.length);

    TabBar tabBar = new TabBar(
      labels: views.map((view) => view.label),
      onChanged: _handleSelectedIndexChanged,
      selectedIndex: selectedIndex,
      isScrollable: isScrollable
    );

    Widget content = views[selectedIndex].buildContent();
    return new Flex([tabBar, new Flexible(child: content)],
      direction: FlexDirection.vertical
    );
  }
}
