// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'package:cassowary/cassowary.dart' as al;
import 'package:sky/rendering/box.dart';
import 'package:sky/rendering/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. Used as a mixin by layout containers and parent data instances
/// of render boxes taking part in auto layout
abstract class _AutoLayoutParamMixin {
  // Ideally, the edges would all be final, but then they would have to be
  // initialized before the constructor. Not sure how to do that using a Mixin
  al.Param _leftEdge;
  al.Param _rightEdge;
  al.Param _topEdge;
  al.Param _bottomEdge;

  List<al.Constraint> _implicitConstraints;

  al.Param get leftEdge => _leftEdge;
  al.Param get rightEdge => _rightEdge;
  al.Param get topEdge => _topEdge;
  al.Param get bottomEdge => _bottomEdge;

  al.Expression get width => _rightEdge - _leftEdge;
  al.Expression get height => _bottomEdge - _topEdge;

  al.Expression get horizontalCenter => (_leftEdge + _rightEdge) / al.cm(2.0);
  al.Expression get verticalCenter => (_topEdge + _bottomEdge) / al.cm(2.0);

  void _setupLayoutParameters(dynamic context) {
    _leftEdge = new al.Param.withContext(context);
    _rightEdge = new al.Param.withContext(context);
    _topEdge = new al.Param.withContext(context);
    _bottomEdge = new al.Param.withContext(context);
  }

  void _setupEditVariablesInSolver(al.Solver solver, double priority) {
    solver.addEditVariables([
        _leftEdge.variable,
        _rightEdge.variable,
        _topEdge.variable,
        _bottomEdge.variable], priority);
  }

  void _applyEditsAtSize(al.Solver solver, Size size) {
    solver.suggestValueForVariable(_leftEdge.variable, 0.0);
    solver.suggestValueForVariable(_topEdge.variable, 0.0);
    solver.suggestValueForVariable(_bottomEdge.variable, size.height);
    solver.suggestValueForVariable(_rightEdge.variable, size.width);
  }

  /// Called when the solver has updated at least one of the layout parameters
  /// of this object. The object is now responsible for applying this update to
  /// it other properties (if necessary)
  void _applyAutolayoutParameterUpdates();

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

  void _setupImplicitConstraints(al.Solver solver) {
    List<al.Constraint> implicit = _constructImplicitConstraints();

    if (implicit == null || implicit.length == 0) {
      return;
    }

    al.Result result = solver.addConstraints(implicit);
    assert(result == al.Result.success);

    _implicitConstraints = implicit;
  }

  void _removeImplicitConstraints(al.Solver solver) {
    if (_implicitConstraints == null || _implicitConstraints.length == 0) {
      return;
    }

    al.Result result = solver.removeConstraints(_implicitConstraints);
    assert(result == al.Result.success);

    _implicitConstraints = null;
  }
}

class AutoLayoutParentData extends BoxParentData
    with ContainerParentDataMixin<RenderBox>, _AutoLayoutParamMixin {

  AutoLayoutParentData(this._renderBox) {
    _setupLayoutParameters(this);
  }

  final RenderBox _renderBox;

  void _applyAutolayoutParameterUpdates() {
    // This is called by the parent's layout function
    // to lay our box out.
    assert(_renderBox.parentData == this);
    assert(_renderBox.parent is RenderAutoLayout);
    assert((_renderBox.parent as RenderAutoLayout).debugDoingThisLayout); // TODO(ianh): Remove cast once the analyzer is cleverer
    BoxConstraints size = new BoxConstraints.tightFor(
      width: _rightEdge.value - _leftEdge.value,
      height: _bottomEdge.value - _topEdge.value
    );
    _renderBox.layout(size);
    position = new Point(_leftEdge.value, _topEdge.value);
  }

  List<al.Constraint> _constructImplicitConstraints() {
    return [
      _leftEdge >= al.cm(0.0), // The left edge must be positive.
      _rightEdge >= _leftEdge, // Width must be positive.
    ];
  }

}

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

  RenderAutoLayout({ List<RenderBox> children }) {
    _setupLayoutParameters(this);
    _setupEditVariablesInSolver(_solver, al.Priority.required - 1);
    addAll(children);
  }

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

  /// Adds all the given constraints to the solver. Either all constraints are
  /// added or none
  al.Result addConstraints(List<al.Constraint> constraints) {
    al.Result result = _solver.addConstraints(constraints);
    if (result == al.Result.success) {
      markNeedsLayout();
      _explicitConstraints.addAll(constraints);
    }
    return result;
  }

  /// Add the given constraint to the solver.
  al.Result addConstraint(al.Constraint constraint) {
    al.Result result = _solver.addConstraint(constraint);

    if (result == al.Result.success) {
      markNeedsLayout();
      _explicitConstraints.add(constraint);
    }

    return result;
  }

  /// Removes all explicitly added constraints.
  al.Result clearAllConstraints() {
    al.Result result = _solver.removeConstraints(_explicitConstraints);

    if (result == al.Result.success) {
      markNeedsLayout();
      _explicitConstraints = new List<al.Constraint>();
    }

    return result;
  }

  void adoptChild(RenderObject child) {
    // Make sure to call super first to setup the parent data
    super.adoptChild(child);
    child.parentData._setupImplicitConstraints(_solver);
  }

  void dropChild(RenderObject child) {
    child.parentData._removeImplicitConstraints(_solver);
    super.dropChild(child);
  }

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

  bool get sizedByParent => true;

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

  void performLayout() {
    // Step 1: Update dimensions of self
    _applyEditsAtSize(_solver, size);

    // Step 2: Resolve solver updates and flush parameters

    // We don't iterate over the children, instead, we ask the solver to tell
    // us the updated parameters. Attached to the parameters (via the context)
    // are the _AutoLayoutParamMixin instances.
    for (_AutoLayoutParamMixin update in _solver.flushUpdates()) {
      update._applyAutolayoutParameterUpdates();
    }
  }

  void _applyAutolayoutParameterUpdates() {
    // Nothing to do since the size update has already been presented to the
    // solver as an edit variable modification. The invokation of this method
    // only indicates that the value has been flushed to the variable.
  }

  void hitTestChildren(HitTestResult result, {Point position}) {
    defaultHitTestChildren(result, position: position);
  }

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

  List<al.Constraint> _constructImplicitConstraints() {
    // Only edits variables are present on layout containers. If, in the future,
    // implicit constraints (for say margins, padding, etc.) need to be added,
    // they must be returned from here.
    return null;
  }
}