// 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.

part of flutter_sprites;

/// A constraint limits or otherwise controls a [Node]'s properties, such as
/// position or rotation. Add a list of constraints by setting the [Node]'s
/// constraints property.
///
/// Constrains are applied after the update calls are
/// completed. They can also be applied at any time by calling
/// [Node.applyConstraints]. It's possible to create custom constraints by
/// overriding this class and implementing the [constrain] method.
abstract class Constraint {
  /// Called before the node's update method is called. This method can be
  /// overridden to create setup work that needs to happen before the the
  /// node is updated, e.g. to calculate the node's speed.
  void preUpdate(Node node, double dt) {
  }

  /// Called after update is complete, if the constraint has been added to a
  /// [Node]. Override this method to modify the node's property according to
  /// the constraint.
  void constrain(Node node, double dt);
}

double _dampenRotation(double src, double dst, double dampening) {
  if (dampening == null)
    return dst;

  double delta = dst - src;
  while (delta > 180.0) delta -= 360;
  while (delta < -180) delta += 360;
  delta *= dampening;

  return src + delta;
}

/// A [Constraint] that aligns a nodes rotation to its movement.
class ConstraintRotationToMovement extends Constraint {
  /// Creates a new constraint the aligns a nodes rotation to its movement
  /// vector. A [baseRotation] and [dampening] can optionally be set.
  ConstraintRotationToMovement({this.baseRotation: 0.0, this.dampening});

  /// The filter factor used when constraining the rotation of the node. Valid
  /// values are in the range 0.0 to 1.0
  final double dampening;

  /// The base rotation will be added to a the movement vectors rotation.
  final double baseRotation;

  Point _lastPosition;

  @override
  void preUpdate(Node node, double dt) {
    _lastPosition = node.position;
  }

  @override
  void constrain(Node node, double dt) {
    if (_lastPosition == null) return;
    if (_lastPosition == node.position) return;

    // Get the target angle
    Offset offset = node.position - _lastPosition;
    double target = degrees(GameMath.atan2(offset.dy, offset.dx)) + baseRotation;

    node.rotation = _dampenRotation(node.rotation, target, dampening);
  }
}

/// A [Constraint] that copies a node's rotation, optionally with [dampening].
class ConstraintRotationToNodeRotation extends Constraint {
  /// Creates a new constraint that copies a node's rotation, optionally
  /// with a [baseRotation] added and using [dampening].
  ConstraintRotationToNodeRotation(this.targetNode, { this.baseRotation: 0.0, this.dampening });

  /// The node to copy the rotation from
  final Node targetNode;

  /// The base rotation will be added to the rotation that copied from the targetNode
  final double baseRotation;

  /// The filter factor used when constraining the rotation of the node. Valid
  /// values are in the range 0.0 to 1.0
  final double dampening;

  @override
  void constrain(Node node, double dt) {
    double target = targetNode.rotation + baseRotation;
    node.rotation = _dampenRotation(node.rotation, target, dampening);
  }
}

/// A [Constraint] that rotates a node to point towards another node. The target
/// node is allowed to have a different parent, but they must be in the same
/// [SpriteBox].
class ConstraintRotationToNode extends Constraint {
  /// Creates a new [Constraint] that rotates the node towards the [targetNode].
  /// The [baseRotation] will be added to the nodes rotation, and [dampening]
  /// can be used to ease the rotation.
  ConstraintRotationToNode(this.targetNode, {this.baseRotation: 0.0, this.dampening});

  /// The node to rotate towards.
  final Node targetNode;

  /// The base rotation will be added after the target rotation is calculated.
  final double baseRotation;

  /// The filter factor used when constraining the rotation of the node. Valid
  /// values are in the range 0.0 to 1.0
  final double dampening;

  @override
  void constrain(Node node, double dt) {
    Offset offset;

    if (targetNode.spriteBox != node.spriteBox) {
      // The target node is in another sprite box or has been removed
      return;
    }

    if (targetNode.parent == node.parent) {
      offset = targetNode.position - node.position;
    } else {
      offset = node.convertPointToBoxSpace(Point.origin)
        - targetNode.convertPointToBoxSpace(Point.origin);
    }

    double target = degrees(GameMath.atan2(offset.dy, offset.dx)) + baseRotation;

    node.rotation = _dampenRotation(node.rotation, target, dampening);
  }
}

/// A [Constraint] that constrains the position of a node to equal the position
/// of another node, optionally with dampening.
class ConstraintPositionToNode extends Constraint {
  /// Creates a new [Constraint] that constrains the poistion of a node to be
  /// equal to the position of the [targetNode]. Optionally an [offset] can
  /// be used and also [dampening]. The targetNode doesn't need to have the
  /// same parent, but they need to be added to the same [SpriteBox].
  ConstraintPositionToNode(this.targetNode, {this.dampening, this.offset: Offset.zero});

  /// Target node to follow.
  final Node targetNode;

  /// Offset to the target node.
  final Offset offset;

  /// Dampening used when following the [targetNode], value between 0.0 and 1.0.
  final double dampening;

  @override
  void constrain(Node node, double dt) {
    Point targetPosition;

    if (targetNode.spriteBox != node.spriteBox || node.parent == null) {
      // The target node is in another sprite box or has been removed
      return;
    }

    if (targetNode.parent == node.parent) {
      targetPosition = targetNode.position;
    } else {
      targetPosition = node.parent.convertPointFromNode(Point.origin, targetNode);
    }

    if (offset != null)
      targetPosition += offset;

    if (dampening == null)
      node.position = targetPosition;
    else
      node.position = GameMath.filterPoint(node.position, targetPosition, dampening);
  }
}