// 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:collection';
import 'dart:math' as math;
import 'dart:ui' as ui show Gradient, lerpDouble;

import 'package:flutter/foundation.dart';

import 'alignment.dart';
import 'basic_types.dart';

class _ColorsAndStops {
  _ColorsAndStops(this.colors, this.stops);
  final List<Color> colors;
  final List<double> stops;
}

/// Calculate the color at position [t] of the gradient defined by [colors] and [stops].
Color _sample(List<Color> colors, List<double> stops, double t) {
  assert(colors != null);
  assert(colors.isNotEmpty);
  assert(stops != null);
  assert(stops.isNotEmpty);
  assert(t != null);
  if (t <= stops.first)
    return colors.first;
  if (t >= stops.last)
    return colors.last;
  final int index = stops.lastIndexWhere((double s) => s <= t);
  assert(index != -1);
  return Color.lerp(
      colors[index], colors[index + 1],
      (t - stops[index]) / (stops[index + 1] - stops[index]),
  );
}

_ColorsAndStops _interpolateColorsAndStops(
  List<Color> aColors,
  List<double> aStops,
  List<Color> bColors,
  List<double> bStops,
  double t,
) {
  assert(aColors.length >= 2);
  assert(bColors.length >= 2);
  assert(aStops.length == aColors.length);
  assert(bStops.length == bColors.length);
  final SplayTreeSet<double> stops = SplayTreeSet<double>()
    ..addAll(aStops)
    ..addAll(bStops);
  final List<double> interpolatedStops = stops.toList(growable: false);
  final List<Color> interpolatedColors = interpolatedStops.map<Color>(
          (double stop) => Color.lerp(_sample(aColors, aStops, stop), _sample(bColors, bStops, stop), t)
  ).toList(growable: false);
  return _ColorsAndStops(interpolatedColors, interpolatedStops);
}

/// A 2D gradient.
///
/// This is an interface that allows [LinearGradient], [RadialGradient], and
/// [SweepGradient] classes to be used interchangeably in [BoxDecoration]s.
///
/// See also:
///
///  * [Gradient](dart-ui/Gradient-class.html), the class in the [dart:ui] library.
///
@immutable
abstract class Gradient {
  /// Initialize the gradient's colors and stops.
  ///
  /// The [colors] argument must not be null, and must have at least two colors
  /// (the length is not verified until the [createShader] method is called).
  ///
  /// If specified, the [stops] argument must have the same number of entries as
  /// [colors] (this is also not verified until the [createShader] method is
  /// called).
  const Gradient({
    @required this.colors,
    this.stops,
  }) : assert(colors != null);

  /// The colors the gradient should obtain at each of the stops.
  ///
  /// If [stops] is non-null, this list must have the same length as [stops].
  ///
  /// This list must have at least two colors in it (otherwise, it's not a
  /// gradient!).
  final List<Color> colors;

  /// A list of values from 0.0 to 1.0 that denote fractions along the gradient.
  ///
  /// If non-null, this list must have the same length as [colors].
  ///
  /// If the first value is not 0.0, then a stop with position 0.0 and a color
  /// equal to the first color in [colors] is implied.
  ///
  /// If the last value is not 1.0, then a stop with position 1.0 and a color
  /// equal to the last color in [colors] is implied.
  ///
  /// The values in the [stops] list must be in ascending order. If a value in
  /// the [stops] list is less than an earlier value in the list, then its value
  /// is assumed to equal the previous value.
  ///
  /// If stops is null, then a set of uniformly distributed stops is implied,
  /// with the first stop at 0.0 and the last stop at 1.0.
  final List<double> stops;

  List<double> _impliedStops() {
    if (stops != null)
      return stops;
    assert(colors.length >= 2, 'colors list must have at least two colors');
    final double separation = 1.0 / (colors.length - 1);
    return List<double>.generate(
      colors.length,
      (int index) => index * separation,
      growable: false,
    );
  }

  /// Creates a [Shader] for this gradient to fill the given rect.
  ///
  /// If the gradient's configuration is text-direction-dependent, for example
  /// it uses [AlignmentDirectional] objects instead of [Alignment]
  /// objects, then the `textDirection` argument must not be null.
  Shader createShader(Rect rect, { TextDirection textDirection });

  /// Returns a new gradient with its properties scaled by the given factor.
  ///
  /// A factor of 0.0 (or less) should result in a variant of the gradient that
  /// is invisible; any two factors epsilon apart should be unnoticeably
  /// different from each other at first glance. From this it follows that
  /// scaling a gradient with values from 1.0 to 0.0 over time should cause the
  /// gradient to smoothly disappear.
  ///
  /// Typically this is the same as interpolating from null (with [lerp]).
  Gradient scale(double factor);

