// ============================================================================
// 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.geom.AffineTransform;
import java.awt.geom.GeneralPath;
import java.awt.*;
import java.util.*;
import java.io.IOException;

/**
 *  Common stuff for players by Rammi.
 */
public abstract class AbstractBasicAsteroidPlayer
        implements GameData
{
  private static final int BASE = 4;
  protected static final GeneralPath HIT_POINT = new GeneralPath();
  static {
    HIT_POINT.moveTo( 3*BASE,    BASE);
    HIT_POINT.lineTo(   BASE,    BASE);
    HIT_POINT.lineTo(   BASE,  3*BASE);
    HIT_POINT.lineTo(-1*BASE,  3*BASE);
    HIT_POINT.lineTo(-1*BASE,    BASE);
    HIT_POINT.lineTo(-3*BASE,    BASE);
    HIT_POINT.lineTo(-3*BASE, -1*BASE);
    HIT_POINT.lineTo(-1*BASE, -1*BASE);
    HIT_POINT.lineTo(-1*BASE, -3*BASE);
    HIT_POINT.lineTo(   BASE, -3*BASE);
    HIT_POINT.lineTo(   BASE, -1*BASE);
    HIT_POINT.lineTo( 3*BASE, -1*BASE);
    HIT_POINT.closePath();
  }
  protected static final double ROT_ANGLE = Math.PI/42;
  protected static final double ROT_BORDER = Math.sin(ROT_ANGLE /2);

  static class RotateAndWait
          implements Comparable<RotateAndWait>
  {
    private final int rotation;
    private final int wait;
    private final int fly;

    public RotateAndWait(int rotation, int wait, int fly)
    {
      this.rotation = rotation;
      this.wait = wait;
      this.fly  = fly;
    }

    public int getRotation()
    {
      return rotation;
    }

    public int getWait()
    {
      return wait;
    }

    public int getFly()
    {
      return fly;
    }

    public int getShootFrames()
    {
      return Math.abs(rotation) + wait;
    }

    public int getHitFrames()
    {
      return getShootFrames() + fly;
    }

    public int compareTo(RotateAndWait o)
    {
      return getHitFrames() - o.getHitFrames();
    }
  }

  protected static interface InfoDrawer
  {
    public void setDanger(Target danger);
    public void addHitTarget(MovingGameObject target, Point2D hitPos);
  }

  protected static final InfoDrawer NULL_INFO_DRAWER = new InfoDrawer() {
    public void setDanger(Target danger)
    {
    }

    public void addHitTarget(MovingGameObject target, Point2D hitPos)
    {
    }
  };

  protected static class DefaultInfoDrawer
          implements InfoDrawer,
                     Drawable
  {
    private static final Color[] COLOR_CYCLE = {
            Color.red, Color.magenta, Color.cyan, Color.orange
    };
    private Collection<Target> targets = new LinkedList<Target>();
    private Target danger;

    public void setDanger(Target danger)
    {
      this.danger = danger;
    }

    public void addHitTarget(MovingGameObject obj, Point2D hitPos)
    {
      Target target = new Target(obj);
      target.setHitPoint(hitPos);
      targets.add(target);
    }

    /**
     * Draw the object.
     *
     * @param g graphics context
     */
    public void draw(Graphics2D g)
    {
      Stroke stroke = g.getStroke();
      g.setStroke(new BasicStroke(4));
      if (danger != null) {
        g.setColor(Color.red);
        g.drawLine(danger.getX() - danger.getSize(), danger.getY() - danger.getSize(),
                   danger.getX() + danger.getSize(), danger.getY() + danger.getSize());
        g.drawLine(danger.getX() - danger.getSize(), danger.getY() + danger.getSize(),
                   danger.getX() + danger.getSize(), danger.getY() - danger.getSize());
      }
      int c = 0;
      for (Target target: targets) {
        g.setColor(COLOR_CYCLE[c]);
        c = (c+1) % COLOR_CYCLE.length;
        target.draw(g);
      }
      g.setStroke(stroke);
    }
  }

  protected static class Target
  {
    private final MovingGameObject object;
    private Point2D                hitPoint;

    protected Target(MovingGameObject object)
    {
      this.object = object;
    }

    public MovingGameObject getObject()
    {
      return object;
    }

    public Point2D getVelocity()
    {
      return object.getVelocity();
    }

    public int getX()
    {
      return object.getX();
    }

    public int getY()
    {
      return object.getY();
    }

    public int getSize()
    {
      return object.getSize();
    }

    public double getVX()
    {
      return object.getVelocityX();
    }

    public double getVY()
    {
      return object.getVelocityY();
    }

    public void setHitPoint(Point2D hitPoint)
    {
      this.hitPoint = hitPoint;
    }

    public void draw(Graphics2D g)
    {
      g.fill(AffineTransform.getTranslateInstance(hitPoint.getX(), hitPoint.getY()).createTransformedShape(
              AsteroidPlayer.HIT_POINT));
      int size = object.getSize();
      g.drawOval(object.getX() - size,
                 object.getY() - size,
                 2* size, 2* size);
    }
  }

  protected Communication com;
  private boolean hyperPressedInLastFrame;
  private boolean firedInLastFrame;
  private BulletLifetimeInformation bulletInformation8;
  private BulletLifetimeInformation bulletInformation16;
  private BulletLifetimeInformation bulletInformation32;
  private static final int MAGICAL_HORIZONT = (6+8)*Bullet.MIN_LIFETIME;
  private static final double DECISION_ANGLE1 = Math.acos(0.5*EXTENT_Y/MAGICAL_HORIZONT);
  private static final double DECISION_ANGLE2 = Math.atan2(EXTENT_Y, EXTENT_X);
  private static final double DECISION_ANGLE3 = Math.acos(0.5*EXTENT_X/MAGICAL_HORIZONT);

  /** Constructor only for memory hogging. */
  protected AbstractBasicAsteroidPlayer()
  {
    try {
      final int overscan = 2;
      bulletInformation8  = BulletLifetimeInformation.getLifetimeInformation(8, overscan);
      //bulletInformation16 = BulletLifetimeInformation.getLifetimeInformation(16, overscan);
      //bulletInformation32 = BulletLifetimeInformation.getLifetimeInformation(32, overscan);
    } catch (IOException e) {
      e.printStackTrace();
      System.exit(2);
    }
  }

  protected AbstractBasicAsteroidPlayer(Communication com)
  {
    this();
    this.com = com;
  }

  private BulletLifetimeInformation getLifetimeInformation(int size)
  {
    return bulletInformation8;
    /*
    if (size < 16  ||  bulletInformation16 == null) {
      return bulletInformation8;
    }
    else if (bulletInformation32 == null  ||  size < 32) {
      return bulletInformation16;
    }
    else {
      return bulletInformation32;
    }
    */
  }

  protected void pushButton(int button)
  {
    if (com != null) {
      if (hyperPressedInLastFrame) {
        button &= ~GameData.BUTTON_HYPERSPACE;
      }
      if (firedInLastFrame) {
        button &= ~GameData.BUTTON_FIRE;
      }
      com.pushButton(button);
      hyperPressedInLastFrame = (button & GameData.BUTTON_HYPERSPACE) != 0;
      firedInLastFrame = (button & GameData.BUTTON_FIRE) != 0;
    }
  }

  protected boolean haveFiredInLastFrame()
  {
    return firedInLastFrame;
  }

  protected static Point2D getShootDirection(SpaceShip ship, AsteroidPlayer.Target target)
  {
    return getShootDirection(ship, target.getObject());
  }

  static Point2D getShootDirection(SpaceShip ship, MovingGameObject target)
  {
    final int bulletSpeed = 8;
    Point2D shipPos   = ship.getCorrectedNextLocation();
    Point2D targetVel = new Point2D.Double(target.getVelocityX() - ship.getVelocityX(),
                                           target.getVelocityY() - ship.getVelocityY());
    Point2D targetPos = new Point2D.Double(target.getCorrectedX() + target.getVelocityX(),
                                           target.getCorrectedY() + target.getVelocityY());

    double deltaX = GameObject.normalizeDeltaX(targetPos.getX() - shipPos.getX());
    double deltaY = GameObject.normalizeDeltaY(targetPos.getY() - shipPos.getY());
    double vt2 = Tools.square(targetVel.getX()) + Tools.square(targetVel.getY());
    double vb2 = bulletSpeed * bulletSpeed;
    double f2 = vt2 - vb2;
    double q2 = -4*(deltaX*deltaX + deltaY*deltaY)*f2 + vt2;
    if (q2 >= 0) {
      double q = 0.5*Math.sqrt(q2)/f2;
      double p = -0.5*Tools.scalarProduct(deltaX, deltaY, targetVel.getX(), targetVel.getY())/f2;
      double lambda1 = p + q;
      double lambda2 = p - q;
      double lambda;
      if (lambda1 > 0) {
        if (lambda2 > 0) {
          lambda = Math.min(lambda1, lambda2);
        }
        else {
          lambda = lambda1;
        }
      }
      else if (lambda2 > 0) {
        lambda = lambda2;
      }
      else {
        return null;
      }
      return new Point2D.Double(deltaX + lambda * targetVel.getX(),
                                deltaY + lambda * targetVel.getY());
    }
    return null;
  }

  static int getFramesUntilCollision(MovingGameObject obj1, MovingGameObject obj2, int maxFrames)
  {
    Point2D pos1 = obj1.getCorrectedNextLocation();
    Point2D pos2 = obj2.getCorrectedNextLocation();
    double deltaVX = obj1.getVelocityX() - obj2.getVelocityX();
    double deltaVY = obj1.getVelocityY() - obj2.getVelocityY();
    double deltaX = pos2.getX() - pos1.getX();
    double deltaY = pos2.getY() - pos1.getY();
    double size = obj1.getSize() + obj2.getSize();
    if (deltaVX != 0) {
      if (deltaX < -size  &&  deltaVX > 0) {
        deltaX += GameData.EXTENT_X;
      }
      else if (deltaX > size  &&  deltaVX < 0) {
        deltaX -= GameData.EXTENT_X;
      }
    }
    if (deltaVY != 0) {
      if (deltaY < -size  &&  deltaVY > 0) {
        deltaY += GameData.EXTENT_Y;
      }
      else if (deltaY > size  &&  deltaVY < 0) {
        deltaY -= GameData.EXTENT_Y;
      }
    }
    /*
    List<Point2D> possibleTargetPositions = getRelevantOffsets(new Point2D.Double(deltaX, deltaY),
                                                               deltaVX, deltaVY);
    */

    double vLen = Tools.getLength(deltaVX, deltaVY);
    if (vLen > 0) {
      double dist = Math.abs(Tools.crossProduct(deltaX, deltaY, deltaVX, deltaVY) / vLen);
      if (dist <= 2*size) {
        // possibly hitting
        int minFrames = (int)Math.floor(Math.sqrt(deltaX*deltaX+deltaY*deltaY-size*size) / vLen);
        if (minFrames <= maxFrames) {
          int frames    = (int)Math.ceil(Math.sqrt(deltaX*deltaX+deltaY*deltaY) / vLen);
          if (minFrames < 0) {
            minFrames = 0;
          }
          boolean hits = false;
          while (frames >= minFrames) {
            if (obj1.isHitting(obj2, frames)) {
              hits = true;
              break;
            }
            --frames;
          }
          if (hits) {
            while (frames > minFrames  &&  obj1.isHitting(obj2, frames)) {
              --frames;
            }
            return frames + 1;
          }
        }
      }
    }
    return 0;
  }

  protected static double bulletWillHit(SpaceShip ship, FrameInfo.Direction bulletDir, MovingGameObject target)
  {
    Point2D shipPos   = ship.getCorrectedNextLocation();
    Point2D targetPos = target.getCorrectedNextLocation();
    shipPos.setLocation(shipPos.getX() + bulletDir.getDisplacement().x,
                        shipPos.getY() + bulletDir.getDisplacement().y);
    double deltaVX = bulletDir.getBulletVelocity().getX() + ship.getVelocityX() - target.getVelocityX();
    double deltaVY = bulletDir.getBulletVelocity().getY() + ship.getVelocityY() - target.getVelocityY();
    Point2D delta = GameObject.getTorusDelta(targetPos, shipPos);
    java.util.List<Point2D> possibleTargetPositions = getRelevantOffsets(delta, deltaVX, deltaVY);

    //int size = target.getSize() + Bullet.BULLET_SIZE - 1;
    int size = 6 + Bullet.BULLET_SIZE - 1;
    double vLen = Tools.getLength(deltaVX, deltaVY);
    if (vLen > 0) {
      double minFrames = 0.0;
      for (Point2D pos: possibleTargetPositions) {
        double dist = Tools.crossProduct(pos.getX(), pos.getY(), deltaVX, deltaVY) / vLen;
        if (Math.abs(dist) < size  &&  Tools.scalarProduct(pos.getX(), pos.getY(), deltaVX, deltaVY) >= 0) {
          // hitting
          double frames = Math.ceil((Math.sqrt(pos.getX()*pos.getX()+pos.getY()*pos.getY() - dist*dist) - Math.sqrt(size*size - dist*dist)) / vLen);
          if (frames < Bullet.MIN_LIFETIME) {
            if (frames > 0) {
              if (minFrames == 0  ||  frames < minFrames) {
                minFrames = frames;
              }
            }
          }
        }
      }
      return minFrames;
    }
    return 0.0;
  }

  private static java.util.List<Point2D> getRelevantOffsets(Point2D delta, double deltaVX, double deltaVY)
  {
    java.util.List<Point2D> possibleTargetPositions = new ArrayList<Point2D>(9);
    possibleTargetPositions.add(delta);
    // double deltaLen = Tools.getLength(delta);
    double angle = Math.atan2(deltaVY, deltaVX);
    double absAngle = Math.abs(angle);
    if (absAngle < DECISION_ANGLE3) {
      possibleTargetPositions.add(new Point2D.Double(delta.getX() + EXTENT_X, delta.getY()));
      if (absAngle >= DECISION_ANGLE1) {
        possibleTargetPositions.add(new Point2D.Double(delta.getX() + EXTENT_X, delta.getY() + deltaVY > 0 ? EXTENT_Y : -EXTENT_Y));
        if (absAngle >= DECISION_ANGLE2) {
          possibleTargetPositions.add(new Point2D.Double(delta.getX(), delta.getY() + deltaVY > 0 ? EXTENT_Y : -EXTENT_Y));
        }
      }
    }
    else if (absAngle > Math.PI - DECISION_ANGLE3) {
      possibleTargetPositions.add(new Point2D.Double(delta.getX() - EXTENT_X, delta.getY()));
      if (absAngle <= Math.PI - DECISION_ANGLE1) {
        possibleTargetPositions.add(new Point2D.Double(delta.getX() - EXTENT_X, delta.getY() + deltaVY > 0 ? EXTENT_Y : -EXTENT_Y));
        if (absAngle <= Math.PI - DECISION_ANGLE2) {
          possibleTargetPositions.add(new Point2D.Double(delta.getX(), delta.getY() + deltaVY > 0 ? EXTENT_Y : -EXTENT_Y));
        }
      }
    }
    else {
      possibleTargetPositions.add(new Point2D.Double(delta.getX(), delta.getY() + deltaVY > 0 ? EXTENT_Y : -EXTENT_Y));
    }
    assert(possibleTargetPositions.size() <= 4);
    return possibleTargetPositions;
  }

  /**
   *  Get the fastest rotation and wait value for destroying a target.
   *  @param ship              the ship
   *  @param dirByte           direction byte in next frame
   *  @param target            the target
   *  @param maxSize           maximum size to use for target
   *  @param minFutureFrames   minimal frames to look for in the future
   *  @param maxFutureFrames   maximum frames to look for in the future
   *  @return best value to destroy target in the given frame range or <code>null</code> if there is no possibility to destroy the target
   */
  protected RotateAndWait getRotationForHit(SpaceShip ship, byte dirByte, MovingGameObject target, int maxSize, int minFutureFrames, int maxFutureFrames)
  {
    Point2D shipPos   = ship.getCorrectedNextLocation();
    Point2D targetPos = target.getCorrectedNextLocation();
    Point2D delta = GameObject.getTorusDelta(targetPos, shipPos);
    double tvx = target.getVelocityX();
    double tvy = target.getVelocityY();
    if (tvx == 0  &&  tvy == 0) {
      return null;
    }
    double deltaVX = tvx - ship.getVelocityX();
    double deltaVY = tvy - ship.getVelocityY();
    double distance = Tools.getLength(delta);
    double speed    = Tools.getLength(deltaVX, deltaVY);
    int minFrame = Math.max((int)Math.floor(distance/(speed + Bullet.BULLET_SPEED)),
                            minFutureFrames);

    RotateAndWait result = null;
    for (int frame = minFrame;  frame <= maxFutureFrames;  ++frame) {
      BulletLifetimeInformation.BulletFrameInfo[] hitInfos =
              getLifetimeInformation(Math.min(maxSize, target.getSize())).getHitBulletInfos(delta.getX() + frame*deltaVX,
                                                                                            delta.getY() + frame*deltaVY);
      for (BulletLifetimeInformation.BulletFrameInfo info: hitInfos) {
        int framesTillShoot = frame - info.getFrame();
        if (framesTillShoot > 0) {
          int deltaDir = (byte)(info.getDirection() - dirByte);
          if (Math.abs(deltaDir) < framesTillShoot) { // can reach shoot angle
            return new RotateAndWait(deltaDir, framesTillShoot - Math.abs(deltaDir) - 1, info.getFrame());
          }
        }
      }
    }
    return result;
  }

  /**
   *  Call this to cleanup any resources taken by this object.
   */
  public abstract void destroy();
}
