// Copyright 2018 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:flutter/widgets.dart';

import 'button_theme.dart';
import 'colors.dart';
import 'raised_button.dart';
import 'theme.dart';

// The total time to make the button's fill color opaque and change
// its elevation.
const Duration _kPressDuration = Duration(milliseconds: 150);

// Half of _kPressDuration: just the time to change the button's
// elevation.
const Duration _kElevationDuration = Duration(milliseconds: 75);

/// A cross between [RaisedButton] and [FlatButton]: a bordered button whose
/// elevation increases and whose background becomes opaque when the button
/// is pressed.
///
/// An outline button's elevation is initially 0.0 and its background [color]
/// is transparent. When the button is pressed its background becomes opaque
/// and then its elevation increases to [highlightElevation].
///
/// The outline button has a border whose shape is defined by [shape]
/// and whose appearance is defined by [borderSide], [disabledBorderColor],
/// and [highlightedBorderColor].
///
/// If the [onPressed] callback is null, then the button will be disabled and by
/// default will resemble a flat button in the [disabledColor].
///
/// If you want an ink-splash effect for taps, but don't want to use a button,
/// consider using [InkWell] directly.
///
/// Outline buttons have a minimum size of 88.0 by 36.0 which can be overidden
/// with [ButtonTheme].
///
/// See also:
///
///  * [RaisedButton], a filled material design button with a shadow.
///  * [FlatButton], a material design button without a shadow.
///  * [DropdownButton], a button that shows options to select from.
///  * [FloatingActionButton], the round button in material applications.
///  * [IconButton], to create buttons that just contain icons.
///  * [InkWell], which implements the ink splash part of a flat button.
///  * <https://material.google.com/components/buttons.html>
class OutlineButton extends StatefulWidget {
  /// Create a filled button.
  ///
  /// The [highlightElevation], [borderWidth], and [clipBehavior]
  /// arguments must not be null.
  const OutlineButton({
    Key key,
    @required this.onPressed,
    this.textTheme,
    this.textColor,
    this.disabledTextColor,
    this.color,
    this.highlightColor,
    this.splashColor,
    this.highlightElevation = 2.0,
    this.borderSide,
    this.disabledBorderColor,
    this.highlightedBorderColor,
    this.padding,
    this.shape,
    this.clipBehavior = Clip.none,
    this.child,
  }) : assert(highlightElevation != null && highlightElevation >= 0.0),
       assert(clipBehavior != null),
       super(key: key);

  /// Create an outline button from a pair of widgets that serve as the button's
  /// [icon] and [label].
  ///
  /// The icon and label are arranged in a row and padded by 12 logical pixels
  /// at the start, and 16 at the end, with an 8 pixel gap in between.
  ///
  /// The [highlightElevation], [icon], [label], and [clipBehavior] must not be
  /// null.
  OutlineButton.icon({
    Key key,
    @required this.onPressed,
    this.textTheme,
    this.textColor,
    this.disabledTextColor,
    this.color,
    this.highlightColor,
    this.splashColor,
    this.highlightElevation = 2.0,
    this.borderSide,
    this.disabledBorderColor,
    this.highlightedBorderColor,
    this.shape,
    this.clipBehavior = Clip.none,
    @required Widget icon,
    @required Widget label,
  }) : assert(highlightElevation != null && highlightElevation >= 0.0),
       assert(icon != null),
       assert(label != null),
       assert(clipBehavior != null),
       padding = const EdgeInsetsDirectional.only(start: 12.0, end: 16.0),
       child = new Row(
         mainAxisSize: MainAxisSize.min,
         children: <Widget>[
           icon,
           const SizedBox(width: 8.0),
           label,
         ],
       ),
       super(key: key);

  /// Called when the button is tapped or otherwise activated.
  ///
  /// If this is set to null, the button will be disabled, see [enabled].
  final VoidCallback onPressed;

  /// Defines the button's base colors, and the defaults for the button's minimum
  /// size, internal padding, and shape.
  ///
  /// Defaults to `ButtonTheme.of(context).textTheme`.
  final ButtonTextTheme textTheme;

  /// The color to use for this button's text.
  ///
  /// The button's [Material.textStyle] will be the current theme's button
  /// text style, [ThemeData.textTheme.button], configured with this color.
  ///
  /// The default text color depends on the button theme's text theme,
  /// [ButtonThemeData.textTheme].
  ///
  /// See also:
  ///
  ///   * [disabledTextColor], the text color to use when the button has been
  ///     disabled.
  final Color textColor;