  /// Linearly interpolates from another [Gradient] to `this`.
  ///
  /// When implementing this method in subclasses, return null if this class
  /// cannot interpolate from `a`. In that case, [lerp] will try `a`'s [lerpTo]
  /// method instead.
  ///
  /// If `a` is null, this must not return null. The base class implements this
  /// by deferring to [scale].
  ///
  /// The `t` argument represents position on the timeline, with 0.0 meaning
  /// that the interpolation has not started, returning `a` (or something
  /// equivalent to `a`), 1.0 meaning that the interpolation has finished,
  /// returning `this` (or something equivalent to `this`), and values in
  /// between meaning that the interpolation is at the relevant point on the
  /// timeline between `a` and `this`. The interpolation can be extrapolated
  /// beyond 0.0 and 1.0, so negative values and values greater than 1.0 are
  /// valid (and can easily be generated by curves such as
  /// [Curves.elasticInOut]).
  ///
  /// Values for `t` are usually obtained from an [Animation<double>], such as
  /// an [AnimationController].
  ///
  /// Instead of calling this directly, use [Gradient.lerp].
  @protected
  Gradient lerpFrom(Gradient a, double t) {
    if (a == null)
      return scale(t);
    return null;
  }

  /// Linearly interpolates from `this` to another [Gradient].
  ///
  /// This is called if `b`'s [lerpTo] did not know how to handle this class.
  ///
  /// When implementing this method in subclasses, return null if this class
  /// cannot interpolate from `b`. In that case, [lerp] will apply a default
  /// behavior instead.
  ///
  /// If `b` is null, this must not return null. The base class implements this
  /// by deferring to [scale].
  ///
  /// The `t` argument represents position on the timeline, with 0.0 meaning
  /// that the interpolation has not started, returning `this` (or something
  /// equivalent to `this`), 1.0 meaning that the interpolation has finished,
  /// returning `b` (or something equivalent to `b`), and values in between
  /// meaning that the interpolation is at the relevant point on the timeline
  /// between `this` and `b`. The interpolation can be extrapolated beyond 0.0
  /// and 1.0, so negative values and values greater than 1.0 are valid (and can
  /// easily be generated by curves such as [Curves.elasticInOut]).
  ///
  /// Values for `t` are usually obtained from an [Animation<double>], such as
  /// an [AnimationController].
  ///
  /// Instead of calling this directly, use [Gradient.lerp].
  @protected
  Gradient lerpTo(Gradient b, double t) {
    if (b == null)
      return scale(1.0 - t);
    return null;
  }

  /// Linearly interpolates between two [Gradient]s.
  ///
  /// This defers to `b`'s [lerpTo] function if `b` is not null. If `b` is
  /// null or if its [lerpTo] returns null, it uses `a`'s [lerpFrom]
  /// function instead. If both return null, it returns `a` before `t == 0.5`
  /// and `b` after `t == 0.5`.
  ///
  /// {@macro dart.ui.shadow.lerp}
  static Gradient lerp(Gradient a, Gradient b, double t) {
    assert(t != null);
    Gradient result;
    if (b != null)
      result = b.lerpFrom(a, t); // if a is null, this must return non-null
    if (result == null && a != null)
      result = a.lerpTo(b, t); // if b is null, this must return non-null
    if (result != null)
      return result;
    if (a == null && b == null)
      return null;
    assert(a != null && b != null);
    return t < 0.5 ? a.scale(1.0 - (t * 2.0)) : b.scale((t - 0.5) * 2.0);
  }
}

