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

import 'package:flutter/services.dart';

import 'basic_types.dart';
import 'shadows.dart';

/// An immutable set of offsets in each of the four cardinal directions.
///
/// Typically used for an offset from each of the four sides of a box. For
/// example, the padding inside a box can be represented using this class.
class EdgeDims {
  /// Constructs an EdgeDims from offsets from the top, right, bottom and left.
  const EdgeDims.TRBL(this.top, this.right, this.bottom, this.left);

  /// Constructs an EdgeDims where all the offsets are value.
  const EdgeDims.all(double value)
      : top = value, right = value, bottom = value, left = value;

  /// Constructs an EdgeDims with only the given values non-zero.
  const EdgeDims.only({ this.top: 0.0,
                        this.right: 0.0,
                        this.bottom: 0.0,
                        this.left: 0.0 });

  /// Constructs an EdgeDims with symmetrical vertical and horizontal offsets.
  const EdgeDims.symmetric({ double vertical: 0.0,
                             double horizontal: 0.0 })
    : top = vertical, left = horizontal, bottom = vertical, right = horizontal;

  /// The offset from the top.
  final double top;

  /// The offset from the right.
  final double right;

  /// The offset from the bottom.
  final double bottom;

  /// The offset from the left.
  final double left;

  /// Whether every dimension is non-negative.
  bool get isNonNegative => top >= 0.0 && right >= 0.0 && bottom >= 0.0 && left >= 0.0;

  /// The size that this edge dims would occupy with an empty interior.
  Size get collapsedSize => new Size(left + right, top + bottom);

  Rect inflateRect(Rect rect) {
    return new Rect.fromLTRB(rect.left - left, rect.top - top, rect.right + right, rect.bottom + bottom);
  }

  EdgeDims operator -(EdgeDims other) {
    return new EdgeDims.TRBL(
      top - other.top,
      right - other.right,
      bottom - other.bottom,
      left - other.left
    );
  }

  EdgeDims operator +(EdgeDims other) {
    return new EdgeDims.TRBL(
      top + other.top,
      right + other.right,
      bottom + other.bottom,
      left + other.left
    );
  }

  EdgeDims operator *(double other) {
    return new EdgeDims.TRBL(
      top * other,
      right * other,
      bottom * other,
      left * other
    );
  }

  EdgeDims operator /(double other) {
    return new EdgeDims.TRBL(
      top / other,
      right / other,
      bottom / other,
      left / other
    );
  }

  EdgeDims operator ~/(double other) {
    return new EdgeDims.TRBL(
      (top ~/ other).toDouble(),
      (right ~/ other).toDouble(),
      (bottom ~/ other).toDouble(),
      (left ~/ other).toDouble()
    );
  }

  EdgeDims operator %(double other) {
    return new EdgeDims.TRBL(
      top % other,
      right % other,
      bottom % other,
      left % other
    );
  }

  /// Linearly interpolate between two EdgeDims.
  ///
  /// If either is null, this function interpolates from [EdgeDims.zero].
  static EdgeDims lerp(EdgeDims a, EdgeDims b, double t) {
    if (a == null && b == null)
      return null;
    if (a == null)
      return b * t;
    if (b == null)
      return a * (1.0 - t);
    return new EdgeDims.TRBL(
      ui.lerpDouble(a.top, b.top, t),
      ui.lerpDouble(a.right, b.right, t),
      ui.lerpDouble(a.bottom, b.bottom, t),
      ui.lerpDouble(a.left, b.left, t)
    );
  }

  /// An EdgeDims with zero offsets in each direction.
  static const EdgeDims zero = const EdgeDims.TRBL(0.0, 0.0, 0.0, 0.0);

  bool operator ==(dynamic other) {
    if (identical(this, other))
      return true;
    if (other is! EdgeDims)
      return false;
    final EdgeDims typedOther = other;
    return top == typedOther.top &&
           right == typedOther.right &&
           bottom == typedOther.bottom &&
           left == typedOther.left;
  }

