// Copyright 2016 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:cassowary/cassowary.dart' as al; // "auto layout"

import 'box.dart';
import 'object.dart';

/// Hosts the edge parameters and vends useful methods to construct expressions
/// for constraints. Also sets up and manages implicit constraints and edit
/// variables.
class AutoLayoutRect {
  AutoLayoutRect() {
    _left = new al.Param();
    _right = new al.Param();
    _top = new al.Param();
    _bottom = new al.Param();
  }

  al.Param _left;
  al.Param _right;
  al.Param _top;
  al.Param _bottom;

  al.Param get left => _left;
  al.Param get right => _right;
  al.Param get top => _top;
  al.Param get bottom => _bottom;

  al.Expression get width => _right - _left;
  al.Expression get height => _bottom - _top;

  al.Expression get horizontalCenter => (_left + _right) / al.cm(2.0);
  al.Expression get verticalCenter => (_top + _bottom) / al.cm(2.0);

  List<al.Constraint> contains(AutoLayoutRect other) {
    return <al.Constraint>[
      other.left >= left,
      other.right <= right,
      other.top >= top,
      other.bottom <= bottom,
    ];
  }
}

class AutoLayoutParentData extends ContainerBoxParentDataMixin<RenderBox> {
  AutoLayoutParentData(this._renderBox);

  final RenderBox _renderBox;

  AutoLayoutRect get rect => _rect;
  AutoLayoutRect _rect;
  void set rect(AutoLayoutRect value) {
    if (_rect == value)
      return;
    if (_rect != null)
      _removeImplicitConstraints();
    _rect = value;
    if (_rect != null)
      _addImplicitConstraints();
  }

  BoxConstraints get _constraintsFromSolver {
    return new BoxConstraints.tightFor(
      width: _rect._right.value - _rect._left.value,
      height: _rect._bottom.value - _rect._top.value
    );
  }

  Offset get _offsetFromSolver {
    return new Offset(_rect._left.value, _rect._top.value);
  }

  List<al.Constraint> _implicitConstraints;

  void _addImplicitConstraints() {
    assert(_renderBox != null);
    if (_renderBox.parent == null || _rect == null)
      return;
    final List<al.Constraint> implicit = _constructImplicitConstraints();
    assert(implicit != null && implicit.isNotEmpty);
    assert(_renderBox.parent is RenderAutoLayout);
    final RenderAutoLayout parent = _renderBox.parent;
    final al.Result result = parent._solver.addConstraints(implicit);
    assert(result == al.Result.success);
    parent.markNeedsLayout();
    _implicitConstraints = implicit;
  }

  void _removeImplicitConstraints() {
    assert(_renderBox != null);
    if (_renderBox.parent == null || _implicitConstraints == null || _implicitConstraints.isEmpty)
      return;
    assert(_renderBox.parent is RenderAutoLayout);
    final RenderAutoLayout parent = _renderBox.parent;
    final al.Result result = parent._solver.removeConstraints(_implicitConstraints);
    assert(result == al.Result.success);
    parent.markNeedsLayout();
    _implicitConstraints = null;
  }

  /// Returns the set of implicit constraints that need to be applied to all
  /// instances of this class when they are moved into a render object with an
  /// active solver. If no implicit constraints needs to be applied, the object
  /// may return null.
  List<al.Constraint> _constructImplicitConstraints() {
    return <al.Constraint>[
      _rect._left >= al.cm(0.0), // The left edge must be positive.
      _rect._right >= _rect._left, // Width must be positive.
      // Why don't we need something similar for the top and the bottom?
    ];
  }
}

abstract class AutoLayoutDelegate {
  /// Abstract const constructor. This constructor enables subclasses to provide
  /// const constructors so that they can be used in const expressions.
  const AutoLayoutDelegate();

  List<al.Constraint> getConstraints(AutoLayoutRect parent);
  bool shouldUpdateConstraints(AutoLayoutDelegate oldDelegate);
}