/// A 2D linear gradient.
///
/// This class is used by [BoxDecoration] to represent linear gradients. This
/// abstracts out the arguments to the [new ui.Gradient.linear] constructor from
/// the `dart:ui` library.
///
/// A gradient has two anchor points, [begin] and [end]. The [begin] point
/// corresponds to 0.0, and the [end] point corresponds to 1.0. These points are
/// expressed in fractions, so that the same gradient can be reused with varying
/// sized boxes without changing the parameters. (This contrasts with [new
/// ui.Gradient.linear], whose arguments are expressed in logical pixels.)
///
/// The [colors] are described by a list of [Color] objects. There must be at
/// least two colors. The [stops] list, if specified, must have the same length
/// as [colors]. It specifies fractions of the vector from start to end, between
/// 0.0 and 1.0, for each color. If it is null, a uniform distribution is
/// assumed.
///
/// The region of the canvas before [begin] and after [end] is colored according
/// to [tileMode].
///
/// Typically this class is used with [BoxDecoration], which does the painting.
/// To use a [LinearGradient] to paint on a canvas directly, see [createShader].
///
/// {@tool sample}
///
/// This sample draws a picture that looks like vertical window shades by having
/// a [Container] display a [BoxDecoration] with a [LinearGradient].
///
/// ```dart
/// Container(
///   decoration: BoxDecoration(
///     gradient: LinearGradient(
///       begin: Alignment.topLeft,
///       end: Alignment(0.8, 0.0), // 10% of the width, so there are ten blinds.
///       colors: [const Color(0xFFFFFFEE), const Color(0xFF999999)], // whitish to gray
///       tileMode: TileMode.repeated, // repeats the gradient over the canvas
///     ),
///   ),
/// )
/// ```
/// {@end-tool}
///
/// See also:
///
///  * [RadialGradient], which displays a gradient in concentric circles, and
///    has an example which shows a different way to use [Gradient] objects.
///  * [SweepGradient], which displays a gradient in a sweeping arc around a
///    center point.
///  * [BoxDecoration], which can take a [LinearGradient] in its
///    [BoxDecoration.gradient] property.
class LinearGradient extends Gradient {
  /// Creates a linear gradient.
  ///
  /// The [colors] argument must not be null. If [stops] is non-null, it must
  /// have the same length as [colors].
  const LinearGradient({
    this.begin = Alignment.centerLeft,
    this.end = Alignment.centerRight,
    @required List<Color> colors,
    List<double> stops,
    this.tileMode = TileMode.clamp,
  }) : assert(begin != null),
       assert(end != null),
       assert(tileMode != null),
       super(colors: colors, stops: stops);

  /// The offset at which stop 0.0 of the gradient is placed.
  ///
  /// If this is an [Alignment], then it is expressed as a vector from
  /// coordinate (0.0, 0.0), in a coordinate space that maps the center of the
  /// paint box at (0.0, 0.0) and the bottom right at (1.0, 1.0).
  ///
  /// For example, a begin offset of (-1.0, 0.0) is half way down the
  /// left side of the box.
  ///
  /// It can also be an [AlignmentDirectional], where the start is the
  /// left in left-to-right contexts and the right in right-to-left contexts. If
  /// a text-direction-dependent value is provided here, then the [createShader]
  /// method will need to be given a [TextDirection].
  final AlignmentGeometry begin;

  /// The offset at which stop 1.0 of the gradient is placed.
  ///
  /// If this is an [Alignment], then it is expressed as a vector from
  /// coordinate (0.0, 0.0), in a coordinate space that maps the center of the
  /// paint box at (0.0, 0.0) and the bottom right at (1.0, 1.0).
  ///
  /// For example, a begin offset of (1.0, 0.0) is half way down the
  /// right side of the box.
  ///
  /// It can also be an [AlignmentDirectional], where the start is the left in
  /// left-to-right contexts and the right in right-to-left contexts. If a
  /// text-direction-dependent value is provided here, then the [createShader]
  /// method will need to be given a [TextDirection].
  final AlignmentGeometry end;