  int get hashCode {
    int value = 373;
    value = 37 * value + top.hashCode;
    value = 37 * value + left.hashCode;
    value = 37 * value + bottom.hashCode;
    value = 37 * value + right.hashCode;
    return value;
  }

  String toString() => "EdgeDims($top, $right, $bottom, $left)";
}

/// A side of a border of a box
class BorderSide {
  const BorderSide({
    this.color: const Color(0xFF000000),
    this.width: 1.0
  });

  /// The color of this side of the border
  final Color color;

  /// The width of this side of the border
  final double width;

  /// A black border side of zero width
  static const none = const BorderSide(width: 0.0);

  bool operator ==(dynamic other) {
    if (identical(this, other))
      return true;
    if (other is! BorderSide)
      return false;
    final BorderSide typedOther = other;
    return color == typedOther.color &&
           width == typedOther.width;
  }

  int get hashCode {
    int value = 373;
    value = 37 * value + color.hashCode;
    value = 37 * value + width.hashCode;
    return value;
  }

  String toString() => 'BorderSide($color, $width)';
}

/// A border of a box, comprised of four sides
class Border {
  const Border({
    this.top: BorderSide.none,
    this.right: BorderSide.none,
    this.bottom: BorderSide.none,
    this.left: BorderSide.none
  });

  /// A uniform border with all sides the same color and width
  factory Border.all({
    Color color: const Color(0xFF000000),
    double width: 1.0
  }) {
    BorderSide side = new BorderSide(color: color, width: width);
    return new Border(top: side, right: side, bottom: side, left: side);
  }

  /// The top side of this border
  final BorderSide top;

  /// The right side of this border
  final BorderSide right;

  /// The bottom side of this border
  final BorderSide bottom;

  /// The left side of this border
  final BorderSide left;

  /// The widths of the sides of this border represented as an EdgeDims
  EdgeDims get dimensions {
    return new EdgeDims.TRBL(top.width, right.width, bottom.width, left.width);
  }

  bool operator ==(dynamic other) {
    if (identical(this, other))
      return true;
    if (other is! Border)
      return false;
    final Border typedOther = other;
    return top == typedOther.top &&
           right == typedOther.right &&
           bottom == typedOther.bottom &&
           left == typedOther.left;
  }

  int get hashCode {
    int value = 373;
    value = 37 * value + top.hashCode;
    value = 37 * value + right.hashCode;
    value = 37 * value + bottom.hashCode;
    value = 37 * value + left.hashCode;
    return value;
  }

  String toString() => 'Border($top, $right, $bottom, $left)';
}

/// A shadow cast by a box
///
/// Note: BoxShadow can cast non-rectangular shadows if the box is
/// non-rectangular (e.g., has a border radius or a circular shape).
class BoxShadow {
  const BoxShadow({
    this.color,
    this.offset,
    this.blur
  });

  /// The color of the shadow
  final Color color;

  /// The displacement of the shadow from the box
  final Offset offset;

  /// The standard deviation of the Gaussian to convolve with the box's shape
  final double blur;

  /// Returns a new box shadow with its offset and blur scaled by the given factor
  BoxShadow scale(double factor) {
    return new BoxShadow(
      color: color,
      offset: offset * factor,
      blur: blur * factor
    );
  }

  /// Linearly interpolate between two box shadows
  ///
  /// If either box shadow is null, this function linearly interpolates from a
  /// a box shadow that matches the other box shadow in color but has a zero
  /// offset and a zero blur.
  static BoxShadow lerp(BoxShadow a, BoxShadow b, double t) {
    if (a == null && b == null)
      return null;
    if (a == null)
      return b.scale(t);
    if (b == null)
      return a.scale(1.0 - t);
    return new BoxShadow(
      color: Color.lerp(a.color, b.color, t),
      offset: Offset.lerp(a.offset, b.offset, t),
      blur: ui.lerpDouble(a.blur, b.blur, t)
    );
  }

