package de.gurkenlabs.litiengine.physics;

import java.awt.geom.Point2D;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.function.Consumer;
import java.util.function.Predicate;

import de.gurkenlabs.litiengine.Game;
import de.gurkenlabs.litiengine.entities.IMobileEntity;
import de.gurkenlabs.litiengine.util.geom.GeometricUtilities;

public class MovementController<T extends IMobileEntity> implements IMovementController {
  private static final double FORCE_APPLY_ACCEPTED_ERROR = 0.1;
  private final List<Force> activeForces;
  private final T mobileEntity;
  private final List<Predicate<IMobileEntity>> movementPredicates;
  private final List<Consumer<Point2D>> movedConsumer;

  public MovementController(final T mobileEntity) {
    this.activeForces = new CopyOnWriteArrayList<>();
    this.movementPredicates = new CopyOnWriteArrayList<>();
    this.movedConsumer = new CopyOnWriteArrayList<>();
    this.mobileEntity = mobileEntity;
  }

  @Override
  public void attach() {
    Game.loop().attach(this);
  }

  @Override
  public void detach() {
    Game.loop().detach(this);
  }

  @Override
  public void apply(final Force force) {
    if (!this.activeForces.contains(force)) {
      this.activeForces.add(force);
    }
  }

  @Override
  public List<Force> getActiveForces() {
    return this.activeForces;
  }

  @Override
  public T getEntity() {
    return this.mobileEntity;
  }

  @Override
  public void onMovementCheck(final Predicate<IMobileEntity> predicate) {
    if (!this.movementPredicates.contains(predicate)) {
      this.movementPredicates.add(predicate);
    }
  }

  @Override
  public void update() {
    this.handleForces();
  }

  @Override
  public void onMoved(Consumer<Point2D> cons) {
    this.movedConsumer.add(cons);
  }

  protected void moveEntity(double deltaX, double deltaY) {
    final Point2D newLocation = new Point2D.Double(this.getEntity().getX() + deltaX, this.getEntity().getY() + deltaY);
    final Point2D oldLocation = this.getEntity().getLocation();
    Game.physics().move(this.getEntity(), newLocation);

    final Point2D delta = new Point2D.Double(this.getEntity().getX() - oldLocation.getX(), this.getEntity().getY() - oldLocation.getY());
    for (Consumer<Point2D> cons : this.movedConsumer) {
      cons.accept(delta);
    }
  }

  protected boolean isMovementAllowed() {
    for (final Predicate<IMobileEntity> predicate : this.movementPredicates) {
      if (!predicate.test(this.getEntity())) {
        return false;
      }
    }

    return true;
  }

  private void handleForces() {
    // clean up forces
    this.activeForces.forEach(x -> {
      if (x.hasEnded()) {
        this.activeForces.remove(x);
      }
    });

    for (final Force force : this.activeForces) {
      if (force.cancelOnReached() && force.hasReached(this.getEntity())) {
        force.end();
        continue;
      }

      final Point2D collisionBoxCenter = this.getEntity().getCollisionBoxCenter();
      if (collisionBoxCenter.distance(force.getLocation()) < FORCE_APPLY_ACCEPTED_ERROR) {
        final double yDelta = this.getEntity().getHeight() - this.getEntity().getCollisionBox().getHeight() + this.getEntity().getCollisionBox().getHeight() / 2;
        final Point2D entityLocation = new Point2D.Double(force.getLocation().getX() - this.getEntity().getWidth() / 2, force.getLocation().getY() - yDelta);
        this.getEntity().setLocation(entityLocation);
      } else {
        final double angle = GeometricUtilities.calcRotationAngleInDegrees(collisionBoxCenter, force.getLocation());
        final boolean success = Game.physics().move(this.getEntity(), (float) angle, Game.loop().getDeltaTime() * 0.001f * force.getStrength() * Game.loop().getTimeScale());
        if (force.cancelOnCollision() && !success) {
          force.end();
        }
      }
    }
  }
}

@@ -2826,24 +2826,180 @@
 %0A    %7D);%0D%0A%0D%0A
+    // disable turn-on-move for force handling%0D%0A    boolean turn = this.getEntity().turnOnMove();%0D%0A    this.getEntity().setTurnOnMove(false);%0D%0A    try %7B%0D%0A  
     for (fin
@@ -3033,24 +3033,26 @@
 eForces) %7B%0D%0A
+  
       if (fo
@@ -3115,32 +3115,34 @@
 ())) %7B%0D%0A        
+  
 force.end();%0D%0A  
@@ -3147,16 +3147,18 @@
         
+  
 continue
@@ -3162,29 +3162,33 @@
 nue;%0D%0A      
+ 
+ 
 %7D%0D%0A%0D%0A
+  
       final 
@@ -3261,24 +3261,26 @@
 r();%0D%0A      
+ 
+ 
 if (collisio
@@ -3349,24 +3349,26 @@
 D_ERROR) %7B%0D%0A
+  
         fina
@@ -3518,32 +3518,34 @@
 ) / 2;%0D%0A        
+ 
+ 
 final Point2D en
@@ -3683,24 +3683,26 @@
 );%0D%0A        
+  
 this.getEnti
@@ -3742,16 +3742,18 @@
 %0D%0A      
+ 
+ 
 %7D else %7B
@@ -3750,24 +3750,26 @@
   %7D else %7B%0D%0A
+  
         fina
@@ -3878,24 +3878,26 @@
 );%0D%0A        
+ 
+ 
 final boolea
@@ -4051,32 +4051,34 @@
 le());%0D%0A        
+  
 if (force.cancel
@@ -4109,32 +4109,34 @@
 s) %7B%0D%0A          
+  
 force.end();%0D%0A  
@@ -4137,24 +4137,26 @@
 );%0D%0A        
+  
 %7D%0D%0A      %7D%0D%0A
@@ -4144,33 +4144,106 @@
        %7D%0D%0A      
-%7D
+  %7D%0D%0A      %7D%0D%0A    %7D finally %7B%0D%0A      this.getEntity().setTurnOnMove(turn);
 %0D%0A    %7D%0D%0A  %7D%0D%0A%7D%0D

Disable turn-on-move when applying forces.