  /// How this gradient should tile the plane beyond in the region before
  /// [begin] and after [end].
  ///
  /// For details, see [TileMode].
  ///
  /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/tile_mode_clamp_linear.png)
  /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/tile_mode_mirror_linear.png)
  /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/tile_mode_repeated_linear.png)
  final TileMode tileMode;

  @override
  Shader createShader(Rect rect, { TextDirection textDirection }) {
    return ui.Gradient.linear(
      begin.resolve(textDirection).withinRect(rect),
      end.resolve(textDirection).withinRect(rect),
      colors, _impliedStops(), tileMode,
    );
  }

  /// Returns a new [LinearGradient] with its colors scaled by the given factor.
  ///
  /// Since the alpha component of the Color is what is scaled, a factor
  /// of 0.0 or less results in a gradient that is fully transparent.
  @override
  LinearGradient scale(double factor) {
    return LinearGradient(
      begin: begin,
      end: end,
      colors: colors.map<Color>((Color color) => Color.lerp(null, color, factor)).toList(),
      stops: stops,
      tileMode: tileMode,
    );
  }

  @override
  Gradient lerpFrom(Gradient a, double t) {
    if (a == null || (a is LinearGradient))
      return LinearGradient.lerp(a, this, t);
    return super.lerpFrom(a, t);
  }

  @override
  Gradient lerpTo(Gradient b, double t) {
    if (b == null || (b is LinearGradient))
      return LinearGradient.lerp(this, b, t);
    return super.lerpTo(b, t);
  }

  /// Linearly interpolate between two [LinearGradient]s.
  ///
  /// If either gradient is null, this function linearly interpolates from a
  /// a gradient that matches the other gradient in [begin], [end], [stops] and
  /// [tileMode] and with the same [colors] but transparent (using [scale]).
  ///
  /// If neither gradient is null, they must have the same number of [colors].
  ///
  /// The `t` argument represents a position on the timeline, with 0.0 meaning
  /// that the interpolation has not started, returning `a` (or something
  /// equivalent to `a`), 1.0 meaning that the interpolation has finished,
  /// returning `b` (or something equivalent to `b`), and values in between
  /// meaning that the interpolation is at the relevant point on the timeline
  /// between `a` and `b`. The interpolation can be extrapolated beyond 0.0 and
  /// 1.0, so negative values and values greater than 1.0 are valid (and can
  /// easily be generated by curves such as [Curves.elasticInOut]).
  ///
  /// Values for `t` are usually obtained from an [Animation<double>], such as
  /// an [AnimationController].
  static LinearGradient lerp(LinearGradient a, LinearGradient b, double t) {
    assert(t != null);
    if (a == null && b == null)
      return null;
    if (a == null)
      return b.scale(t);
    if (b == null)
      return a.scale(1.0 - t);
    final _ColorsAndStops interpolated = _interpolateColorsAndStops(
        a.colors,
        a._impliedStops(),
        b.colors,
        b._impliedStops(),
        t,
    );
    return LinearGradient(
      begin: AlignmentGeometry.lerp(a.begin, b.begin, t),
      end: AlignmentGeometry.lerp(a.end, b.end, t),
      colors: interpolated.colors,
      stops: interpolated.stops,
      tileMode: t < 0.5 ? a.tileMode : b.tileMode, // TODO(ianh): interpolate tile mode
    );
  }

  @override
  bool operator ==(dynamic other) {
    if (identical(this, other))
      return true;
    if (runtimeType != other.runtimeType)
      return false;
    final LinearGradient typedOther = other;
    if (begin != typedOther.begin ||
        end != typedOther.end ||
        tileMode != typedOther.tileMode ||
        colors?.length != typedOther.colors?.length ||
        stops?.length != typedOther.stops?.length)
      return false;
    if (colors != null) {
      assert(typedOther.colors != null);
      assert(colors.length == typedOther.colors.length);
      for (int i = 0; i < colors.length; i += 1) {
        if (colors[i] != typedOther.colors[i])
          return false;
      }
    }
    if (stops != null) {
      assert(typedOther.stops != null);
      assert(stops.length == typedOther.stops.length);
      for (int i = 0; i < stops.length; i += 1) {
        if (stops[i] != typedOther.stops[i])
          return false;
      }
    }
    return true;
  }

  @override
  int get hashCode => hashValues(begin, end, tileMode, hashList(colors), hashList(stops));

  @override
  String toString() {
    return '$runtimeType($begin, $end, $colors, $stops, $tileMode)';
  }
}

