// 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:async';
import 'dart:math' as math;
import 'dart:sky' as sky;
import 'dart:sky' show Point, Offset, Size, Rect, Color, Paint, Path;

import 'package:sky/base/lerp.dart';
import 'package:sky/painting/shadows.dart';

class BorderSide {
  const BorderSide({
    this.color: const Color(0xFF000000),
    this.width: 1.0
  });
  final Color color;
  final double width;

  static const none = const BorderSide(width: 0.0);

  int get hashCode {
    int value = 373;
    value = 37 * value * color.hashCode;
    value = 37 * value * width.hashCode;
    return value;
  }
  String toString() => 'BorderSide($color, $width)';
}

class Border {
  const Border({
    this.top: BorderSide.none,
    this.right: BorderSide.none,
    this.bottom: BorderSide.none,
    this.left: BorderSide.none
  });

  const Border.all(BorderSide side) :
    top = side,
    right = side,
    bottom = side,
    left = side;

  final BorderSide top;
  final BorderSide right;
  final BorderSide bottom;
  final BorderSide 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)';
}

class BoxShadow {
  const BoxShadow({
    this.color,
    this.offset,
    this.blur
  });

  final Color color;
  final Offset offset;
  final double blur;

  BoxShadow scale(double factor) {
    return new BoxShadow(
      color: color,
      offset: offset * factor,
      blur: blur * factor
    );
  }

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

BoxShadow lerpBoxShadow(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: lerpColor(a.color, b.color, t),
      offset: lerpOffset(a.offset, b.offset, t),
      blur: lerpNum(a.blur, b.blur, t));
}