  /// Linearly interpolate between two lists of box shadows
  ///
  /// If the lists differ in length, excess items are lerped with null.
  static List<BoxShadow> lerpList(List<BoxShadow> a, List<BoxShadow> b, double t) {
    if (a == null && b == null)
      return null;
    if (a == null)
      a = new List<BoxShadow>();
    if (b == null)
      b = new List<BoxShadow>();
    List<BoxShadow> result = new List<BoxShadow>();
    int commonLength = math.min(a.length, b.length);
    for (int i = 0; i < commonLength; ++i)
      result.add(BoxShadow.lerp(a[i], b[i], t));
    for (int i = commonLength; i < a.length; ++i)
      result.add(a[i].scale(1.0 - t));
    for (int i = commonLength; i < b.length; ++i)
      result.add(b[i].scale(t));
    return result;
  }

  bool operator ==(dynamic other) {
    if (identical(this, other))
      return true;
    if (other is! BoxShadow)
      return false;
    final BoxShadow typedOther = other;
    return color == typedOther.color &&
           offset == typedOther.offset &&
           blur == typedOther.blur;
  }

  int get hashCode {
    int value = 373;
    value = 37 * value + color.hashCode;
    value = 37 * value + offset.hashCode;
    value = 37 * value + blur.hashCode;
    return value;
  }

  String toString() => 'BoxShadow($color, $offset, $blur)';
}

/// A 2D gradient
abstract class Gradient {
  const Gradient();
  ui.Shader createShader();
}

/// A 2D linear gradient
class LinearGradient extends Gradient {
  const LinearGradient({
    this.begin,
    this.end,
    this.colors,
    this.stops,
    this.tileMode: ui.TileMode.clamp
  });

  /// The point at which stop 0.0 of the gradient is placed
  final Point begin;

  /// The point at which stop 1.0 of the gradient is placed
  final Point end;

  /// The colors the gradient should obtain at each of the stops
  ///
  /// Note: This list must have the same length as [stops].
  final List<Color> colors;

  /// A list of values from 0.0 to 1.0 that denote fractions of the vector from start to end
  ///
  /// Note: If specified, this list must have the same length as [colors]. Otherwise the colors
  /// are distributed evenly between [begin] and [end].
  final List<double> stops;

  /// How this gradient should tile the plane
  final ui.TileMode tileMode;

  ui.Shader createShader() {
    return new ui.Gradient.linear(<Point>[begin, end], this.colors, this.stops, this.tileMode);
  }

  bool operator ==(dynamic other) {
    if (identical(this, other))
      return true;
    if (other is! LinearGradient)
      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;
  }

  int get hashCode {
    int value = 373;
    value = 37 * value + begin.hashCode;
    value = 37 * value + end.hashCode;
    value = 37 * value + tileMode.hashCode;
    if (colors != null) {
      for (int i = 0; i < colors.length; i += 1)
        value = 37 * value + colors[i].hashCode;
    } else {
      value = 37 * value + null.hashCode;
    }
    if (stops != null) {
      for (int i = 0; i < stops.length; i += 1)
        value = 37 * value + stops[i].hashCode;
    } else {
      value = 37 * value + null.hashCode;
    }
    return value;
  }

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

/// A 2D radial gradient
class RadialGradient extends Gradient {
  const RadialGradient({
    this.center,
    this.radius,
    this.colors,
    this.stops,
    this.tileMode: ui.TileMode.clamp
  });

  /// The center of the gradient
  final Point center;

  /// The radius at which stop 1.0 is placed
  final double radius;

  /// The colors the gradient should obtain at each of the stops
  ///
  /// Note: This list must have the same length as [stops].
  final List<Color> colors;

  /// A list of values from 0.0 to 1.0 that denote concentric rings
  ///
  /// The rings are centered at [center] and have a radius equal to the value of
  /// the stop times [radius].
  ///
  /// Note: This list must have the same length as [colors].
  final List<double> stops;

  /// How this gradient should tile the plane
  final ui.TileMode tileMode;

  ui.Shader createShader() {
    return new ui.Gradient.radial(center, radius, colors, stops, tileMode);
  }

