// 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 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';

import 'button.dart';
import 'button_theme.dart';
import 'colors.dart';
import 'constants.dart';
import 'theme.dart';
import 'theme_data.dart';

/// A material design "raised button".
///
/// A raised button is based on a [Material] widget whose [Material.elevation]
/// increases when the button is pressed.
///
/// Use raised buttons to add dimension to otherwise mostly flat layouts, e.g.
/// in long busy lists of content, or in wide spaces. Avoid using raised buttons
/// on already-raised content such as dialogs or cards.
///
/// 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 are
/// trying to change the button's [color] and it is not having any effect, check
/// that you are passing a non-null [onPressed] handler.
///
/// If you want an ink-splash effect for taps, but don't want to use a button,
/// consider using [InkWell] directly.
///
/// Raised buttons have a minimum size of 88.0 by 36.0 which can be overidden
/// with [ButtonTheme].
///
/// See also:
///
///  * [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.
///  * [RawMaterialButton], the widget this widget is based on.
///  * <https://material.google.com/components/buttons.html>
class RaisedButton extends StatelessWidget {
  /// Create a filled button.
  ///
  /// The [elevation], [highlightElevation], [disabledElevation], and
  /// [clipBehavior] arguments must not be null.
  const RaisedButton({
    Key key,
    @required this.onPressed,
    this.onHighlightChanged,
    this.textTheme,
    this.textColor,
    this.disabledTextColor,
    this.color,
    this.disabledColor,
    this.highlightColor,
    this.splashColor,
    this.colorBrightness,
    this.elevation = 2.0,
    this.highlightElevation = 8.0,
    this.disabledElevation = 0.0,
    this.padding,
    this.shape,
    this.clipBehavior = Clip.none,
    this.materialTapTargetSize,
    this.animationDuration = kThemeChangeDuration,
    this.child,
  }) : assert(elevation != null),
       assert(highlightElevation != null),
       assert(disabledElevation != null),
       assert(animationDuration != null),
       assert(clipBehavior != null),
       super(key: key);