/// A 2D radial gradient.
///
/// This class is used by [BoxDecoration] to represent radial gradients. This
/// abstracts out the arguments to the [new ui.Gradient.radial] constructor from
/// the `dart:ui` library.
///
/// A normal radial gradient has a [center] and a [radius]. The [center] point
/// corresponds to 0.0, and the ring at [radius] from the center corresponds
/// to 1.0. These lengths are expressed in fractions, so that the same gradient
/// can be reused with varying sized boxes without changing the parameters.
/// (This contrasts with [new ui.Gradient.radial], whose arguments are expressed
/// in logical pixels.)
///
/// It is also possible to create a two-point (or focal pointed) radial gradient
/// (which is sometimes referred to as a two point conic gradient, but is not the
/// same as a CSS conic gradient which corresponds to a [SweepGradient]). A [focal]
/// point and [focalRadius] can be specified similarly to [center] and [radius],
/// which will make the rendered gradient appear to be pointed or directed in the
/// direction of the [focal] point. This is only important if [focal] and [center]
/// are not equal or [focalRadius] > 0.0 (as this case is visually identical to a
/// normal radial gradient).  One important case to avoid is having [focal] and
/// [center] both resolve to [Offset.zero] when [focalRadius] > 0.0. In such a case,
/// a valid shader cannot be created by the framework.
///
/// The [colors] are described by a list of [Color] objects. There must be at
/// least two colors. The [stops] list, if specified, must have the same length
/// as [colors]. It specifies fractions of the radius between 0.0 and 1.0,
/// giving concentric rings for each color stop. If it is null, a uniform
/// distribution is assumed.
///
/// The region of the canvas beyond [radius] from the [center] is colored
/// according to [tileMode].
///
/// Typically this class is used with [BoxDecoration], which does the painting.
/// To use a [RadialGradient] to paint on a canvas directly, see [createShader].
///
/// {@tool sample}
///
/// This function draws a gradient that looks like a sun in a blue sky.
///
/// ```dart
/// void paintSky(Canvas canvas, Rect rect) {
///   var gradient = RadialGradient(
///     center: const Alignment(0.7, -0.6), // near the top right
///     radius: 0.2,
///     colors: [
///       const Color(0xFFFFFF00), // yellow sun
///       const Color(0xFF0099FF), // blue sky
///     ],
///     stops: [0.4, 1.0],
///   );
///   // rect is the area we are painting over
///   var paint = Paint()
///     ..shader = gradient.createShader(rect);
///   canvas.drawRect(rect, paint);
/// }
/// ```
/// {@end-tool}
///
/// See also:
///
///  * [LinearGradient], which displays a gradient in parallel lines, and has an
///    example which shows a different way to use [Gradient] objects.
///  * [SweepGradient], which displays a gradient in a sweeping arc around a
///    center point.
///  * [BoxDecoration], which can take a [RadialGradient] in its
///    [BoxDecoration.gradient] property.
///  * [CustomPainter], which shows how to use the above sample code in a custom
///    painter.
class RadialGradient extends Gradient {
  /// Creates a radial gradient.
  ///
  /// The [colors] argument must not be null. If [stops] is non-null, it must
  /// have the same length as [colors].
  const RadialGradient({
    this.center = Alignment.center,
    this.radius = 0.5,
    @required List<Color> colors,
    List<double> stops,
    this.tileMode = TileMode.clamp,
    this.focal,
    this.focalRadius = 0.0,
  }) : assert(center != null),
       assert(radius != null),
       assert(tileMode != null),
       assert(focalRadius != null),
       super(colors: colors, stops: stops);

  /// The center of the gradient, as an offset into the (-1.0, -1.0) x (1.0, 1.0)
  /// square describing the gradient which will be mapped onto the paint box.
  ///
  /// For example, an alignment of (0.0, 0.0) will place the radial
  /// gradient in the center of the box.
  ///
  /// If this is an [Alignment], then it is expressed as a vector from
  /// coordinate (0.0, 0.0), in a coordinate space that maps the center of the
  /// paint box at (0.0, 0.0) and the bottom right at (1.0, 1.0).
  ///
  /// It can also be an [AlignmentDirectional], where the start is the left in
  /// left-to-right contexts and the right in right-to-left contexts. If a
  /// text-direction-dependent value is provided here, then the [createShader]
  /// method will need to be given a [TextDirection].
  final AlignmentGeometry center;

  /// The radius of the gradient, as a fraction of the shortest side
  /// of the paint box.
  ///
  /// For example, if a radial gradient is painted on a box that is
  /// 100.0 pixels wide and 200.0 pixels tall, then a radius of 1.0
  /// will place the 1.0 stop at 100.0 pixels from the [center].
  final double radius;