  /// The color to use for this button's text when the button is disabled.
  ///
  /// The button's [Material.textStyle] will be the current theme's button
  /// text style, [ThemeData.textTheme.button], configured with this color.
  ///
  /// The default value is the theme's disabled color,
  /// [ThemeData.disabledColor].
  ///
  /// See also:
  ///
  ///  * [textColor], which specifies the color to use for this button's text
  ///    when the button is [enabled].
  final Color disabledTextColor;

  /// The button's opaque fill color when it's [enabled] and has been pressed.
  ///
  /// If null this value defaults to white for light themes (see
  /// [ThemeData.brightness]), and black for dark themes.
  final Color color;

  /// The splash color of the button's [InkWell].
  ///
  /// The ink splash indicates that the button has been touched. It
  /// appears on top of the button's child and spreads in an expanding
  /// circle beginning where the touch occurred.
  ///
  /// If [textTheme] is [ButtonTextTheme.primary], the default splash color is
  /// is based on the theme's primary color [ThemeData.primaryColor],
  /// otherwise it's the current theme's splash color, [ThemeData.splashColor].
  ///
  /// The appearance of the splash can be configured with the theme's splash
  /// factory, [ThemeData.splashFactory].
  final Color splashColor;

  /// The highlight color of the button's [InkWell].
  ///
  /// The highlight indicates that the button is actively being pressed. It
  /// appears on top of the button's child and quickly spreads to fill
  /// the button, and then fades out.
  ///
  /// If [textTheme] is [ButtonTextTheme.primary], the default highlight color is
  /// transparent (in other words the highlight doesn't appear). Otherwise it's
  /// the current theme's highlight color, [ThemeData.highlightColor].
  final Color highlightColor;

  /// The elevation of the button when it's [enabled] and has been pressed.
  ///
  /// If null, this value defaults to 2.0.
  ///
  /// The elevation of an outline button is always 0.0 unless its enabled
  /// and has been pressed.
  final double highlightElevation;

  /// Defines the color of the border when the button is enabled but not
  /// pressed, and the border outline's width and style in general.
  ///
  /// If the border side's [BorderSide.style] is [BorderStyle.none], then
  /// an outline is not drawn.
  ///
  /// If null the default border's style is [BorderStyle.solid], its
  /// [BorderSide.width] is 2.0, and its color is a light shade of grey.
  final BorderSide borderSide;

  /// The outline border's color when the button is [enabled] and pressed.
  ///
  /// If null this value defaults to the theme's primary color,
  /// [ThemeData.primaryColor].
  final Color highlightedBorderColor;

  /// The outline border's color when the button is not [enabled].
  ///
  /// If null this value defaults to a very light shade of grey for light
  /// themes (see [ThemeData.brightness]), and a very dark shade of grey for
  /// dark themes.
  final Color disabledBorderColor;

  /// The internal padding for the button's [child].
  ///
  /// Defaults to the value from the current [ButtonTheme],
  /// [ButtonThemeData.padding].
  final EdgeInsetsGeometry padding;

  /// The shape of the button's [Material] and its outline.
  ///
  /// The button's highlight and splash are clipped to this shape. If the
  /// button has a [highlightElevation], then its drop shadow is defined by this
  /// shape as well.
  final ShapeBorder shape;

  /// {@macro flutter.widgets.Clip}
  final Clip clipBehavior;

  /// The button's label.
  ///
  /// Often a [Text] widget in all caps.
  final Widget child;

  /// Whether the button is enabled or disabled.
  ///
  /// Buttons are disabled by default. To enable a button, set its [onPressed]
  /// property to a non-null value.
  bool get enabled => onPressed != null;

  @override
  _OutlineButtonState createState() => new _OutlineButtonState();


