part of skysprites;

double convertDegrees2Radians(double degrees) => degrees * math.PI/180.8;

double convertRadians2Degrees(double radians) => radians * 180.0/math.PI;

/// A base class for all objects that can be added to the sprite node tree and rendered to screen using [SpriteBox] and
/// [SpriteWidget].
///
/// The [Node] class itself doesn't render any content, but provides the basic functions of any type of node, such as
/// handling transformations and user input. To render the node tree, a root node must be added to a [SpriteBox] or a
/// [SpriteWidget]. Commonly used sub-classes of [Node] are [Sprite], [NodeWithSize], and many more upcoming subclasses.
///
/// Nodes form a hierarchical tree. Each node can have a number of children, and the transformation (positioning,
/// rotation, and scaling) of a node also affects its children.
class Node {

  // Member variables

  SpriteBox _spriteBox;
  Node _parent;

  Point _position = Point.origin;
  double _rotation = 0.0;

  Matrix4 _transformMatrix = new Matrix4.identity();
  Matrix4 _transformMatrixNodeToBox;
  Matrix4 _transformMatrixBoxToNode;

  double _scaleX = 1.0;
  double _scaleY = 1.0;

  double _skewX = 0.0;
  double _skewY = 0.0;

  /// The visibility of this node and its children.
  bool visible = true;

  double _zPosition = 0.0;
  int _addedOrder;
  int _childrenLastAddedOrder = 0;
  bool _childrenNeedSorting = false;
  Matrix4 _savedTotalMatrix;

  /// Decides if the node and its children is currently paused.
  ///
  /// A paused node will not receive any input events, update calls, or run any animations.
  ///
  ///     myNodeTree.paused = true;
  bool paused = false;

  bool _userInteractionEnabled = false;

  /// If set to true the node will receive multiple pointers, otherwise it will only receive events the first pointer.
  ///
  /// This property is only meaningful if [userInteractionEnabled] is set to true. Default value is false.
  ///
  ///     class MyCustomNode extends Node {
  ///       handleMultiplePointers = true;
  ///     }
  bool handleMultiplePointers = false;
  int _handlingPointer;

  List<Node>_children = [];

  ActionController _actions;

  /// The [ActionController] associated with this node.
  ///
  ///     myNode.actions.run(myAction);
  ActionController get actions {
    if (_actions == null) {
      _actions = new ActionController();
      if (_spriteBox != null) _spriteBox._actionControllers = null;
    }
    return _actions;
  }

  List<Constraint> _constraints;

  List<Constraint> get constraints {
    return _constraints;
  }

  set constraints(List<Constraint> constraints) {
    _constraints = constraints;
    if (_spriteBox != null) _spriteBox._constrainedNodes = null;
  }

  void applyConstraints(double dt) {
    if (_constraints == null) return;

    for (Constraint constraint in _constraints) {
      constraint.constrain(this, dt);
    }
  }

  // Constructors

  /// Creates a new [Node] without any transformation.
  ///
  ///     var myNode = new Node();
  Node() {
  }

  // Property setters and getters

  /// The [SpriteBox] this node is added to, or null if it's not currently added to a [SpriteBox].
  ///
  /// For most applications it's not necessary to access the [SpriteBox] directly.
  ///
  ///     // Get the transformMode of the sprite box
  ///     var transformMode = myNode.spriteBox.transformMode;
  SpriteBox get spriteBox => _spriteBox;

  /// The parent of this node, or null if it doesn't have a parent.
  ///
  ///     // Hide the parent
  ///     myNode.parent.visible = false;
  Node get parent => _parent;

  /// The rotation of this node in degrees.
  ///
  ///     myNode.rotation = 45.0;
  double get rotation => _rotation;

  void set rotation(double rotation) {
    assert(rotation != null);
    _rotation = rotation;
    invalidateTransformMatrix();
  }

  /// The position of this node relative to its parent.
  ///
  ///     myNode.position = new Point(42.0, 42.0);
  Point get position => _position;

  void set position(Point position) {
    assert(position != null);
    _position = position;
    invalidateTransformMatrix();
  }

  /// The skew along the x-axis of this node in degrees.
  ///
  ///     myNode.skewX = 45.0;
  double get skewX => _skewX;

  void set skewX (double skewX) {
    assert(skewX != null);
    _skewX = skewX;
    invalidateTransformMatrix();
  }

  /// The skew along the y-axis of this node in degrees.
  ///
  ///     myNode.skewY = 45.0;
  double get skewY => _skewY;

  void set skewY (double skewY) {
    assert(skewY != null);
    _skewY = skewY;
    invalidateTransformMatrix();
  }