  /// Create a filled 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 [elevation], [highlightElevation], [disabledElevation], [icon],
  /// [label], and [clipBehavior] arguments must not be null.
  RaisedButton.icon({
    Key key,
    @required this.onPressed,
    this.onHighlightChanged,
    this.textTheme,
    this.textColor,
    this.disabledTextColor,
    this.color,
    this.disabledColor,
    this.highlightColor,
    this.splashColor,
    this.colorBrightness,
    this.elevation = 2.0,
    this.highlightElevation = 8.0,
    this.disabledElevation = 0.0,
    this.shape,
    this.clipBehavior = Clip.none,
    this.materialTapTargetSize,
    this.animationDuration = kThemeChangeDuration,
    @required Widget icon,
    @required Widget label,
  }) : assert(elevation != null),
       assert(highlightElevation != null),
       assert(disabledElevation != null),
       assert(icon != null),
       assert(label != null),
       assert(animationDuration != 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;

  /// Called by the underlying [InkWell] widget's [InkWell.onHighlightChanged]
  /// callback.
  final ValueChanged<bool> onHighlightChanged;

  /// 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] - The color to use for this button's text when the button is [enabled].
  final Color disabledTextColor;

  /// The button's fill color, displayed by its [Material], while it
  /// is in its default (unpressed, [enabled]) state.
  ///
  /// The default fill color is the theme's button color, [ThemeData.buttonColor].
  ///
  /// Typically the default color will be overidden with a Material color,
  /// for example:
  ///
  /// ```dart
  ///  new RaisedButton(
  ///    color: Colors.blue,
  ///    onPressed: _handleTap,
  ///    child: new Text('DEMO'),
  ///  ),
  /// ```
  ///
  /// See also:
  ///   * [disabledColor] - the fill color of the button when the button is disabled.
  final Color color;

  /// The fill color of the button when the button is disabled.
  ///
  /// The default value of this color is the theme's disabled color,
  /// [ThemeData.disabledColor].
  ///
  /// See also:
  ///   * [color] - the fill color of the button when the button is [enabled].
  final Color disabledColor;

  /// 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.
  ///
  /// The default splash color is 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 z-coordinate at which to place this button. This controls the size of
  /// the shadow below the raised button.
  ///
  /// Defaults to 2, the appropriate elevation for raised buttons.
  ///
  /// See also:
  ///
  ///  * [FlatButton], a button with no elevation or fill color.
  ///  * [disabledElevation], the elevation when the button is disabled.
  ///  * [highlightElevation], the elevation when the button is pressed.
  final double elevation;

  /// The elevation for the button's [Material] when the button
  /// is [enabled] but not pressed.
  ///
  /// Defaults to 2.0.
  ///
  /// See also:
  ///
  ///  * [highlightElevation], the default elevation.
  ///  * [disabledElevation], the elevation when the button is disabled.

  /// The elevation for the button's [Material] when the button
  /// is [enabled] and pressed.
  ///
  /// This controls the size of the shadow below the button. When a tap
  /// down gesture occurs within the button, its [InkWell] displays a
  /// [highlightColor] "highlight".
  ///
  /// Defaults to 8.0.
  ///
  /// See also:
  ///
  ///  * [elevation], the default elevation.
  ///  * [disabledElevation], the elevation when the button is disabled.
  final double highlightElevation;

  /// The elevation for the button's [Material] when the button
  /// is not [enabled].
  ///
  /// Defaults to 0.0.
  ///
  /// See also:
  ///
  ///  * [elevation], the default elevation.
  ///  * [highlightElevation], the elevation when the button is pressed.
  final double disabledElevation;

  /// The theme brightness to use for this button.
  ///
  /// Defaults to the theme's brightness, [ThemeData.brightness].
  final Brightness colorBrightness;

  /// 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;

  /// 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].
  ///
  /// The button's highlight and splash are clipped to this shape. If the
  /// button has an elevation, then its drop shadow is defined by this
  /// shape as well.
  final ShapeBorder shape;

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

  /// Defines the duration of animated changes for [shape] and [elevation].
  ///
  /// The default value is [kThemeChangeDuration].
  final Duration animationDuration;

  /// Configures the minimum size of the tap target.
  ///
  /// Defaults to [ThemeData.materialTapTargetSize].
  ///
  /// See also:
  ///
  ///   * [MaterialTapTargetSize], for a description of how this affects tap targets.
  final MaterialTapTargetSize materialTapTargetSize;

  Brightness _getBrightness(ThemeData theme) {
    return colorBrightness ?? theme.brightness;
  }

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

  Color _getFillColor(ThemeData theme, ButtonThemeData buttonTheme) {
    final Color fillColor = enabled ? color : disabledColor;
    if (fillColor != null)
      return fillColor;

    final bool themeIsDark = _getBrightness(theme) == Brightness.dark;
    switch (_getTextTheme(buttonTheme)) {
      case ButtonTextTheme.normal:
      case ButtonTextTheme.accent:
        return enabled
          ? theme.buttonColor
          : theme.disabledColor;
      case ButtonTextTheme.primary:
        return enabled
          ? theme.buttonColor
          : (themeIsDark ? Colors.white12 : Colors.black12);
    }
    return null;
  }

  Color _getTextColor(ThemeData theme, ButtonThemeData buttonTheme, Color fillColor) {
    final Color color = enabled ? textColor : disabledTextColor;
    if (color != null)
      return color;

    final bool themeIsDark = _getBrightness(theme) == Brightness.dark;
    final bool fillIsDark = fillColor != null
      ? ThemeData.estimateBrightnessForColor(fillColor) == Brightness.dark
      : themeIsDark;

    switch (_getTextTheme(buttonTheme)) {
      case ButtonTextTheme.normal:
        return enabled
          ? (themeIsDark ? Colors.white : Colors.black87)
          : theme.disabledColor;
      case ButtonTextTheme.accent:
        return enabled
          ? theme.accentColor
          : theme.disabledColor;
      case ButtonTextTheme.primary:
        return enabled
          ? (fillIsDark ? Colors.white : Colors.black)
          : (themeIsDark ? Colors.white30 : Colors.black38);
    }
    return null;
  }

  Color _getHighlightColor(ThemeData theme, ButtonThemeData buttonTheme) {
    if (highlightColor != null)
      return highlightColor;

    switch (_getTextTheme(buttonTheme)) {
      case ButtonTextTheme.normal:
      case ButtonTextTheme.accent:
        return theme.highlightColor;
      case ButtonTextTheme.primary:
        return Colors.transparent;
    }
    return Colors.transparent;
  }

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

    return new RawMaterialButton(
      onPressed: onPressed,
      onHighlightChanged: onHighlightChanged,
      fillColor: fillColor,
      textStyle: theme.textTheme.button.copyWith(color: textColor),
      highlightColor: _getHighlightColor(theme, buttonTheme),
      splashColor: splashColor ?? theme.splashColor,
      elevation: elevation,
      highlightElevation: highlightElevation,
      disabledElevation: disabledElevation,
      padding: padding ?? buttonTheme.padding,
      constraints: buttonTheme.constraints,
      shape: shape ?? buttonTheme.shape,
      clipBehavior: clipBehavior,
      animationDuration: animationDuration,
      child: child,
      materialTapTargetSize: materialTapTargetSize ?? theme.materialTapTargetSize,
    );
  }

  @override
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
    properties.add(new ObjectFlagProperty<VoidCallback>('onPressed', onPressed, ifNull: 'disabled'));
    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>('disabledColor', disabledColor, defaultValue: null));
    properties.add(new DiagnosticsProperty<Color>('highlightColor', highlightColor, defaultValue: null));
    properties.add(new DiagnosticsProperty<Color>('splashColor', splashColor, defaultValue: null));
    properties.add(new DiagnosticsProperty<Brightness>('colorBrightness', colorBrightness, defaultValue: null));
    properties.add(new DiagnosticsProperty<double>('elevation', elevation, defaultValue: null));
    properties.add(new DiagnosticsProperty<double>('highlightElevation', highlightElevation, defaultValue: null));
    properties.add(new DiagnosticsProperty<double>('disabledElevation', disabledElevation, defaultValue: null));
    properties.add(new DiagnosticsProperty<EdgeInsetsGeometry>('padding', padding, defaultValue: null));
    properties.add(new DiagnosticsProperty<ShapeBorder>('shape', shape, defaultValue: null));
  }
}
