// 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:flutter/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 {
  /// Creates parameters for a rectangle for use with auto layout.
  AutoLayoutRect()
    : left = new al.Param(),
      right = new al.Param(),
      top = new al.Param(),
      bottom = new al.Param();

  /// A parameter that represents the left edge of the rectangle.
  final al.Param left;

  /// A parameter that represents the right edge of the rectangle.
  final al.Param right;

  /// A parameter that represents the top edge of the rectangle.
  final al.Param top;

  /// A parameter that represents the bottom edge of the rectangle.
  final al.Param bottom;

  /// An expression that represents the horizontal extent of the rectangle.
  al.Expression get width => right - left;

  /// An expression that represents the vertical extent of the rectangle.
  al.Expression get height => bottom - top;

  /// An expression that represents halfway between the left and right edges of the rectangle.
  al.Expression get horizontalCenter => (left + right) / al.cm(2.0);

  /// An expression that represents halfway between the top and bottom edges of the rectangle.
  al.Expression get verticalCenter => (top + bottom) / al.cm(2.0);

  /// Constraints that require that this rect contains the given rect.
  List<al.Constraint> contains(AutoLayoutRect other) {
    return <al.Constraint>[
      other.left >= left,
      other.right <= right,
      other.top >= top,
      other.bottom <= bottom,
    ];
  }
}

/// Parent data for use with [RenderAutoLayout].
class AutoLayoutParentData extends ContainerBoxParentDataMixin<RenderBox> {
  /// Creates parent data associated with the given render box.
  AutoLayoutParentData(this._renderBox);

  final RenderBox _renderBox;

  /// Parameters that represent the size and position of the render box.
  AutoLayoutRect get rect => _rect;
  AutoLayoutRect _rect;
  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.
      // TODO(chinmay): Check whether we need something similar for the top and
      // bottom.
    ];
  }
}

/// Subclass to control the layout of a [RenderAutoLayout].
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();

  /// Returns the constraints to use when computing layout.
  ///
  /// The `parent` argument contains the parameters for the parent's position
  /// and size. Typical implementations will return constraints that determine
  /// the size and position of each child.
  ///
  /// The delegate interface does not provide a mechanism for obtaining the
  /// parameters for children. Subclasses are expected to obtain those
  /// parameters through some other mechanism.
  List<al.Constraint> getConstraints(AutoLayoutRect parent);

  /// Override this method to return true when new constraints need to be generated.
  bool shouldUpdateConstraints(AutoLayoutDelegate oldDelegate);
}

/// A render object that uses the cassowary constraint solver to automatically size and position children.
class RenderAutoLayout extends RenderBox
    with ContainerRenderObjectMixin<RenderBox, AutoLayoutParentData>,
         RenderBoxContainerDefaultsMixin<RenderBox, AutoLayoutParentData> {
  /// Creates a render box that automatically sizes and positions its children.
  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);
  }

  /// The delegate that generates constraints for the layout.
  ///
  /// If the new delegate is the same as the previous one, this does nothing.
  ///
  /// If the new delegate is the same class as the previous one, then the new
  /// delegate has its [AutoLayoutDelegate.shouldUpdateConstraints] called; if
  /// the result is `true`, then the delegate will be called.
  ///
  /// If the new delegate is a different class than the previous one, then the
  /// delegate will be called.
  ///
  /// If the delgate is null, the layout is unconstrained.
  AutoLayoutDelegate get delegate => _delegate;
  AutoLayoutDelegate _delegate;
  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);
  }
}