  /// The draw order of this node compared to its parent and its siblings.
  ///
  /// By default nodes are drawn in the order that they have been added to a parent. To override this behavior the
  /// [zPosition] property can be used. A higher value of this property will force the node to be drawn in front of
  /// siblings that have a lower value. If a negative value is used the node will be drawn behind its parent.
  ///
  ///     nodeInFront.zPosition = 1.0;
  ///     nodeBehind.zPosition = -1.0;
  double get zPosition => _zPosition;

  void set zPosition(double zPosition) {
    assert(zPosition != null);
    _zPosition = zPosition;
    if (_parent != null) {
      _parent._childrenNeedSorting = true;
    }
  }

  /// The scale of this node relative its parent.
  ///
  /// The [scale] property is only valid if [scaleX] and [scaleY] are equal values.
  ///
  ///     myNode.scale = 5.0;
  double get scale {
    assert(_scaleX == _scaleY);
    return _scaleX;
  }

  void set scale(double scale) {
    assert(scale != null);
    _scaleX = _scaleY = scale;
    invalidateTransformMatrix();
  }

  /// The horizontal scale of this node relative its parent.
  ///
  ///     myNode.scaleX = 5.0;
  double get scaleX => _scaleX;

  void set scaleX(double scaleX) {
    assert(scaleX != null);
    _scaleX = scaleX;
    invalidateTransformMatrix();
  }

  /// The vertical scale of this node relative its parent.
  ///
  ///     myNode.scaleY = 5.0;
  double get scaleY => _scaleY;

  void set scaleY(double scaleY) {
    assert(scaleY != null);
    _scaleY = scaleY;
    invalidateTransformMatrix();
  }

  /// A list of the children of this node.
  ///
  /// This list should only be modified by using the [addChild] and [removeChild] methods.
  ///
  ///     // Iterate over a nodes children
  ///     for (Node child in myNode.children) {
  ///       // Do something with the child
  ///     }
  List<Node> get children {
    _sortChildren();
    return _children;
  }

  // Adding and removing children

  /// Adds a child to this node.
  ///
  /// The same node cannot be added to multiple nodes.
  ///
  ///     addChild(new Sprite(myImage));
  void addChild(Node child) {
    assert(child != null);
    assert(child._parent == null);

    _childrenNeedSorting = true;
    _children.add(child);
    child._parent = this;
    child._spriteBox = this._spriteBox;
    _childrenLastAddedOrder += 1;
    child._addedOrder = _childrenLastAddedOrder;
    if (_spriteBox != null) _spriteBox._registerNode(child);
  }

  /// Removes a child from this node.
  ///
  ///     removeChild(myChildNode);
  void removeChild(Node child) {
    assert(child != null);
    if (_children.remove(child)) {
      child._parent = null;
      child._spriteBox = null;
      if (_spriteBox != null) _spriteBox._deregisterNode(child);
    }
  }

  /// Removes this node from its parent node.
  ///
  ///     removeFromParent();
  void removeFromParent() {
    assert(_parent != null);
    _parent.removeChild(this);
  }

  /// Removes all children of this node.
  ///
  ///     removeAllChildren();
  void removeAllChildren() {
    for (Node child in _children) {
      child._parent = null;
      child._spriteBox = null;
    }
    _children = [];
    _childrenNeedSorting = false;
    if (_spriteBox != null) _spriteBox._deregisterNode(null);
  }

  void _sortChildren() {
    // Sort children primarily by zPosition, secondarily by added order
    if (_childrenNeedSorting) {
      _children.sort((Node a, Node b) {
        if (a._zPosition == b._zPosition) {
          return a._addedOrder - b._addedOrder;
        }
        else if (a._zPosition > b._zPosition) {
          return 1;
        }
        else {
          return -1;
        }
      });
      _childrenNeedSorting = false;
    }
  }

  // Calculating the transformation matrix

  /// The transformMatrix describes the transformation from the node's parent.
  ///
  /// You cannot set the transformMatrix directly, instead use the position, rotation and scale properties.
  ///
  ///     Matrix4 matrix = myNode.transformMatrix;
  Matrix4 get transformMatrix {
    if (_transformMatrix == null) {
      _transformMatrix = computeTransformMatrix();
    }
    return _transformMatrix;
  }