  bool operator ==(dynamic other) {
    if (identical(this, other))
      return true;
    if (other is! RadialGradient)
      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)
      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;
  }

  int get hashCode {
    int value = 373;
    value = 37 * value + center.hashCode;
    value = 37 * value + radius.hashCode;
    value = 37 * value + tileMode.hashCode;
    if (colors != null) {
      for (int i = 0; i < colors.length; i += 1)
        value = 37 * value + colors[i].hashCode;
    } else {
      value = 37 * value + null.hashCode;
    }
    if (stops != null) {
      for (int i = 0; i < stops.length; i += 1)
        value = 37 * value + stops[i].hashCode;
    } else {
      value = 37 * value + null.hashCode;
    }
    return value;
  }

  String toString() {
    return 'RadialGradient($center, $radius, $colors, $stops, $tileMode)';
  }
}

/// How an image should be inscribed into a box
enum ImageFit {
  /// Fill the box by distorting the image's aspect ratio
  fill,

  /// As large as possible while still containing the image entirely within the box
  contain,

  /// As small as possible while still covering the entire box
  cover,

  /// Center the image within the box and discard any portions of the image that
  /// lie outside the box
  none,

  /// Center the image within the box and, if necessary, scale the image down to
  /// ensure that the image fits within the box
  scaleDown
}

/// How to paint any portions of a box not covered by an image
enum ImageRepeat {
  /// Repeat the image in both the x and y directions until the box is filled
  repeat,

  /// Repeat the image in the x direction until the box is filled horizontally
  repeatX,

  /// Repeat the image in the y direction until the box is filled vertically
  repeatY,

  /// Leave uncovered poritions of the box transparent
  noRepeat
}

/// Paint an image into the given rectangle in the canvas
void paintImage({
  Canvas canvas,
  Rect rect,
  ui.Image image,
  ColorFilter colorFilter,
  ImageFit fit,
  repeat: ImageRepeat.noRepeat,
  Rect centerSlice,
  double positionX: 0.5,
  double positionY: 0.5
}) {
  Size outputSize = rect.size;
  Size inputSize = new Size(image.width.toDouble(), image.height.toDouble());
  Offset sliceBorder;
  if (centerSlice != null) {
    sliceBorder = new Offset(
      centerSlice.left + inputSize.width - centerSlice.right,
      centerSlice.top + inputSize.height - centerSlice.bottom
    );
    outputSize -= sliceBorder;
    inputSize -= sliceBorder;
  }
  Size sourceSize;
  Size destinationSize;
  fit ??= centerSlice == null ? ImageFit.scaleDown : ImageFit.fill;
  assert(centerSlice == null || (fit != ImageFit.none && fit != ImageFit.cover));
  switch (fit) {
    case ImageFit.fill:
      sourceSize = inputSize;
      destinationSize = outputSize;
      break;
    case ImageFit.contain:
      sourceSize = inputSize;
      if (outputSize.width / outputSize.height > sourceSize.width / sourceSize.height)
        destinationSize = new Size(sourceSize.width * outputSize.height / sourceSize.height, outputSize.height);
      else
        destinationSize = new Size(outputSize.width, sourceSize.height * outputSize.width / sourceSize.width);
      break;
    case ImageFit.cover:
      if (outputSize.width / outputSize.height > inputSize.width / inputSize.height)
        sourceSize = new Size(inputSize.width, inputSize.width * outputSize.height / outputSize.width);
      else
        sourceSize = new Size(inputSize.height * outputSize.width / outputSize.height, inputSize.height);
      destinationSize = outputSize;
      break;
    case ImageFit.none:
      sourceSize = new Size(math.min(inputSize.width, outputSize.width),
                            math.min(inputSize.height, outputSize.height));
      destinationSize = sourceSize;
      break;
    case ImageFit.scaleDown:
      sourceSize = inputSize;
      destinationSize = outputSize;
      if (sourceSize.height > destinationSize.height)
        destinationSize = new Size(sourceSize.width * destinationSize.height / sourceSize.height, sourceSize.height);
      if (sourceSize.width > destinationSize.width)
        destinationSize = new Size(destinationSize.width, sourceSize.height * destinationSize.width / sourceSize.width);
      break;
  }
  if (centerSlice != null) {
    outputSize += sliceBorder;
    destinationSize += sliceBorder;
    // We don't have the ability to draw a subset of the image at the same time
    // as we apply a nine-patch stretch.
    assert(sourceSize == inputSize);
  }
  // TODO(abarth): Implement |repeat|.
  Paint paint = new Paint()..isAntiAlias = false;
  if (colorFilter != null)
    paint.colorFilter = colorFilter;
  double dx = (outputSize.width - destinationSize.width) * positionX;
  double dy = (outputSize.height - destinationSize.height) * positionY;
  Point destinationPosition = rect.topLeft + new Offset(dx, dy);
  Rect destinationRect = destinationPosition & destinationSize;
  if (centerSlice == null)
    canvas.drawImageRect(image, Point.origin & sourceSize, destinationRect, paint);
  else
    canvas.drawImageNine(image, centerSlice, destinationRect, paint);
}