  @override
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
    properties.add(new ObjectFlagProperty<VoidCallback>('onPressed', onPressed, ifNull: 'disabled'));
    properties.add(new DiagnosticsProperty<ButtonTextTheme>('textTheme', textTheme, defaultValue: null));
    properties.add(new DiagnosticsProperty<Color>('textColor', textColor, defaultValue: null));
    properties.add(new DiagnosticsProperty<Color>('disabledTextColor', disabledTextColor, defaultValue: null));
    properties.add(new DiagnosticsProperty<Color>('color', color, defaultValue: null));
    properties.add(new DiagnosticsProperty<Color>('highlightColor', highlightColor, defaultValue: null));
    properties.add(new DiagnosticsProperty<Color>('splashColor', splashColor, defaultValue: null));
    properties.add(new DiagnosticsProperty<double>('highlightElevation', highlightElevation, defaultValue: 2.0));
    properties.add(new DiagnosticsProperty<BorderSide>('borderSide', borderSide, defaultValue: null));
    properties.add(new DiagnosticsProperty<Color>('disabledBorderColor', disabledBorderColor, defaultValue: null));
    properties.add(new DiagnosticsProperty<Color>('highlightedBorderColor', highlightedBorderColor, defaultValue: null));
    properties.add(new DiagnosticsProperty<EdgeInsetsGeometry>('padding', padding, defaultValue: null));
    properties.add(new DiagnosticsProperty<ShapeBorder>('shape', shape, defaultValue: null));
  }
}

class _OutlineButtonState extends State<OutlineButton> with SingleTickerProviderStateMixin {
  AnimationController _controller;
  Animation<double> _fillAnimation;
  Animation<double> _elevationAnimation;
  bool _pressed = false;

  @override
  void initState() {
    super.initState();

    // The Material widget animates its shape (which includes the outline
    // border) and elevation over _kElevationDuration. When pressed, the
    // button makes its fill color opaque white first, and then sets
    // its highlightElevation. We can't change the elevation while the
    // button's fill is translucent, because the shadow fills the interior
    // of the button.

    _controller = new AnimationController(
      duration: _kPressDuration,
      vsync: this
    );
    _fillAnimation = new CurvedAnimation(
      parent: _controller,
      curve: const Interval(0.0, 0.5,
        curve: Curves.fastOutSlowIn,
      ),
    );
    _elevationAnimation = new CurvedAnimation(
      parent: _controller,
      curve: const Interval(0.5, 0.5),
      reverseCurve: const Interval(1.0, 1.0),
    );
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  ButtonTextTheme _getTextTheme(ButtonThemeData buttonTheme) {
    return widget.textTheme ?? buttonTheme.textTheme;
  }

  // TODO(hmuller): this method is the same as FlatButton
  Color _getTextColor(ThemeData theme, ButtonThemeData buttonTheme) {
    final Color color = widget.enabled ? widget.textColor : widget.disabledTextColor;
    if (color != null)
      return color;

    final bool themeIsDark = theme.brightness == Brightness.dark;
    switch (_getTextTheme(buttonTheme)) {
      case ButtonTextTheme.normal:
        return widget.enabled
          ? (themeIsDark ? Colors.white : Colors.black87)
          : theme.disabledColor;
      case ButtonTextTheme.accent:
        return widget.enabled
          ? theme.accentColor
          : theme.disabledColor;
      case ButtonTextTheme.primary:
        return widget.enabled
          ? theme.buttonColor
          : (themeIsDark ? Colors.white30 : Colors.black38);
    }
    return null;
  }

  Color _getFillColor(ThemeData theme) {
    final bool themeIsDark = theme.brightness == Brightness.dark;
    final Color color = widget.color ?? (themeIsDark
      ? const Color(0x00000000)
      : const Color(0x00FFFFFF));
    final Tween<Color> colorTween = new ColorTween(
      begin: color.withAlpha(0x00),
      end: color.withAlpha(0xFF),
    );
    return colorTween.evaluate(_fillAnimation);
  }

  // TODO(hmuller): this method is the same as FlatButton
  Color _getSplashColor(ThemeData theme, ButtonThemeData buttonTheme) {
    if (widget.splashColor != null)
      return widget.splashColor;

    switch (_getTextTheme(buttonTheme)) {
      case ButtonTextTheme.normal:
      case ButtonTextTheme.accent:
        return theme.splashColor;
      case ButtonTextTheme.primary:
        return theme.brightness == Brightness.dark
          ? Colors.white12
          : theme.primaryColor.withOpacity(0.12);
    }
    return Colors.transparent;
  }

  BorderSide _getOutline(ThemeData theme, ButtonThemeData buttonTheme) {
    final bool themeIsDark = theme.brightness == Brightness.dark;
    if (widget.borderSide?.style == BorderStyle.none)
      return widget.borderSide;

    final Color color = widget.enabled
      ? (_pressed
         ? widget.highlightedBorderColor ?? theme.primaryColor
         : (widget.borderSide?.color ??
            (themeIsDark ? Colors.grey[600] : Colors.grey[200])))
      : (widget.disabledBorderColor ??
         (themeIsDark ? Colors.grey[800] : Colors.grey[100]));

    return new BorderSide(
      color: color,
      width: widget.borderSide?.width ?? 2.0,
    );
  }

  double _getHighlightElevation() {
    return new Tween<double>(
      begin: 0.0,
      end: widget.highlightElevation ?? 2.0,
    ).evaluate(_elevationAnimation);
  }

  @override
  Widget build(BuildContext context) {
    final ThemeData theme = Theme.of(context);
    final ButtonThemeData buttonTheme = ButtonTheme.of(context);
    final Color textColor = _getTextColor(theme, buttonTheme);
    final Color splashColor = _getSplashColor(theme, buttonTheme);

    return new AnimatedBuilder(
      animation: _controller,
      builder: (BuildContext context, Widget child) {
        return new RaisedButton(
          textColor: textColor,
          disabledTextColor: widget.disabledTextColor,
          color: _getFillColor(theme),
          splashColor: splashColor,
          highlightColor: widget.highlightColor,
          disabledColor: Colors.transparent,
          onPressed: widget.onPressed,
          elevation: 0.0,
          disabledElevation: 0.0,
          highlightElevation: _getHighlightElevation(),
          onHighlightChanged: (bool value) {
            setState(() {
              _pressed = value;
              if (value)
                _controller.forward();
              else
                _controller.reverse();
            });
          },
          padding: widget.padding,
          shape: new _OutlineBorder(
            shape: widget.shape ?? buttonTheme.shape,
            side: _getOutline(theme, buttonTheme),
          ),
          clipBehavior: widget.clipBehavior,
          animationDuration: _kElevationDuration,
          child: widget.child,
        );
      },
    );
  }
}

// Render the button's outline border using using the OutlineButton's
// border parameters and the button or buttonTheme's shape.
class _OutlineBorder extends ShapeBorder {
  const _OutlineBorder({
    @required this.shape,
    @required this.side,
  }) : assert(shape != null),
       assert(side != null);