  /// Computes the transformation matrix of this node. This method can be
  /// overriden if a custom matrix is required. There is usually no reason to
  /// call this method directly.
  Matrix4 computeTransformMatrix() {
    double cx, sx, cy, sy;

    if (_rotation == 0.0) {
      cx = 1.0;
      sx = 0.0;
      cy = 1.0;
      sy = 0.0;
    }
    else {
      double radiansX = convertDegrees2Radians(_rotation);
      double radiansY = convertDegrees2Radians(_rotation);

      cx = math.cos(radiansX);
      sx = math.sin(radiansX);
      cy = math.cos(radiansY);
      sy = math.sin(radiansY);
    }

    // Create transformation matrix for scale, position and rotation
    Matrix4 matrix = new Matrix4(cy * _scaleX, sy * _scaleX, 0.0, 0.0,
               -sx * _scaleY, cx * _scaleY, 0.0, 0.0,
               0.0, 0.0, 1.0, 0.0,
              _position.x, _position.y, 0.0, 1.0);

    if (_skewX != 0.0 || _skewY != 0.0) {
      // Needs skew transform
      Matrix4 skew = new Matrix4(1.0, math.tan(radians(_skewX)), 0.0, 0.0,
                                 math.tan(radians(_skewY)), 1.0, 0.0, 0.0,
                                 0.0, 0.0, 1.0, 0.0,
                                 0.0, 0.0, 0.0, 1.0);
      matrix.multiply(skew);
    }

    return matrix;
  }

  /// Invalidates the current transform matrix. If the [computeTransformMatrix]
  /// method is overidden, this method should be called whenever a property
  /// changes that affects the matrix.
  void invalidateTransformMatrix() {
    _transformMatrix = null;
    _invalidateToBoxTransformMatrix();
  }

  void _invalidateToBoxTransformMatrix () {
    _transformMatrixNodeToBox = null;
    _transformMatrixBoxToNode = null;

    for (Node child in children) {
      child._invalidateToBoxTransformMatrix();
    }
  }

  // Transforms to other nodes

  Matrix4 _nodeToBoxMatrix() {
    assert(_spriteBox != null);
    if (_transformMatrixNodeToBox != null) {
      return _transformMatrixNodeToBox;
    }

    if (_parent == null) {
      // Base case, we are at the top
      assert(this == _spriteBox.rootNode);
      _transformMatrixNodeToBox = new Matrix4.copy(_spriteBox.transformMatrix).multiply(transformMatrix);
    }
    else {
      _transformMatrixNodeToBox = new Matrix4.copy(_parent._nodeToBoxMatrix()).multiply(transformMatrix);
    }
    return _transformMatrixNodeToBox;
  }

  Matrix4 _boxToNodeMatrix() {
    assert(_spriteBox != null);

    if (_transformMatrixBoxToNode != null) {
      return _transformMatrixBoxToNode;
    }

    _transformMatrixBoxToNode = new Matrix4.copy(_nodeToBoxMatrix());
    _transformMatrixBoxToNode.invert();

    return _transformMatrixBoxToNode;
  }

  /// Converts a point from the coordinate system of the [SpriteBox] to the local coordinate system of the node.
  ///
  /// This method is particularly useful when handling pointer events and need the pointers position in a local
  /// coordinate space.
  ///
  ///     Point localPoint = myNode.convertPointToNodeSpace(pointInBoxCoordinates);
  Point convertPointToNodeSpace(Point boxPoint) {
    assert(boxPoint != null);
    assert(_spriteBox != null);

    Vector4 v =_boxToNodeMatrix().transform(new Vector4(boxPoint.x, boxPoint.y, 0.0, 1.0));
    return new Point(v[0], v[1]);
  }

  /// Converts a point from the local coordinate system of the node to the coordinate system of the [SpriteBox].
  ///
  ///     Point pointInBoxCoordinates = myNode.convertPointToBoxSpace(localPoint);
  Point convertPointToBoxSpace(Point nodePoint) {
    assert(nodePoint != null);
    assert(_spriteBox != null);

    Vector4 v =_nodeToBoxMatrix().transform(new Vector4(nodePoint.x, nodePoint.y, 0.0, 1.0));
    return new Point(v[0], v[1]);
  }

  /// Converts a [point] from another [node]s coordinate system into the local coordinate system of this node.
  ///
  ///     Point pointInNodeASpace = nodeA.convertPointFromNode(pointInNodeBSpace, nodeB);
  Point convertPointFromNode(Point point, Node node) {
    assert(node != null);
    assert(point != null);
    assert(_spriteBox != null);
    assert(_spriteBox == node._spriteBox);

    Point boxPoint = node.convertPointToBoxSpace(point);
    Point localPoint = convertPointToNodeSpace(boxPoint);

    return localPoint;
  }

  // Hit test

  /// Returns true if the [point] is inside the node, the [point] is in the local coordinate system of the node.
  ///
  ///     myNode.isPointInside(localPoint);
  ///
  /// [NodeWithSize] provides a basic bounding box check for this method, if you require a more detailed check this
  /// method can be overridden.
  ///
  ///     bool isPointInside (Point nodePoint) {
  ///       double minX = -size.width * pivot.x;
  ///       double minY = -size.height * pivot.y;
  ///       double maxX = minX + size.width;
  ///       double maxY = minY + size.height;
  ///       return (nodePoint.x >= minX && nodePoint.x < maxX &&
  ///       nodePoint.y >= minY && nodePoint.y < maxY);
  ///     }
  bool isPointInside(Point point) {
    assert(point != null);

    return false;
  }

