part of flutter_sprites;

enum PhysicsBodyType {
    static,
    dynamic
}

/// A physics body can be assigned to any node to make it simulated by physics.
/// The body has a shape, and physical properties such as density, friction,
/// and velocity.
///
/// Bodies can be either dynamic or static. Dynamic bodies will move and rotate
/// the nodes that are associated with it. Static bodies can be moved by moving
/// or animating the node associated with them.
///
/// For a body to be simulated it needs to be associated with a [Node], through
/// the node's physicsBody property. The node also need to be a child of either
/// a [PhysicsWorld] or a [PhysicsGroup] (which in turn is a child of a
/// [PhysicsWorld] or a [Physics Group]).
class PhysicsBody {
  PhysicsBody(this.shape, {
    this.tag: null,
    this.type: PhysicsBodyType.dynamic,
    double density: 1.0,
    double friction: 0.0,
    double restitution: 0.0,
    bool isSensor: false,
    Offset linearVelocity: Offset.zero,
    double angularVelocity: 0.0,
    this.linearDampening: 0.0,
    double angularDampening: 0.0,
    bool allowSleep: true,
    bool awake: true,
    bool fixedRotation: false,
    bool bullet: false,
    bool active: true,
    this.gravityScale: 1.0,
    String collisionCategory: "Default",
    List<Object> collisionMask
  }) {
    this.density = density;
    this.friction = friction;
    this.restitution = restitution;
    this.isSensor = isSensor;

    this.linearVelocity = linearVelocity;
    this.angularVelocity = angularVelocity;
    this.angularDampening = angularDampening;

    this.allowSleep = allowSleep;
    this.awake = awake;
    this.fixedRotation = fixedRotation;
    this.bullet = bullet;
    this.active = active;

    this.collisionCategory = collisionCategory;
    this.collisionMask = collisionMask;
  }

  Vector2 _lastPosition;
  double _lastRotation;
  Vector2 _targetPosition;
  double _targetAngle;

  double _scale;

  /// An object associated with this body, normally used for detecting
  /// collisions.
  ///
  /// myBody.tag = "SpaceShip";
  Object tag;

  /// The shape of this physics body. The shape cannot be modified once the
  /// body is created. If the shape is required to change, create a new body.
  ///
  ///     myShape = myBody.shape;
  final PhysicsShape shape;

  /// The type of the body. This is either [PhysicsBodyType.dynamic] or
  /// [PhysicsBodyType.static]. Dynamic bodies are simulated by the physics,
  /// static objects affect the physics but are not moved by the physics.
  ///
  ///     myBody.type = PhysicsBodyType.static;
  PhysicsBodyType type;

  double _density;

  /// The density of the body, default value is 1.0. The density has no specific
  /// unit, instead densities are relative to each other.
  ///
  ///     myBody.density = 0.5;
  double get density => _density;

  void set density(double density) {
    _density = density;

    if (_body == null)
      return;
    for (box2d.Fixture f = _body.getFixtureList(); f != null; f = f.getNext()) {
      f.setDensity(density);
    }
  }

  double _friction;

  /// The fricion of the body, the default is 0.0 and the value should be in
  /// the range of 0.0 to 1.0.
  ///
  ///     myBody.friction = 0.4;
  double get friction => _friction;

  void set friction(double friction) {
    _friction = friction;

    if (_body == null)
      return;
    for (box2d.Fixture f = _body.getFixtureList(); f != null; f = f.getNext()) {
      f.setFriction(friction);
    }
  }

  double _restitution;

  double get restitution => _restitution;

  /// The restitution of the body, the default is 0.0 and the value should be in
  /// the range of 0.0 to 1.0.
  ///
  ///     myBody.restitution = 0.5;
  void set restitution(double restitution) {
    _restitution = restitution;

    if (_body == null)
      return;
    for (box2d.Fixture f = _body.getFixtureList(); f != null; f = f.getNext()) {
      f.setRestitution(restitution);
    }
  }

  bool _isSensor;

  /// True if the body is a sensor. Sensors doesn't collide with other bodies,
  /// but will return collision callbacks. Use a sensor body to detect if two
  /// bodies are overlapping.
  ///
  ///     myBody.isSensor = true;
  bool get isSensor => _isSensor;