/// A background image for a box.
class BackgroundImage {
  BackgroundImage({
    ImageResource image,
    this.fit,
    this.repeat: ImageRepeat.noRepeat,
    this.centerSlice,
    this.colorFilter
  }) : _imageResource = image;

  /// How the background image should be inscribed into the box.
  final ImageFit fit;

  /// How to paint any portions of the box not covered by the background image.
  final ImageRepeat repeat;

  /// The center slice for a nine-patch image.
  ///
  /// The region of the image inside the center slice will be stretched both
  /// horizontally and vertically to fit the image into its destination. The
  /// region of the image above and below the center slice will be stretched
  /// only horizontally and the region of the image to the left and right of
  /// the center slice will be stretched only vertically.
  final Rect centerSlice;

  /// A color filter to apply to the background image before painting it.
  final ColorFilter colorFilter;

  /// The image to be painted into the background.
  ui.Image get image => _image;
  ui.Image _image;

  final ImageResource _imageResource;

  final List<VoidCallback> _listeners =
    new List<VoidCallback>();

  /// Call listener when the background images changes (e.g., arrives from the network).
  void addChangeListener(VoidCallback listener) {
    // We add the listener to the _imageResource first so that the first change
    // listener doesn't get callback synchronously if the image resource is
    // already resolved.
    if (_listeners.isEmpty)
      _imageResource.addListener(_handleImageChanged);
    _listeners.add(listener);
  }

  /// No longer call listener when the background image changes.
  void removeChangeListener(VoidCallback listener) {
    _listeners.remove(listener);
    // We need to remove ourselves as listeners from the _imageResource so that
    // we're not kept alive by the image_cache.
    if (_listeners.isEmpty)
      _imageResource.removeListener(_handleImageChanged);
  }

  void _handleImageChanged(ui.Image resolvedImage) {
    if (resolvedImage == null)
      return;
    _image = resolvedImage;
    final List<VoidCallback> localListeners =
      new List<VoidCallback>.from(_listeners);
    for (VoidCallback listener in localListeners)
      listener();
  }

  bool operator ==(dynamic other) {
    if (identical(this, other))
      return true;
    if (other is! BackgroundImage)
      return false;
    final BackgroundImage typedOther = other;
    return fit == typedOther.fit &&
           repeat == typedOther.repeat &&
           centerSlice == typedOther.centerSlice &&
           colorFilter == typedOther.colorFilter &&
           _imageResource == typedOther._imageResource;
  }

  int get hashCode {
    int value = 373;
    value = 37 * value + fit.hashCode;
    value = 37 * value + repeat.hashCode;
    value = 37 * value + centerSlice.hashCode;
    value = 37 * value + colorFilter.hashCode;
    value = 37 * value + _imageResource.hashCode;
    return value;
  }

  String toString() => 'BackgroundImage($fit, $repeat)';
}