  // Rendering

  void _visit(PaintingCanvas canvas, Matrix4 totalMatrix) {
    assert(canvas != null);
    if (!visible) return;

    _prePaint(canvas, totalMatrix);
    _visitChildren(canvas, totalMatrix);
    _postPaint(canvas, totalMatrix);
  }

  void _prePaint(PaintingCanvas canvas, Matrix4 matrix) {
    _savedTotalMatrix = new Matrix4.copy(matrix);

    // Get the transformation matrix and apply transform
    matrix.multiply(transformMatrix);
  }

  /// Paints this node to the canvas.
  ///
  /// Subclasses, such as [Sprite], override this method to do the actual painting of the node. To do custom
  /// drawing override this method and make calls to the [canvas] object. All drawing is done in the node's local
  /// coordinate system, relative to the node's position. If you want to make the drawing relative to the node's
  /// bounding box's origin, override [NodeWithSize] and call the applyTransformForPivot method before making calls for
  /// drawing.
  ///
  ///     void paint(PaintingCanvas canvas) {
  ///       canvas.save();
  ///       applyTransformForPivot(canvas);
  ///
  ///       // Do painting here
  ///
  ///       canvas.restore();
  ///     }
  void paint(PaintingCanvas canvas) {
  }

  void _visitChildren(PaintingCanvas canvas, Matrix4 totalMatrix) {
    // Sort children if needed
    _sortChildren();

    int i = 0;

    // Visit children behind this node
    while (i < _children.length) {
      Node child = _children[i];
      if (child.zPosition >= 0.0) break;
      child._visit(canvas, totalMatrix);
      i++;
    }

    // Paint this node
    canvas.setMatrix(totalMatrix.storage);
    paint(canvas);

    // Visit children in front of this node
    while (i < _children.length) {
      Node child = _children[i];
      child._visit(canvas, totalMatrix);
      i++;
    }
  }

  void _postPaint(PaintingCanvas canvas, Matrix4 totalMatrix) {
    totalMatrix.setFrom(_savedTotalMatrix);
  }

  // Receiving update calls

  /// Called before a frame is drawn.
  ///
  /// Override this method to do any updates to the node or node tree before it's drawn to screen.
  ///
  ///     // Make the node rotate at a fixed speed
  ///     void update(double dt) {
  ///       rotation = rotation * 10.0 * dt;
  ///     }
  void update(double dt) {
  }

  /// Called whenever the [SpriteBox] is modified or resized, or if the device is rotated.
  ///
  /// Override this method to do any updates that may be necessary to correctly display the node or node tree with the
  /// new layout of the [SpriteBox].
  ///
  ///     void spriteBoxPerformedLayout() {
  ///       // Move some stuff around here
  ///     }
  void spriteBoxPerformedLayout() {
  }

  // Handling user interaction

  /// The node will receive user interactions, such as pointer (touch or mouse) events.
  ///
  ///     class MyCustomNode extends NodeWithSize {
  ///       userInteractionEnabled = true;
  ///     }
  bool get userInteractionEnabled => _userInteractionEnabled;

  void set userInteractionEnabled(bool userInteractionEnabled) {
    _userInteractionEnabled = userInteractionEnabled;
    if (_spriteBox != null) _spriteBox._eventTargets = null;
  }

  /// Handles an event, such as a pointer (touch or mouse) event.
  ///
  /// Override this method to handle events. The node will only receive events if the [userInteractionEnabled] property
  /// is set to true and the [isPointInside] method returns true for the position of the pointer down event (default
  /// behavior provided by [NodeWithSize]). Unless [handleMultiplePointers] is set to true, the node will only receive
  /// events for the first pointer that is down.
  ///
  /// Return true if the node has consumed the event, if an event is consumed it will not be passed on to nodes behind
  /// the current node.
  ///
  ///     // MyTouchySprite gets transparent when we touch it
  ///     class MyTouchySprite extends Sprite {
  ///
  ///       MyTouchySprite(Image img) : super (img) {
  ///         userInteractionEnabled = true;
  ///       }
  ///
  ///       bool handleEvent(SpriteBoxEvent event) {
  ///         if (event.type == 'pointerdown) {
  ///           opacity = 0.5;
  ///         }
  ///         else if (event.type == 'pointerup') {
  ///           opacity = 1.0;
  ///         }
  ///         return true;
  ///       }
  ///     }
  bool handleEvent(SpriteBoxEvent event) {
    return false;
  }
}