  void set isSensor(bool isSensor) {
    _isSensor = isSensor;

    if (_body == null)
      return;
    for (box2d.Fixture f = _body.getFixtureList(); f != null; f = f.getNext()) {
      f.setSensor(isSensor);
    }
  }

  Offset _linearVelocity;

  /// The current linear velocity of the body in points / second.
  ///
  ///     myBody.velocity = Offset.zero;
  Offset get linearVelocity {
    if (_body == null)
      return _linearVelocity;
    else {
      double dx = _body.linearVelocity.x * _physicsWorld.b2WorldToNodeConversionFactor;
      double dy = _body.linearVelocity.y * _physicsWorld.b2WorldToNodeConversionFactor;
      return new Offset(dx, dy);
    }
  }

  void set linearVelocity(Offset linearVelocity) {
    _linearVelocity = linearVelocity;

    if (_body != null) {
      Vector2 vec = new Vector2(
        linearVelocity.dx / _physicsWorld.b2WorldToNodeConversionFactor,
        linearVelocity.dy / _physicsWorld.b2WorldToNodeConversionFactor
      );
      _body.linearVelocity = vec;
    }
  }

  double _angularVelocity;

  /// The angular velocity of the body in degrees / second.
  ///
  ///     myBody.angularVelocity = 0.0;
  double get angularVelocity {
    if (_body == null)
      return _angularVelocity;
    else
      return _body.angularVelocity;
  }

  void set angularVelocity(double angularVelocity) {
    _angularVelocity = angularVelocity;

    if (_body != null) {
      _body.angularVelocity = angularVelocity;
    }
  }

  // TODO: Should this be editable in box2d.Body ?

  /// Linear dampening, in the 0.0 to 1.0 range, default is 0.0.
  ///
  ///     double dampening = myBody.linearDampening;
  final double linearDampening;

  double _angularDampening;

  /// Angular dampening, in the 0.0 to 1.0 range, default is 0.0.
  ///
  ///     myBody.angularDampening = 0.1;
  double get angularDampening => _angularDampening;

  void set angularDampening(double angularDampening) {
    _angularDampening = angularDampening;

    if (_body != null)
      _body.angularDamping = angularDampening;
  }

  bool _allowSleep;

  /// Allows the body to sleep if it hasn't moved.
  ///
  ///     myBody.allowSleep = false;
  bool get allowSleep => _allowSleep;

  void set allowSleep(bool allowSleep) {
    _allowSleep = allowSleep;

    if (_body != null)
      _body.setSleepingAllowed(allowSleep);
  }

  bool _awake;

  /// True if the body is currently awake.
  ///
  ///     bool isAwake = myBody.awake;
  bool get awake {
    if (_body != null)
      return _body.isAwake();
    else
      return _awake;
  }

  void set awake(bool awake) {
    _awake = awake;

    if (_body != null)
      _body.setAwake(awake);
  }

  bool _fixedRotation;

  /// If true, the body cannot be rotated by the physics simulation.
  ///
  ///     myBody.fixedRotation = true;
  bool get fixedRotation => _fixedRotation;

  void set fixedRotation(bool fixedRotation) {
    _fixedRotation = fixedRotation;

    if (_body != null)
      _body.setFixedRotation(fixedRotation);
  }

  bool _bullet;

  bool get bullet => _bullet;

  /// If true, the body cannot pass through other objects when moved at high
  /// speed. Bullet bodies are slower to simulate, so only use this option
  /// if neccessary.
  ///
  ///     myBody.bullet = true;
  void set bullet(bool bullet) {
    _bullet = bullet;

    if (_body != null) {
      _body.setBullet(bullet);
    }
  }

  bool _active;

  /// An active body is used in the physics simulation. Set this to false if
  /// you want to temporarily exclude a body from the simulation.
  ///
  ///     myBody.active = false;
  bool get active {
    if (_body != null)
      return _body.isActive();
    else
      return _active;
  }

  void set active(bool active) {
    _active = active;

    if (_body != null)
      _body.setActive(active);
  }

  double gravityScale;

  Object _collisionCategory;