  /// How this gradient should tile the plane beyond the outer ring at [radius]
  /// pixels from the [center].
  ///
  /// For details, see [TileMode].
  ///
  /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/tile_mode_clamp_radial.png)
  /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/tile_mode_mirror_radial.png)
  /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/tile_mode_repeated_radial.png)
  ///
  /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/tile_mode_clamp_radialWithFocal.png)
  /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/tile_mode_mirror_radialWithFocal.png)
  /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/tile_mode_repeated_radialWithFocal.png)
  final TileMode tileMode;

  /// The focal point of the gradient.  If specified, the gradient will appear
  /// to be focused along the vector from [center] to focal.
  ///
  /// See [center] for a description of how the coordinates are mapped.
  ///
  /// If this value is specified and [focalRadius] > 0.0, care should be taken
  /// to ensure that either this value or [center] will not both resolve to
  /// [Offset.zero], which would fail to create a valid gradient.
  final AlignmentGeometry focal;

  /// The radius of the focal point of gradient, as a fraction of the shortest
  /// side of the paint box.
  ///
  /// For example, if a radial gradient is painted on a box that is
  /// 100.0 pixels wide and 200.0 pixels tall, then a radius of 1.0
  /// will place the 1.0 stop at 100.0 pixels from the [focus].
  ///
  /// If this value is specified and is greater than 0.0, either [focal] or
  /// [center] must not resolve to [Offset.zero], which would fail to create
  /// a valid gradient.
  final double focalRadius;

  @override
  Shader createShader(Rect rect, { TextDirection textDirection }) {
    return ui.Gradient.radial(
      center.resolve(textDirection).withinRect(rect),
      radius * rect.shortestSide,
      colors, _impliedStops(), tileMode,
      null, // transform
      focal == null  ? null : focal.resolve(textDirection).withinRect(rect),
      focalRadius * rect.shortestSide,
    );
  }

  /// Returns a new [RadialGradient] with its colors scaled by the given factor.
  ///
  /// Since the alpha component of the Color is what is scaled, a factor
  /// of 0.0 or less results in a gradient that is fully transparent.
  @override
  RadialGradient scale(double factor) {
    return RadialGradient(
      center: center,
      radius: radius,
      colors: colors.map<Color>((Color color) => Color.lerp(null, color, factor)).toList(),
      stops: stops,
      tileMode: tileMode,
      focal: focal,
      focalRadius: focalRadius,
    );
  }

  @override
  Gradient lerpFrom(Gradient a, double t) {
    if (a == null || (a is RadialGradient))
      return RadialGradient.lerp(a, this, t);
    return super.lerpFrom(a, t);
  }

  @override
  Gradient lerpTo(Gradient b, double t) {
    if (b == null || (b is RadialGradient))
      return RadialGradient.lerp(this, b, t);
    return super.lerpTo(b, t);
  }

  /// Linearly interpolate between two [RadialGradient]s.
  ///
  /// If either gradient is null, this function linearly interpolates from a
  /// a gradient that matches the other gradient in [center], [radius], [stops] and
  /// [tileMode] and with the same [colors] but transparent (using [scale]).
  ///
  /// If neither gradient is null, they must have the same number of [colors].
  ///
  /// The `t` argument represents a position on the timeline, with 0.0 meaning
  /// that the interpolation has not started, returning `a` (or something
  /// equivalent to `a`), 1.0 meaning that the interpolation has finished,
  /// returning `b` (or something equivalent to `b`), and values in between
  /// meaning that the interpolation is at the relevant point on the timeline
  /// between `a` and `b`. The interpolation can be extrapolated beyond 0.0 and
  /// 1.0, so negative values and values greater than 1.0 are valid (and can
  /// easily be generated by curves such as [Curves.elasticInOut]).
  ///
  /// Values for `t` are usually obtained from an [Animation<double>], such as
  /// an [AnimationController].
  static RadialGradient lerp(RadialGradient a, RadialGradient b, double t) {
    assert(t != null);
    if (a == null && b == null)
      return null;
    if (a == null)
      return b.scale(t);
    if (b == null)
      return a.scale(1.0 - t);
    final _ColorsAndStops interpolated = _interpolateColorsAndStops(
        a.colors,
        a._impliedStops(),
        b.colors,
        b._impliedStops(),
        t,
    );
    return RadialGradient(
      center: AlignmentGeometry.lerp(a.center, b.center, t),
      radius: math.max(0.0, ui.lerpDouble(a.radius, b.radius, t)),
      colors: interpolated.colors,
      stops: interpolated.stops,
      tileMode: t < 0.5 ? a.tileMode : b.tileMode, // TODO(ianh): interpolate tile mode
      focal: AlignmentGeometry.lerp(a.focal, b.focal, t),
      focalRadius: math.max(0.0, ui.lerpDouble(a.focalRadius, b.focalRadius, t)),
    );
  }

  @override
  bool operator ==(dynamic other) {
    if (identical(this, other))
      return true;
    if (runtimeType != other.runtimeType)
      return false;
    final RadialGradient typedOther = other;
    if (center != typedOther.center ||
        radius != typedOther.radius ||
        tileMode != typedOther.tileMode ||
        colors?.length != typedOther.colors?.length ||
        stops?.length != typedOther.stops?.length ||
        focal != typedOther.focal ||
        focalRadius != typedOther.focalRadius)
      return false;
    if (colors != null) {
      assert(typedOther.colors != null);
      assert(colors.length == typedOther.colors.length);
      for (int i = 0; i < colors.length; i += 1) {
        if (colors[i] != typedOther.colors[i])
          return false;
      }
    }
    if (stops != null) {
      assert(typedOther.stops != null);
      assert(stops.length == typedOther.stops.length);
      for (int i = 0; i < stops.length; i += 1) {
        if (stops[i] != typedOther.stops[i])
          return false;
      }
    }
    return true;
  }

  @override
  int get hashCode => hashValues(center, radius, tileMode, hashList(colors), hashList(stops), focal, focalRadius);

  @override
  String toString() {
    return '$runtimeType($center, $radius, $colors, $stops, $tileMode, $focal, $focalRadius)';
  }
}