// TODO(abarth): Rename to BoxShape?
/// A 2D geometrical shape
enum Shape {
  /// An axis-aligned, 2D rectangle
  rectangle,

  /// A 2D locus of points equidistant from a single point
  circle
}

/// An immutable description of how to paint a box
class BoxDecoration {
  const BoxDecoration({
    this.backgroundColor, // null = don't draw background color
    this.backgroundImage, // null = don't draw background image
    this.border, // null = don't draw border
    this.borderRadius, // null = use more efficient background drawing; note that this must be null for circles
    this.boxShadow, // null = don't draw shadows
    this.gradient, // null = don't allocate gradient objects
    this.shape: Shape.rectangle
  });

  /// The color to fill in the background of the box
  ///
  /// The color is filled into the shape of the box (e.g., either a rectangle,
  /// potentially with a border radius, or a circle).
  final Color backgroundColor;

  /// An image to paint above the background color
  final BackgroundImage backgroundImage;

  /// A border to draw above the background
  final Border border;

  /// If non-null, the corners of this box are rounded by this radius
  ///
  /// Applies only to boxes with rectangular shapes.
  final double borderRadius;

  /// A list of shadows cast by this box behind the background
  final List<BoxShadow> boxShadow;

  /// A graident to use when filling the background
  final Gradient gradient;

  /// The shape to fill the background color into and to cast as a shadow
  final Shape shape;

  /// Returns a new box decoration that is scalled by the given factor
  BoxDecoration scale(double factor) {
    // TODO(abarth): Scale ALL the things.
    return new BoxDecoration(
      backgroundColor: Color.lerp(null, backgroundColor, factor),
      backgroundImage: backgroundImage,
      border: border,
      borderRadius: ui.lerpDouble(null, borderRadius, factor),
      boxShadow: BoxShadow.lerpList(null, boxShadow, factor),
      gradient: gradient,
      shape: shape
    );
  }

  /// Linearly interpolate between two box decorations
  ///
  /// Interpolates each parameter of the box decoration separately.
  static BoxDecoration lerp(BoxDecoration a, BoxDecoration b, double t) {
    if (a == null && b == null)
      return null;
    if (a == null)
      return b.scale(t);
    if (b == null)
      return a.scale(1.0 - t);
    // TODO(abarth): lerp ALL the fields.
    return new BoxDecoration(
      backgroundColor: Color.lerp(a.backgroundColor, b.backgroundColor, t),
      backgroundImage: b.backgroundImage,
      border: b.border,
      borderRadius: ui.lerpDouble(a.borderRadius, b.borderRadius, t),
      boxShadow: BoxShadow.lerpList(a.boxShadow, b.boxShadow, t),
      gradient: b.gradient,
      shape: b.shape
    );
  }

  bool operator ==(dynamic other) {
    if (identical(this, other))
      return true;
    if (other is! BoxDecoration)
      return false;
    final BoxDecoration typedOther = other;
    return backgroundColor == typedOther.backgroundColor &&
           backgroundImage == typedOther.backgroundImage &&
           border == typedOther.border &&
           borderRadius == typedOther.borderRadius &&
           boxShadow == typedOther.boxShadow &&
           gradient == typedOther.gradient &&
           shape == typedOther.shape;
  }

  int get hashCode {
    int value = 373;
    value = 37 * value + backgroundColor.hashCode;
    value = 37 * value + backgroundImage.hashCode;
    value = 37 * value + border.hashCode;
    value = 37 * value + borderRadius.hashCode;
    value = 37 * value + boxShadow.hashCode;
    value = 37 * value + gradient.hashCode;
    value = 37 * value + shape.hashCode;
    return value;
  }