  /// The collision category assigned to this body. The default value is
  /// "Default". The body will only collide with bodies that have the either
  /// the [collisionMask] set to null or has the category in the mask.
  ///
  ///     myBody.collisionCategory = "Air";
  Object get collisionCategory {
    return _collisionCategory;
  }

  void set collisionCategory(Object collisionCategory) {
    _collisionCategory = collisionCategory;
    _updateFilter();
  }

  List<Object> _collisionMask;

  /// A list of collision categories that this object will collide with. If set
  /// to null (the default value) the body will collide with all other bodies.
  ///
  ///     myBody.collisionMask = ["Air", "Ground"];
  List<Object> get collisionMask => _collisionMask;

  void set collisionMask(List<Object> collisionMask) {
    _collisionMask = collisionMask;
    _updateFilter();
  }

  box2d.Filter get _b2Filter {
    print("_physicsNode: $_physicsWorld groups: ${_physicsWorld._collisionGroups}");
    box2d.Filter f = new box2d.Filter();
    f.categoryBits = _physicsWorld._collisionGroups.getBitmaskForKeys([_collisionCategory]);
    f.maskBits = _physicsWorld._collisionGroups.getBitmaskForKeys(_collisionMask);

    print("Filter: $f category: ${f.categoryBits} mask: ${f.maskBits}");

    return f;
  }

  void _updateFilter() {
    if (_body != null) {
      box2d.Filter filter = _b2Filter;
      for (box2d.Fixture fixture = _body.getFixtureList(); fixture != null; fixture = fixture.getNext()) {
        fixture.setFilterData(filter);
      }
    }
  }

  PhysicsWorld _physicsWorld;
  Node _node;

  box2d.Body _body;

  List<PhysicsJoint> _joints = <PhysicsJoint>[];

  bool _attached = false;

  /// Applies a force to the body at the [worldPoint] position in world
  /// cordinates.
  ///
  ///     myBody.applyForce(new Offset(0.0, 100.0), myNode.position);
  void applyForce(Offset force, Point worldPoint) {
    assert(_body != null);

    Vector2 b2Force = new Vector2(
      force.dx / _physicsWorld.b2WorldToNodeConversionFactor,
      force.dy / _physicsWorld.b2WorldToNodeConversionFactor);

    Vector2 b2Point = new Vector2(
      worldPoint.x / _physicsWorld.b2WorldToNodeConversionFactor,
      worldPoint.y / _physicsWorld.b2WorldToNodeConversionFactor
    );

    _body.applyForce(b2Force, b2Point);
  }

  /// Applice a force to the body at the its center of gravity.
  ///
  ///     myBody.applyForce(new Offset(0.0, 100.0));
  void applyForceToCenter(Offset force) {
    assert(_body != null);

    Vector2 b2Force = new Vector2(
      force.dx / _physicsWorld.b2WorldToNodeConversionFactor,
      force.dy / _physicsWorld.b2WorldToNodeConversionFactor);

    _body.applyForceToCenter(b2Force);
  }

  /// Applies a torque to the body.
  ///
  ///     myBody.applyTorque(10.0);
  void applyTorque(double torque) {
    assert(_body != null);

    _body.applyTorque(torque / _physicsWorld.b2WorldToNodeConversionFactor);
  }

  /// Applies a linear impulse to the body at the [worldPoint] position in world
  /// cordinates.
  ///
  ///     myBody.applyLinearImpulse(new Offset(0.0, 100.0), myNode.position);
  void applyLinearImpulse(Offset impulse, Point worldPoint, [bool wake = true]) {
    assert(_body != null);

    Vector2 b2Impulse = new Vector2(
      impulse.dx / _physicsWorld.b2WorldToNodeConversionFactor,
      impulse.dy / _physicsWorld.b2WorldToNodeConversionFactor);

    Vector2 b2Point = new Vector2(
      worldPoint.x / _physicsWorld.b2WorldToNodeConversionFactor,
      worldPoint.y / _physicsWorld.b2WorldToNodeConversionFactor
    );

    _body.applyLinearImpulse(b2Impulse, b2Point, wake);
  }