/// A 2D sweep gradient.
///
/// This class is used by [BoxDecoration] to represent sweep gradients. This
/// abstracts out the arguments to the [new ui.Gradient.sweep] constructor from
/// the `dart:ui` library.
///
/// A gradient has a [center], a [startAngle], and an [endAngle]. The [startAngle]
/// corresponds to 0.0, and the [endAngle] corresponds to 1.0. These angles are
/// expressed in radians.
///
/// The [colors] are described by a list of [Color] objects. There must be at
/// least two colors. The [stops] list, if specified, must have the same length
/// as [colors]. It specifies fractions of the vector from start to end, between
/// 0.0 and 1.0, for each color. If it is null, a uniform distribution is
/// assumed.
///
/// The region of the canvas before [startAngle] and after [endAngle] is colored
/// according to [tileMode].
///
/// Typically this class is used with [BoxDecoration], which does the painting.
/// To use a [SweepGradient] to paint on a canvas directly, see [createShader].
///
/// {@tool sample}
///
/// This sample draws a different color in each quadrant.
///
/// ```dart
/// Container(
///   decoration: BoxDecoration(
///     gradient: SweepGradient(
///       center: FractionalOffset.center,
///       startAngle: 0.0,
///       endAngle: math.pi * 2,
///       colors: const <Color>[
///         Color(0xFF4285F4), // blue
///         Color(0xFF34A853), // green
///         Color(0xFFFBBC05), // yellow
///         Color(0xFFEA4335), // red
///         Color(0xFF4285F4), // blue again to seamlessly transition to the start
///       ],
///       stops: const <double>[0.0, 0.25, 0.5, 0.75, 1.0],
///      ),
///   ),
///  )
/// ```
/// {@end-tool}
///
/// See also:
///
///  * [LinearGradient], which displays a gradient in parallel lines, and has an
///    example which shows a different way to use [Gradient] objects.
///  * [RadialGradient], which displays a gradient in concentric circles, and
///    has an example which shows a different way to use [Gradient] objects.
///  * [BoxDecoration], which can take a [SweepGradient] in its
///    [BoxDecoration.gradient] property.
class SweepGradient extends Gradient {
  /// Creates a sweep gradient.
  ///
  /// The [colors] argument must not be null. If [stops] is non-null, it must
  /// have the same length as [colors].
  const SweepGradient({
    this.center = Alignment.center,
    this.startAngle = 0.0,
    this.endAngle = math.pi * 2,
    @required List<Color> colors,
    List<double> stops,
    this.tileMode = TileMode.clamp,
  }) : assert(center != null),
       assert(startAngle != null),
       assert(endAngle != null),
       assert(tileMode != null),
       super(colors: colors, stops: stops);

  /// The center of the gradient, as an offset into the (-1.0, -1.0) x (1.0, 1.0)
  /// square describing the gradient which will be mapped onto the paint box.
  ///
  /// For example, an alignment of (0.0, 0.0) will place the sweep
  /// gradient in the center of the box.
  ///
  /// If this is an [Alignment], then it is expressed as a vector from
  /// coordinate (0.0, 0.0), in a coordinate space that maps the center of the
  /// paint box at (0.0, 0.0) and the bottom right at (1.0, 1.0).
  ///
  /// It can also be an [AlignmentDirectional], where the start is the left in
  /// left-to-right contexts and the right in right-to-left contexts. If a
  /// text-direction-dependent value is provided here, then the [createShader]
  /// method will need to be given a [TextDirection].
  final AlignmentGeometry center;

  /// The angle in radians at which stop 0.0 of the gradient is placed.
  ///
  /// Defaults to 0.0.
  final double startAngle;

  /// The angle in radians at which stop 1.0 of the gradient is placed.
  ///
  /// Defaults to math.pi * 2.
  final double endAngle;

