// ============================================================================
// File:               $File$
//
// Project:            
//
// Purpose:            
//
// Author:             Rammi
//
// Copyright Notice:   (c) 2008  Rammi (rammi@caff.de)
//                     This code is in the public domain.
//                     Use at own risk.
//                     No guarantees given.
//
// Latest change:      $Date$
//
// History:	       $Log$
//=============================================================================
package de.caff.asteroid;

import de.caff.util.Tools;

import java.awt.*;
import java.awt.geom.AffineTransform;
import java.awt.geom.GeneralPath;
import java.awt.geom.Point2D;
import java.util.Collection;

/**
 *
 *  A moving game object has a position and a velocity and it can be drawn.
 * 
 *  This is part of a solution for a competition
 *  <a href="http://www.heise.de/ct/creativ/08/02/details/">by the German computer magazine c't</a>
 */
public abstract class MovingGameObject
        extends GameObject
{
  /** Score of objects which can't be hit. */
  public static final int NO_SCORE = 0;
  /** TEST: calculate maximum velocities? */
  protected static final boolean WATCH_VELOCITIES = false;

  // if you are interested which y coordinates are used change the "if (false)" line in contructor to "if (true)"
  private static int minY = Integer.MAX_VALUE;
  private static int maxY = Integer.MIN_VALUE;

  /** Size of head of velocity arrow. */
  private static final int ARROW_HEAD_SIZE = 8;

  /** The arrow head for velocity drawings. */
  private static final GeneralPath ARROW_HEAD = new GeneralPath();
  static {
    ARROW_HEAD.moveTo(0, 0);
    ARROW_HEAD.lineTo(-ARROW_HEAD_SIZE, ARROW_HEAD_SIZE);
    ARROW_HEAD.lineTo(-ARROW_HEAD_SIZE, -ARROW_HEAD_SIZE);
    ARROW_HEAD.closePath();
  }

  /** The x coordinate of the velocity vector. */
  private double vx;
  /** The y coordinate of the velocity vector. */
  private double vy;
  /** Lifetime of this object (set externally). */
  private int lifetime;
  /** The identity of this object (set externally). */
  private Integer identity;
  /** The next incarnation of this object. */
  private MovingGameObject reincarnation;

  /**
   *  Constructor.
   *  @param x  x coordinate
   *  @param y  y coordinate
   */
  protected MovingGameObject(int x, int y)
  {
    super(x, y);
    if (false) {
      if (y < minY) {
        minY = y;
        System.out.println("new min/max: "+minY+","+maxY);
      }
      if (y > maxY) {
        maxY = y;
        System.out.println("new min/max: "+minY+","+maxY);
      }
    }
  }

  /**
   *  Get the center of the object.
   *  @return center point
   */
  public Point getCenter()
  {
    return new Point(getX(), getY());
  }

  /**
   *  Get the size of the object.
   *
   *  The size returned by this method is half the length of a square which contains the object,
   *  so the object's bounding box is between (x - size, y - size) and (x + size, y + size).
   *  @return object size
   */
  public abstract int getSize();

  /**
   *  Draw the object.
   *  @param g graphics context
   */
  public void draw(Graphics2D g)
  {
    g.setColor(Color.white);
    int size = getSize();
    g.drawOval(getX() - size, getY() - size, 2*size, 2*size);

    drawVelocityVector(g, Color.red);
  }

  /**
   *  Get the squared size of this object.
   *  @return squared size
   */
  public int getSquaredSize()
  {
    int size = getSize();
    return size*size;
  }

  /**
   *  Set the velocity.
   *
   *  The velocity is the step between frames.
   *
   *  It is not calculated internally but has to be set from outside. For an example see
   *  {@link de.caff.asteroid.SimpleVelocityPreparer#prepareFrames(java.util.LinkedList)}.
   *  @param x x coordinate of velocity
   *  @param y y coordinate of velocity
   */
  public void setVelocity(double x, double y)
  {
    vx = x;
    vy = y;
  }

  /**
   *  Set the velocity.
   *
   *  The velocity is the step between frames.
   *  @param v velocity vector
   */
  public void setVelocity(Point2D v)
  {
    setVelocity(v.getX(), v.getY());
  }

  /**
   *  Set the velocity assuming that the given object is at the same place as
   *  this object in the last frame.
   *
   *  @param obj comparision object (<code>null</code> allowed, but then nothing happens)
   */
  public void setVelocityFromDelta(MovingGameObject obj)
  {
    if (obj != null) {
      setVelocity(obj.getDelta(this));
    }
  }

  /**
   *  Get the velocity in x.
   *  @return x component of velocity vector
   */
  public double getVelocityX()
  {
    return vx;
  }

  /**
   *  Get the velocity in y.
   *  @return y component of velocity vector
   */
  public double getVelocityY()
  {
    return vy;
  }

  /**
   *  Get the velocity vector.
   *  @return velocity vector (movement between frames)
   */
  public Point2D getVelocity()
  {
    return new Point2D.Double(vx, vy);
  }

  /**
   *  Get the velocity angle.
   *
   *  The angle is measured counterclockwise, with <code>0</code> pointing to the right
   *  @return velocity in radians, or -Math.PI if ship has no velocity
   */
  public double getVelocityAngle()
  {
    return vx != 0 || vy != 0  ?  Math.atan2(vy, vx)  :  -Math.PI;
  }

  /**
   *  Has this object a velocity.
   *  @return velocity
   */
  public boolean hasKnownVelocity()
  {
    return vx != 0 || vy != 0;
  }

  /**
   *  Draw a vector displaying the current velocity.
   *  @param g graphics context
   *  @param color color to use
   */
  protected void drawVelocityVector(Graphics2D g, Color color)
  {
    if (vx != 0  ||  vy != 0) {
      int scale = 16;
      g.setColor(color);
      g.drawLine(getX(), getY(), getX() + (int)(scale*vx), getY() + (int)(scale*vy));
      double angle = Math.atan2(vy, vx);
      AffineTransform at = AffineTransform.getTranslateInstance(getX() + scale*vx, getY() + scale*vy);
      at.concatenate(AffineTransform.getRotateInstance(angle));
      g.fill(at.createTransformedShape(ARROW_HEAD));
    }
  }

  /**
   * Get the bounding box of this rectangle.
   *
   * @return the bounding box
   */
  public Rectangle getBounds()
  {
    Point center = getCenter();
    int size = getSize();
    return new Rectangle(center.x - size, center.y - size, 2*size, 2*size);
  }

  /**
   *  Get the position in the next frame assuming that the velocity is constant.
   *  @return position in next frame
   */
  public Point getNextLocation()
  {
    return new Point((int)(getCorrectedX()+vx),
                     (int)(getCorrectedY()+vy));
  }

  /**
   *  Get the corrected postion in the next frame assuming the velocity is constant.
   *  @return position in next frame
   */
  public Point2D getCorrectedNextLocation()
  {
    return new Point2D.Double(getCorrectedX() + vx, getCorrectedY() + vy);
  }

  /**
   *  Get the position in a coming (or gone) frame assuming that the velocity is constant.
   *  @param nrFrames number of frames to skip
   *  @return the position
   */
  public Point getPredictedLocation(int nrFrames)
  {
    return new Point((int)(getCorrectedX() + nrFrames * vx),
                     (int)(getCorrectedY() + nrFrames * vy));
  }

  /**
   *  Get the position in a coming (or gone) frame assuming that the velocity is constant.
   *  @param nrFrames number of frames to skip
   *  @return the position
   */
  public Point2D getCorrectedPredictedLocation(double nrFrames)
  {
    return new Point2D.Double(getCorrectedX() + nrFrames * vx,
                              getCorrectedY() + nrFrames * vy);
  }

  /**
   * Get the properties of this object.
   *
   * @return collection of properties
   */
  @Override
  public Collection<Property> getProperties()
  {
    Collection<Property> props = super.getProperties();
    props.add(new Property<Integer>("ID", getIdentity()));
    props.add(new Property<Integer>("Size", getSize()));
    props.add(new Property<Rectangle>("Bounds", getBounds()));
    props.add(new Property<Point2D>("Velocity", getVelocity()));
    return props;
  }

  /**
   *  Get the score which is added if this object is hit.
   *  @return score or {@link #NO_SCORE}
   */
  public int getScore()
  {
    return NO_SCORE;
  }

  /**
   *  Get the lifetime of this buller.
   *
   *  Because it is not always easy to connect two bullets between frames the
   *  returned lifetime is not 100% correct. Also it is not calculated
   *  internally but has to be set from outside. For an example see
   *  {@link SimpleVelocityPreparer#prepareFrames(java.util.LinkedList)}.
   *  @return lifetime (number of frames this bullet was displayed)
   */
  public int getLifetime()
  {
    return lifetime;
  }

  /**
   *  Set the lifetime.
   *  @param lifetime life time (number of frames 'this' bullet exists)
   */
  public void setLifetime(int lifetime)
  {
    this.lifetime = lifetime;
  }

  /**
   *  Get the identity of this object.
   *  The identity has to be set from the outside.
   *  @return identity or <code>null</code> if not set
   */
  public Integer getIdentity()
  {
    return identity;
  }

  /**
   *  Set the identity of this object.
   *  @param identity identity
   */
  public void setIdentity(Integer identity)
  {
    this.identity = identity;
  }

  /**
   *  Inheret properties from another game object.
   *  This basic implementation sets the identity to the same value as
   *  the one of the other object.
   *  @param obj other object, probably of previous frame
   */
  public void inheret(MovingGameObject obj)
  {
    setIdentity(obj.getIdentity());
    obj.reincarnation = this;
    lifetime = obj.lifetime + 1;
  }

  /**
   *  Will the two game objects hit in the given number of frames?
   *  @param other  other game object
   *  @param frames number of frames
   *  @return the answer
   */
  public boolean _isHitting(MovingGameObject other, int frames)
  {
    Point2D delta = getTorusDelta(getCorrectedPredictedLocation(frames), other.getCorrectedPredictedLocation(frames));
    if (true) {
      return delta.getX()*delta.getX() + delta.getY()*delta.getY() < Tools.square(getSize() + other.getSize());
    }
    else {
      int size = getSize() + other.getSize();
      return Math.abs(delta.getX()) <= size  &&  Math.abs(delta.getY()) <= size;
    }
  }

  /**
   *  Will the two game objects hit in the given number of frames?
   *  @param other  other game object
   *  @param frames number of frames
   *  @return the answer
   */
  public boolean isHitting(MovingGameObject other, int frames)
  {
    Point2D delta1 = getTorusDelta(getCorrectedPredictedLocation(frames), other.getCorrectedPredictedLocation(frames));
    double size = getSize() + other.getSize();
    double len1 = Tools.getLength(delta1);
    if (len1 < size) {
      return true;
    }
    Point2D delta = getTorusDelta(getCorrectedPredictedLocation(frames-1), other.getCorrectedPredictedLocation(frames-1));
    if (delta.getX() * delta1.getX() > 0  && delta.getY() * delta1.getY() > 0) {
      return false;
    }
    delta.setLocation(delta.getX() - delta1.getX(),
                      delta.getY() - delta1.getY());
    double len = Tools.getLength(delta);
    return len != 0 && Math.abs(Tools.crossProduct(delta, delta1) / len) < size;
  }

  /**
   *  Get the next incarnation of this object.
   *  @return the object in the next frame, which is associated with this object
   */
  public MovingGameObject getReincarnation()
  {
    return reincarnation;
  }
}