  final ShapeBorder shape;
  final BorderSide side;

  @override
  EdgeInsetsGeometry get dimensions {
    return new EdgeInsets.all(side.width);
  }

  @override
  ShapeBorder scale(double t) {
    return new _OutlineBorder(
      shape: shape.scale(t),
      side: side.scale(t),
    );
  }

  @override
  ShapeBorder lerpFrom(ShapeBorder a, double t) {
    assert(t != null);
    if (a is _OutlineBorder) {
      return new _OutlineBorder(
        side: BorderSide.lerp(a.side, side, t),
        shape: ShapeBorder.lerp(a.shape, shape, t),
      );
    }
    return super.lerpFrom(a, t);
  }

  @override
  ShapeBorder lerpTo(ShapeBorder b, double t) {
    assert(t != null);
    if (b is _OutlineBorder) {
      return new _OutlineBorder(
        side: BorderSide.lerp(side, b.side, t),
        shape: ShapeBorder.lerp(shape, b.shape, t),
      );
    }
    return super.lerpTo(b, t);
  }

  @override
  Path getInnerPath(Rect rect, { TextDirection textDirection }) {
    return shape.getInnerPath(rect.deflate(side.width), textDirection: textDirection);
  }

  @override
  Path getOuterPath(Rect rect, { TextDirection textDirection }) {
    return shape.getOuterPath(rect, textDirection: textDirection);
  }

  @override
  void paint(Canvas canvas, Rect rect, { TextDirection textDirection }) {
    switch (side.style) {
      case BorderStyle.none:
        break;
      case BorderStyle.solid:
        canvas.drawPath(shape.getOuterPath(rect), side.toPaint());
    }
  }

  @override
  bool operator ==(dynamic other) {
    if (identical(this, other))
      return true;
    if (runtimeType != other.runtimeType)
      return false;
    final _OutlineBorder typedOther = other;
    return side == typedOther.side && shape == typedOther.shape;
  }

  @override
  int get hashCode => hashValues(side, shape);
}