  /// Applies an angular impulse to the body.
  ///
  ///     myBody.applyAngularImpulse(20.0);
  void applyAngularImpulse(double impulse) {
    assert(_body != null);

    _body.applyAngularImpulse(impulse / _physicsWorld.b2WorldToNodeConversionFactor);
  }

  void _attach(PhysicsWorld physicsNode, Node node) {
    assert(_attached == false);

    _physicsWorld = physicsNode;

    // Account for physics groups
    Point positionWorld = node._positionToPhysics(node.position, node.parent);
    double rotationWorld = node._rotationToPhysics(node.rotation, node.parent);
    double scaleWorld = node._scaleToPhysics(node.scale, node.parent);

    // Update scale
    _scale = scaleWorld;

    // Create BodyDef
    box2d.BodyDef bodyDef = new box2d.BodyDef();
    bodyDef.linearVelocity = new Vector2(linearVelocity.dx, linearVelocity.dy);
    bodyDef.angularVelocity = angularVelocity;
    bodyDef.linearDamping = linearDampening;
    bodyDef.angularDamping = angularDampening;
    bodyDef.allowSleep = allowSleep;
    bodyDef.awake = awake;
    bodyDef.fixedRotation = fixedRotation;
    bodyDef.bullet = bullet;
    bodyDef.active = active;
    bodyDef.gravityScale = gravityScale;
    if (type == PhysicsBodyType.dynamic)
      bodyDef.type = box2d.BodyType.DYNAMIC;
    else
      bodyDef.type = box2d.BodyType.STATIC;

    // Convert to world coordinates and set position and angle
    double conv = physicsNode.b2WorldToNodeConversionFactor;
    bodyDef.position = new Vector2(positionWorld.x / conv, positionWorld.y / conv);
    bodyDef.angle = radians(rotationWorld);

    // Create Body
    _body = physicsNode.b2World.createBody(bodyDef);

    _createFixtures(physicsNode);

    _body.userData = this;

    _node = node;

    _attached = true;

    // Attach any joints
    for (PhysicsJoint joint in _joints) {
      if (joint.bodyA._attached && joint.bodyB._attached) {
        joint._attach(physicsNode);
      }
    }
  }

  void _createFixtures(PhysicsWorld physicsNode) {
    // Create FixtureDef
    box2d.FixtureDef fixtureDef = new box2d.FixtureDef();
    fixtureDef.friction = friction;
    fixtureDef.restitution = restitution;
    fixtureDef.density = density;
    fixtureDef.isSensor = isSensor;
    fixtureDef.filter = _b2Filter;

    // Get shapes
    List<box2d.Shape> b2Shapes = <box2d.Shape>[];
    List<PhysicsShape> physicsShapes = <PhysicsShape>[];
    _addB2Shapes(physicsNode, shape, b2Shapes, physicsShapes);

    // Create fixtures
    for (int i = 0; i < b2Shapes.length; i++) {
      box2d.Shape b2Shape = b2Shapes[i];
      PhysicsShape physicsShape = physicsShapes[i];

      fixtureDef.shape = b2Shape;
      box2d.Fixture fixture = _body.createFixtureFromFixtureDef(fixtureDef);
      fixture.userData = physicsShape;
    }
  }

  void _detach() {
    if (_attached) {
      _physicsWorld._bodiesScheduledForDestruction.add(_body);
      _attached = false;
    }
  }

  void _updateScale(PhysicsWorld physicsNode) {
    // Destroy old fixtures
    for (box2d.Fixture fixture = _body.getFixtureList(); fixture != null; fixture = fixture.getNext()) {
      _body.destroyFixture(fixture);
    }

    // Make sure we create new b2Shapes
    shape._invalidate();

    // Create new fixtures
    _createFixtures(physicsNode);
  }

  void _addB2Shapes(PhysicsWorld physicsNode, PhysicsShape shape, List<box2d.Shape> b2Shapes, List<PhysicsShape> physicsShapes) {
    if (shape is PhysicsShapeGroup) {
      for (PhysicsShape child in shape.shapes) {
        _addB2Shapes(physicsNode, child, b2Shapes, physicsShapes);
      }
    } else {
      b2Shapes.add(shape.getB2Shape(physicsNode, _scale));
      physicsShapes.add(shape);
    }
  }
}