  /// How this gradient should tile the plane beyond in the region before
  /// [startAngle] and after [endAngle].
  ///
  /// For details, see [TileMode].
  ///
  /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/tile_mode_clamp_sweep.png)
  /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/tile_mode_mirror_sweep.png)
  /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/tile_mode_repeated_sweep.png)
  final TileMode tileMode;

  @override
  Shader createShader(Rect rect, { TextDirection textDirection }) {
    return ui.Gradient.sweep(
      center.resolve(textDirection).withinRect(rect),
      colors, _impliedStops(), tileMode,
      startAngle,
      endAngle,
    );
  }

  /// Returns a new [SweepGradient] with its colors scaled by the given factor.
  ///
  /// Since the alpha component of the Color is what is scaled, a factor
  /// of 0.0 or less results in a gradient that is fully transparent.
  @override
  SweepGradient scale(double factor) {
    return SweepGradient(
      center: center,
      startAngle: startAngle,
      endAngle: endAngle,
      colors: colors.map<Color>((Color color) => Color.lerp(null, color, factor)).toList(),
      stops: stops,
      tileMode: tileMode,
    );
  }

  @override
  Gradient lerpFrom(Gradient a, double t) {
    if (a == null || (a is SweepGradient))
      return SweepGradient.lerp(a, this, t);
    return super.lerpFrom(a, t);
  }

  @override
  Gradient lerpTo(Gradient b, double t) {
    if (b == null || (b is SweepGradient))
      return SweepGradient.lerp(this, b, t);
    return super.lerpTo(b, t);
  }

  /// Linearly interpolate between two [SweepGradient]s.
  ///
  /// If either gradient is null, then the non-null gradient is returned with
  /// its color scaled in the same way as the [scale] function.
  ///
  /// If neither gradient is null, they must have the same number of [colors].
  ///
  /// The `t` argument represents a position on the timeline, with 0.0 meaning
  /// that the interpolation has not started, returning `a` (or something
  /// equivalent to `a`), 1.0 meaning that the interpolation has finished,
  /// returning `b` (or something equivalent to `b`), and values in between
  /// meaning that the interpolation is at the relevant point on the timeline
  /// between `a` and `b`. The interpolation can be extrapolated beyond 0.0 and
  /// 1.0, so negative values and values greater than 1.0 are valid (and can
  /// easily be generated by curves such as [Curves.elasticInOut]).
  ///
  /// Values for `t` are usually obtained from an [Animation<double>], such as
  /// an [AnimationController].
  static SweepGradient lerp(SweepGradient a, SweepGradient b, double t) {
    assert(t != null);
    if (a == null && b == null)
      return null;
    if (a == null)
      return b.scale(t);
    if (b == null)
      return a.scale(1.0 - t);
    final _ColorsAndStops interpolated = _interpolateColorsAndStops(
        a.colors,
        a._impliedStops(),
        b.colors,
        b._impliedStops(),
        t,
    );
    return SweepGradient(
      center: AlignmentGeometry.lerp(a.center, b.center, t),
      startAngle: math.max(0.0, ui.lerpDouble(a.startAngle, b.startAngle, t)),
      endAngle: math.max(0.0, ui.lerpDouble(a.endAngle, b.endAngle, t)),
      colors: interpolated.colors,
      stops: interpolated.stops,
      tileMode: t < 0.5 ? a.tileMode : b.tileMode, // TODO(ianh): interpolate tile mode
    );
  }

  @override
  bool operator ==(dynamic other) {
    if (identical(this, other))
      return true;
    if (runtimeType != other.runtimeType)
      return false;
    final SweepGradient typedOther = other;
    if (center != typedOther.center ||
        startAngle != typedOther.startAngle ||
        endAngle != typedOther.endAngle ||
        tileMode != typedOther.tileMode ||
        colors?.length != typedOther.colors?.length ||
        stops?.length != typedOther.stops?.length)
      return false;
    if (colors != null) {
      assert(typedOther.colors != null);
      assert(colors.length == typedOther.colors.length);
      for (int i = 0; i < colors.length; i += 1) {
        if (colors[i] != typedOther.colors[i])
          return false;
      }
    }
    if (stops != null) {
      assert(typedOther.stops != null);
      assert(stops.length == typedOther.stops.length);
      for (int i = 0; i < stops.length; i += 1) {
        if (stops[i] != typedOther.stops[i])
          return false;
      }
    }
    return true;
  }

  @override
  int get hashCode => hashValues(center, startAngle, endAngle, tileMode, hashList(colors), hashList(stops));

  @override
  String toString() {
    return '$runtimeType($center, $startAngle, $endAngle, $colors, $stops, $tileMode)';
  }
}