  String toString([String prefix = '']) {
    List<String> result = <String>[];
    if (backgroundColor != null)
      result.add('${prefix}backgroundColor: $backgroundColor');
    if (backgroundImage != null)
      result.add('${prefix}backgroundImage: $backgroundImage');
    if (border != null)
      result.add('${prefix}border: $border');
    if (borderRadius != null)
      result.add('${prefix}borderRadius: $borderRadius');
    if (boxShadow != null)
      result.add('${prefix}boxShadow: ${boxShadow.map((BoxShadow shadow) => shadow.toString())}');
    if (gradient != null)
      result.add('${prefix}gradient: $gradient');
    if (shape != Shape.rectangle)
      result.add('${prefix}shape: $shape');
    if (result.isEmpty)
      return '$prefix<no decorations specified>';
    return result.join('\n');
  }
}

/// An object that paints a [BoxDecoration] into a canvas
class BoxPainter {
  BoxPainter(BoxDecoration decoration) : _decoration = decoration {
    assert(decoration != null);
  }

  BoxDecoration _decoration;
  /// The box decoration to paint
  BoxDecoration get decoration => _decoration;
  void set decoration (BoxDecoration value) {
    assert(value != null);
    if (value == _decoration)
      return;
    _decoration = value;
    _cachedBackgroundPaint = null;
  }

  Paint _cachedBackgroundPaint;
  Paint get _backgroundPaint {
    if (_cachedBackgroundPaint == null) {
      Paint paint = new Paint();

      if (_decoration.backgroundColor != null)
        paint.color = _decoration.backgroundColor;

      if (_decoration.boxShadow != null) {
        var builder = new ShadowDrawLooperBuilder();
        for (BoxShadow boxShadow in _decoration.boxShadow)
          builder.addShadow(boxShadow.offset, boxShadow.color, boxShadow.blur);
        paint.drawLooper = builder.build();
      }

      if (_decoration.gradient != null)
        paint.shader = _decoration.gradient.createShader();

      _cachedBackgroundPaint = paint;
    }

    return _cachedBackgroundPaint;
  }

  bool get _hasUniformBorder {
    Color color = _decoration.border.top.color;
    bool hasUniformColor =
      _decoration.border.right.color == color &&
      _decoration.border.bottom.color == color &&
      _decoration.border.left.color == color;

    if (!hasUniformColor)
      return false;

    double width = _decoration.border.top.width;
    bool hasUniformWidth =
      _decoration.border.right.width == width &&
      _decoration.border.bottom.width == width &&
      _decoration.border.left.width == width;

    return hasUniformWidth;
  }

  double _getEffectiveBorderRadius(Rect rect) {
    double shortestSide = rect.shortestSide;
    // In principle, we should use shortestSide / 2.0, but we don't want to
    // run into floating point rounding errors. Instead, we just use
    // shortestSide and let ui.Canvas do any remaining clamping.
    return _decoration.borderRadius > shortestSide ? shortestSide : _decoration.borderRadius;
  }

  void _paintBackgroundColor(ui.Canvas canvas, Rect rect) {
    if (_decoration.backgroundColor != null ||
        _decoration.boxShadow != null ||
        _decoration.gradient != null) {
      switch (_decoration.shape) {
        case Shape.circle:
          assert(_decoration.borderRadius == null);
          Point center = rect.center;
          double radius = rect.shortestSide / 2.0;
          canvas.drawCircle(center, radius, _backgroundPaint);
          break;
        case Shape.rectangle:
          if (_decoration.borderRadius == null) {
            canvas.drawRect(rect, _backgroundPaint);
          } else {
            double radius = _getEffectiveBorderRadius(rect);
            canvas.drawRRect(new ui.RRect.fromRectXY(rect, radius, radius), _backgroundPaint);
          }
          break;
      }
    }
  }

  void _paintBackgroundImage(ui.Canvas canvas, Rect rect) {
    final BackgroundImage backgroundImage = _decoration.backgroundImage;
    if (backgroundImage == null)
      return;
    ui.Image image = backgroundImage.image;
    if (image == null)
      return;
    paintImage(
      canvas: canvas,
      rect: rect,
      image: image,
      colorFilter: backgroundImage.colorFilter,
      fit:  backgroundImage.fit,
      repeat: backgroundImage.repeat
    );
  }