class RenderAutoLayout extends RenderBox
    with ContainerRenderObjectMixin<RenderBox, AutoLayoutParentData>,
         RenderBoxContainerDefaultsMixin<RenderBox, AutoLayoutParentData> {

  RenderAutoLayout({
    AutoLayoutDelegate delegate,
    List<RenderBox> children
  }) : _delegate = delegate, _needToUpdateConstraints = (delegate != null) {
    _solver.addEditVariables(<al.Variable>[
        _rect._left.variable,
        _rect._right.variable,
        _rect._top.variable,
        _rect._bottom.variable
      ], al.Priority.required - 1);

    addAll(children);
  }

  AutoLayoutDelegate get delegate => _delegate;
  AutoLayoutDelegate _delegate;
  void set delegate(AutoLayoutDelegate newDelegate) {
    if (_delegate == newDelegate)
      return;
    AutoLayoutDelegate oldDelegate = _delegate;
    _delegate = newDelegate;
    if (newDelegate == null) {
      assert(oldDelegate != null);
      _needToUpdateConstraints = true;
      markNeedsLayout();
    } else if (oldDelegate == null ||
        newDelegate.runtimeType != oldDelegate.runtimeType ||
        newDelegate.shouldUpdateConstraints(oldDelegate)) {
      _needToUpdateConstraints = true;
      markNeedsLayout();
    }
  }

  bool _needToUpdateConstraints;

  final AutoLayoutRect _rect = new AutoLayoutRect();

  final al.Solver _solver = new al.Solver();
  final List<al.Constraint> _explicitConstraints = new List<al.Constraint>();

  void _setExplicitConstraints(List<al.Constraint> constraints) {
    assert(constraints != null);
    if (constraints.isEmpty)
      return;
    if (_solver.addConstraints(constraints) == al.Result.success)
      _explicitConstraints.addAll(constraints);
  }

  void _clearExplicitConstraints() {
    if (_explicitConstraints.isEmpty)
      return;
    if (_solver.removeConstraints(_explicitConstraints) == al.Result.success)
      _explicitConstraints.clear();
  }

  @override
  void adoptChild(RenderObject child) {
    // Make sure to call super first to setup the parent data
    super.adoptChild(child);
    final AutoLayoutParentData childParentData = child.parentData;
    childParentData._addImplicitConstraints();
    assert(child.parentData == childParentData);
  }

  @override
  void dropChild(RenderObject child) {
    final AutoLayoutParentData childParentData = child.parentData;
    childParentData._removeImplicitConstraints();
    assert(child.parentData == childParentData);
    super.dropChild(child);
  }

  @override
  void setupParentData(RenderObject child) {
    if (child.parentData is! AutoLayoutParentData)
      child.parentData = new AutoLayoutParentData(child);
  }

  @override
  bool get sizedByParent => true;

  @override
  void performResize() {
    size = constraints.biggest;
  }

  Size _previousSize;

  @override
  void performLayout() {
    bool needToFlushUpdates = false;

    if (_needToUpdateConstraints) {
      _clearExplicitConstraints();
      if (_delegate != null)
        _setExplicitConstraints(_delegate.getConstraints(_rect));
      _needToUpdateConstraints = false;
      needToFlushUpdates = true;
    }

    if (size != _previousSize) {
      _solver
        ..suggestValueForVariable(_rect._left.variable, 0.0)
        ..suggestValueForVariable(_rect._top.variable, 0.0)
        ..suggestValueForVariable(_rect._bottom.variable, size.height)
        ..suggestValueForVariable(_rect._right.variable, size.width);
      _previousSize = size;
      needToFlushUpdates = true;
    }

    if (needToFlushUpdates)
      _solver.flushUpdates();

    RenderBox child = firstChild;
    while (child != null) {
      final AutoLayoutParentData childParentData = child.parentData;
      child.layout(childParentData._constraintsFromSolver);
      childParentData.offset = childParentData._offsetFromSolver;
      assert(child.parentData == childParentData);
      child = childParentData.nextSibling;
    }
  }

  @override
  bool hitTestChildren(HitTestResult result, { Point position }) {
    return defaultHitTestChildren(result, position: position);
  }

  @override
  void paint(PaintingContext context, Offset offset) {
    defaultPaint(context, offset);
  }
}
