// ============================================================================
// File:               $File$
//
// Project:            
//
// Purpose:            
//
// Author:             Rammi
//
// Copyright Notice:   (c) 2008  Rammi (rammi@caff.de)
//
// Latest change:      $Date$
//
// History:	       $Log$
//=============================================================================
package de.caff.asteroid.rammi;

import de.caff.asteroid.*;
import de.caff.util.Tools;

import java.awt.geom.Point2D;
import java.awt.*;
import java.util.*;
import java.util.List;

/**
 *  Player which calculates the frames till it is possible to fire,
 *  and fires on the first possible object.
 */
public class FramesTillFirePlayer
        extends AbstractBasicAsteroidPlayer
        implements FrameListener
{
  private static final int FUTURE_FRAMES = 60;

  static class PossibleTarget
  {
    private final int rotation;
    private final MovingGameObject target;

    PossibleTarget(int rotation, MovingGameObject target)
    {
      this.rotation = rotation;
      this.target = target;
    }

    public int getRotation()
    {
      return rotation;
    }

    public MovingGameObject getTarget()
    {
      return target;
    }
  }

  private Futurologist futurologist;
  private ShootingDirectionFixer shootingDirectionFixer;

  public FramesTillFirePlayer(Communication com)
  {
    super(com);
    futurologist = new StandardFuturologist();
    if (com != null) {
      shootingDirectionFixer = new ShootingDirectionFixer();
      com.setFramePreparer(new FramePreparerSequence(new ImprovedVelocityPreparer(),
                                                     new ScoreFixer(),
                                                     shootingDirectionFixer));
      com.addDatagramListener(shootingDirectionFixer);
    }
  }

  /**
   * Call this to cleanup any resources taken by this object.
   */
  public void destroy()
  {
    if (shootingDirectionFixer != null) {
      com.removeDatagramListener(shootingDirectionFixer);
      shootingDirectionFixer = null;
    }
  }

  /**
   * Called each time a frame is received.
   * <p/>
   * <b>ATTENTION:</b> this is called from the communication thread!
   * Implementing classes must be aware of this and take care by synchronization or similar!
   *
   * @param frame the received frame
   */
  public void frameReceived(FrameInfo frame)
  {
    futurologist.frameReceived(frame);

    SpaceShip ship = frame.getSpaceShip();
    if (ship != null) {
      if (hasIncomingDangerousAsteroids(frame, 2)  ||
          hasIncomingDangerousShots(frame, 2)) {
        pushButton(BUTTON_HYPERSPACE);
      }
      else {
        getNextTargets(frame, ship);

        /*
        Point2D bulletDir = frame.getNextShootingDirection().getBulletVelocity();

        if (!nextTargets.isEmpty()) {
          // turn to next target
          Point2D dir = getShootDirection(ship, nextTargets.get(0));
          if (dir != null) {
            //infoDrawer.setDanger(target);
            dir = Tools.normalize(dir);
            Point2D shipDir = Tools.normalize(bulletDir);

            double cross = Tools.crossProduct(dir, shipDir);
            if (cross <= -ROT_BORDER ) {
              pushButton(BUTTON_LEFT);
            }
            else if (cross >= ROT_BORDER) {
              pushButton(BUTTON_RIGHT);
            }
          }
        }
        */
      }
    }
  }

  private void getNextTargets(FrameInfo info, SpaceShip ship)
  {
    byte dirByte = info.getNextShootingDirectionLowLevel();
    SortedMap<Integer, PossibleTarget> targets = new TreeMap<Integer, PossibleTarget>();

    List<Asteroid> unhitAsteroids = new LinkedList<Asteroid>();
    for (Asteroid ast: info.getAsteroids()) {
      if (futurologist.getFramesTillDestroyed(ast, info) < 0) {
        unhitAsteroids.add(ast);
      }
    }
    Asteroid ast = findHitAsteroid(ship,
                                   FrameInfo.getShootingDirection(dirByte),
                                   unhitAsteroids,
                                   1);
    if (ast != null) {
      targets.put(1, new PossibleTarget(dirByte, ast));
    }
    for (int f = 1;  f <= FUTURE_FRAMES  &&  !unhitAsteroids.isEmpty();  ++f) {
      for (int dir = -1;  dir <= 1;  dir += 2) {
        ast = findHitAsteroid(ship,
                              FrameInfo.getShootingDirection(dirByte + dir),
                              unhitAsteroids,
                              f);
        if (ast != null) {
          targets.put(f, new PossibleTarget(dirByte+dir, ast));
        }
      }
    }
    /*
    Ufo ufo = info.getUfo();
    if (ufo != null) {

    }
    */
    if (!targets.isEmpty()) {
      PossibleTarget target = targets.remove(targets.firstKey());
      if (target.getRotation() == 0  &&  !haveFiredInLastFrame()) {
        pushButton(BUTTON_FIRE);
      }
      else if (!targets.isEmpty()) {
        target = targets.get(targets.firstKey());
        if (target.getRotation() < 0) {
          pushButton(BUTTON_RIGHT);
        }
        else {
          pushButton(BUTTON_LEFT);
        }
      }
    }
  }

  private static Asteroid findHitAsteroid(SpaceShip ship,
                                          FrameInfo.Direction shootDir,
                                          List<Asteroid> unhitAsteroids,
                                          int frames)
  {
    Point shipPos = ship.getPredictedLocation(frames);
    Point2D bulletPos      = new Point(shipPos.x + shootDir.getDisplacement().x,
                                       shipPos.y + shootDir.getDisplacement().y);
    Point2D bulletVelocity = new Point2D.Double(ship.getVelocityX() + shootDir.getBulletVelocity().getX(),
                                                ship.getVelocityY() + shootDir.getBulletVelocity().getY());
    for (int f = 0;  f < Bullet.MIN_LIFETIME;  ++f) {
      for (ListIterator<Asteroid> it = unhitAsteroids.listIterator();  it.hasNext();  ) {
        Asteroid ast = it.next();
        Point p = ast.getPredictedLocation(f + frames);
        Point delta = GameObject.getTorusDelta(p.x, p.y, (int)bulletPos.getX(), (int)bulletPos.getY());
        if (Tools.getSquaredLength(delta) < ast.getSquaredSize()) {
          it.remove();
          return ast;
        }
      }
      bulletPos.setLocation(bulletPos.getX() + bulletVelocity.getX(),
                            bulletPos.getY() + bulletVelocity.getY());
    }
    return null;
  }

  /**
   *  Are there any shots which will hit the ship in the over-next frame?
   *  @param info the frame info
   *  @param predictionLag lag used for prediction
   *  @return the answer
   */
  private static boolean hasIncomingDangerousShots(FrameInfo info, int predictionLag)
  {
    SpaceShip ship = info.getSpaceShip();
    Point sPos = ship.getPredictedLocation(predictionLag);
    int size = ship.getSize();
    for (Bullet bullet: info.getBullets()) {
      if (bullet.getVelocityX() == 0  &&  bullet.getVelocityY() == 0) {
        continue;
      }
      Point distance = GameObject.getTorusDelta(bullet.getPredictedLocation(predictionLag), sPos);

      if (Math.abs(distance.x) <= size &&
          Math.abs(distance.y) <= size)  {
        return true;
      }
    }
    return false;
  }

  /**
   *  Are there any asteroids which will hit the ship in the over-next frame?
   *  @param info frame info
   *  @param predictionLag lag used for prediction
   *  @return the answer
   */
  private boolean hasIncomingDangerousAsteroids(FrameInfo info, int predictionLag)
  {
    SpaceShip ship = info.getSpaceShip();
    for (Asteroid ast: info.getAsteroids()) {
      int framesTillDestroyed = futurologist.getFramesTillDestroyed(ast, info);
      if (framesTillDestroyed < 0  ||
          framesTillDestroyed > predictionLag) {
        double hitFrames = getFramesUntilCollision(ship, ast, predictionLag);
        if (hitFrames > 0  &&  hitFrames <= predictionLag) {
          return true;
        }
      }
    }
    return false;
  }

}