  void _paintBorder(ui.Canvas canvas, Rect rect) {
    if (_decoration.border == null)
      return;

    if (_hasUniformBorder) {
      if (_decoration.borderRadius != null) {
        _paintBorderWithRadius(canvas, rect);
        return;
      }
      if (_decoration.shape == Shape.circle) {
        _paintBorderWithCircle(canvas, rect);
        return;
      }
    }

    assert(_decoration.borderRadius == null); // TODO(abarth): Support non-uniform rounded borders.
    assert(_decoration.shape == Shape.rectangle); // TODO(ianh): Support non-uniform borders on circles.

    assert(_decoration.border.top != null);
    assert(_decoration.border.right != null);
    assert(_decoration.border.bottom != null);
    assert(_decoration.border.left != null);

    Paint paint = new Paint();
    Path path;

    paint.color = _decoration.border.top.color;
    path = new Path();
    path.moveTo(rect.left, rect.top);
    path.lineTo(rect.left + _decoration.border.left.width, rect.top + _decoration.border.top.width);
    path.lineTo(rect.right - _decoration.border.right.width, rect.top + _decoration.border.top.width);
    path.lineTo(rect.right, rect.top);
    path.close();
    canvas.drawPath(path, paint);

    paint.color = _decoration.border.right.color;
    path = new Path();
    path.moveTo(rect.right, rect.top);
    path.lineTo(rect.right - _decoration.border.right.width, rect.top + _decoration.border.top.width);
    path.lineTo(rect.right - _decoration.border.right.width, rect.bottom - _decoration.border.bottom.width);
    path.lineTo(rect.right, rect.bottom);
    path.close();
    canvas.drawPath(path, paint);

    paint.color = _decoration.border.bottom.color;
    path = new Path();
    path.moveTo(rect.right, rect.bottom);
    path.lineTo(rect.right - _decoration.border.right.width, rect.bottom - _decoration.border.bottom.width);
    path.lineTo(rect.left + _decoration.border.left.width, rect.bottom - _decoration.border.bottom.width);
    path.lineTo(rect.left, rect.bottom);
    path.close();
    canvas.drawPath(path, paint);

    paint.color = _decoration.border.left.color;
    path = new Path();
    path.moveTo(rect.left, rect.bottom);
    path.lineTo(rect.left + _decoration.border.left.width, rect.bottom - _decoration.border.bottom.width);
    path.lineTo(rect.left + _decoration.border.left.width, rect.top + _decoration.border.top.width);
    path.lineTo(rect.left, rect.top);
    path.close();
    canvas.drawPath(path, paint);
  }

  void _paintBorderWithRadius(ui.Canvas canvas, Rect rect) {
    assert(_hasUniformBorder);
    assert(_decoration.shape == Shape.rectangle);
    Color color = _decoration.border.top.color;
    double width = _decoration.border.top.width;
    double radius = _getEffectiveBorderRadius(rect);

    ui.RRect outer = new ui.RRect.fromRectXY(rect, radius, radius);
    ui.RRect inner = new ui.RRect.fromRectXY(rect.deflate(width), radius - width, radius - width);
    canvas.drawDRRect(outer, inner, new Paint()..color = color);
  }

  void _paintBorderWithCircle(ui.Canvas canvas, Rect rect) {
    assert(_hasUniformBorder);
    assert(_decoration.shape == Shape.circle);
    assert(_decoration.borderRadius == null);
    double width = _decoration.border.top.width;
    if (width <= 0.0) {
      return;
    }
    Paint paint = new Paint()
      ..color = _decoration.border.top.color
      ..strokeWidth = width
      ..style = ui.PaintingStyle.stroke;
    Point center = rect.center;
    double radius = (rect.shortestSide - width) / 2.0;
    canvas.drawCircle(center, radius, paint);
  }

  /// Paint the box decoration into the given location on the given canvas
  void paint(ui.Canvas canvas, Rect rect) {
    _paintBackgroundColor(canvas, rect);
    _paintBackgroundImage(canvas, rect);
    _paintBorder(canvas, rect);
  }
}