List<BoxShadow> lerpListBoxShadow(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(lerpBoxShadow(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;
}

abstract class Gradient {
  sky.Shader createShader();
}

class LinearGradient extends Gradient {
  LinearGradient({
    this.endPoints,
    this.colors,
    this.colorStops,
    this.tileMode: sky.TileMode.clamp
  });

  String toString() =>
      'LinearGradient($endPoints, $colors, $colorStops, $tileMode)';

  sky.Shader createShader() {
    return new sky.Gradient.linear(this.endPoints, this.colors, this.colorStops,
                                   this.tileMode);
  }

  final List<Point> endPoints;
  final List<Color> colors;
  final List<double> colorStops;
  final sky.TileMode tileMode;
}

class RadialGradient extends Gradient {
  RadialGradient({
    this.center,
    this.radius,
    this.colors,
    this.colorStops,
    this.tileMode: sky.TileMode.clamp
  });

  String toString() =>
      'RadialGradient($center, $radius, $colors, $colorStops, $tileMode)';

  sky.Shader createShader() {
    return new sky.Gradient.radial(this.center, this.radius, this.colors,
                                   this.colorStops, this.tileMode);
  }

  final Point center;
  final double radius;
  final List<Color> colors;
  final List<double> colorStops;
  final sky.TileMode tileMode;
}

enum BackgroundFit { fill, contain, cover, none, scaleDown }

enum BackgroundRepeat { repeat, repeatX, repeatY, noRepeat }

// TODO(jackson): We should abstract this out into a separate class
// that handles the image caching and so forth, which has callbacks
// for "size changed" and "image changed". This would also enable us
// to do animated images.

class BackgroundImage {
  final BackgroundFit fit;
  final BackgroundRepeat repeat;
  BackgroundImage({
    Future<sky.Image> image,
    this.fit: BackgroundFit.scaleDown,
    this.repeat: BackgroundRepeat.noRepeat
  }) {
    image.then((resolvedImage) {
      if (resolvedImage == null)
        return;
      _image = resolvedImage;
      _size = new Size(resolvedImage.width.toDouble(), resolvedImage.height.toDouble());
      for (Function listener in _listeners) {
        listener();
      }
    });
  }

  sky.Image _image;
  sky.Image get image => _image;

  Size _size;

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

  void addChangeListener(Function listener) {
    _listeners.add(listener);
  }

  void removeChangeListener(Function listener) {
    _listeners.remove(listener);
  }

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

enum Shape { rectangle, circle }

// This must be immutable, because we won't notice when it changes
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
  });

  final Color backgroundColor;
  final BackgroundImage backgroundImage;
  final double borderRadius;
  final Border border;
  final List<BoxShadow> boxShadow;
  final Gradient gradient;
  final Shape shape;

  BoxDecoration scale(double factor) {
    // TODO(abarth): Scale ALL the things.
    return new BoxDecoration(
      backgroundColor: lerpColor(null, backgroundColor, factor),
      backgroundImage: backgroundImage,
      border: border,
      borderRadius: lerpNum(null, borderRadius, factor),
      boxShadow: lerpListBoxShadow(null, boxShadow, factor),
      gradient: gradient,
      shape: shape
    );
  }

  String toString([String prefix = '']) {
    List<String> result = [];
    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((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');
  }
}

BoxDecoration lerpBoxDecoration(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: lerpColor(a.backgroundColor, b.backgroundColor, t),
    backgroundImage: b.backgroundImage,
    border: b.border,
    borderRadius: lerpNum(a.borderRadius, b.borderRadius, t),
    boxShadow: lerpListBoxShadow(a.boxShadow, b.boxShadow, t),
    gradient: b.gradient,
    shape: b.shape
  );
}

class BoxPainter {
  BoxPainter(BoxDecoration decoration) : _decoration = decoration {
    assert(decoration != null);
  }

  BoxDecoration _decoration;
  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.setDrawLooper(builder.build());
      }

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

      _cachedBackgroundPaint = paint;
    }

    return _cachedBackgroundPaint;
  }

  void _paintBackgroundColor(sky.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
            canvas.drawRRect(new sky.RRect()..setRectXY(rect, _decoration.borderRadius, _decoration.borderRadius), _backgroundPaint);
          break;
      }
    }
  }

  void _paintBackgroundImage(sky.Canvas canvas, Rect rect) {
    if (_decoration.backgroundImage == null)
      return;
    sky.Image image = _decoration.backgroundImage.image;
    if (image != null) {
      Size bounds = rect.size;
      Size imageSize = _decoration.backgroundImage._size;
      Size src;
      Size dst;
      switch(_decoration.backgroundImage.fit) {
        case BackgroundFit.fill:
          src = imageSize;
          dst = bounds;
          break;
        case BackgroundFit.contain:
          src = imageSize;
          if (bounds.width / bounds.height > src.width / src.height) {
            dst = new Size(bounds.width, src.height * bounds.width / src.width);
          } else {
            dst = new Size(src.width * bounds.height / src.height, bounds.height);
          }
          break;
        case BackgroundFit.cover:
          if (bounds.width / bounds.height > imageSize.width / imageSize.height) {
            src = new Size(imageSize.width, imageSize.width * bounds.height / bounds.width);
          } else {
            src = new Size(imageSize.height * bounds.width / bounds.height, imageSize.height);
          }
          dst = bounds;
          break;
        case BackgroundFit.none:
          src = new Size(math.min(imageSize.width, bounds.width),
                         math.min(imageSize.height, bounds.height));
          dst = src;
          break;
        case BackgroundFit.scaleDown:
          src = imageSize;
          dst = bounds;
          if (src.height > dst.height) {
            dst = new Size(src.width * dst.height / src.height, src.height);
          }
          if (src.width > dst.width) {
            dst = new Size(dst.width, src.height * dst.width / src.width);
          }
          break;
      }
      canvas.drawImageRect(image, Point.origin & src, rect.topLeft & dst, new Paint());
    }
  }

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

    assert(_decoration.borderRadius == null); // TODO(abarth): Implement borders with border radius.
    assert(_decoration.shape == Shape.rectangle); // TODO(ianh): Implement 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 paint(sky.Canvas canvas, Rect rect) {
    _paintBackgroundColor(canvas, rect);
    _paintBackgroundImage(canvas, rect);
    _paintBorder(canvas, rect);
  }
}
