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

import de.caff.asteroid.*;
import de.caff.asteroid.analysis.FrameKeyInfo;
import de.caff.util.Tools;
import de.caff.i18n.I18n;

import java.util.*;

/**
 *  How many bullets are fired, and how many bullets have hit, what objects were it etc.
 *
 *  @author <a href="mailto:rammi@caff.de">Rammi</a>
 *  @version $Revision$
 */
public class BulletStatistics
        extends AbstractBasicDumpFileStatistics
{
  private static class BulletStats
          extends AbstractBasicGameObjectVisitor
  {
    private int missed;
    private int asteroidHits;
    private int ufoHits;
    private int shipHits;

    /**
     * Handle an asteroid.
     *
     * @param asteroid asteroid to handle
     */
    @Override
    public void handle(Asteroid asteroid)
    {
      ++asteroidHits;
    }

    /**
     * Handle a space ship.
     *
     * @param ship space ship to handle
     */
    @Override
    public void handle(SpaceShip ship)
    {
      ++shipHits;
    }

    /**
     * Handle an ufo.
     *
     * @param ufo ufo to handle
     */
    public void handle(Ufo ufo)
    {
      ++ufoHits;
    }

    public void addMissed()
    {
      ++missed;
    }

    public int getMissed()
    {
      return missed;
    }

    public int getAsteroidHits()
    {
      return asteroidHits;
    }

    public int getShipHits()
    {
      return shipHits;
    }

    public int getUfoHits()
    {
      return ufoHits;
    }

    public int getHits()
    {
      return asteroidHits + shipHits + ufoHits;
    }

    public int getShots()
    {
      return getHits() + getMissed();
    }

    public void addToProperties(Collection<Property> props, String prefix)
    {
      props.add(new Property<Integer>(prefix+I18n.getString("propBulTotalShoots"), getShots()));
      props.add(new Property<Integer>(prefix+I18n.getString("propBulHits"), getHits()));
      props.add(new Property<Integer>(prefix+I18n.getString("propBulHitsAst"), getAsteroidHits()));
      props.add(new Property<Integer>(prefix+I18n.getString("propBulHitsUfo"), getUfoHits()));
      props.add(new Property<Integer>(prefix+I18n.getString("propBulHitsShip"), getShipHits()));
      props.add(new Property<Integer>(prefix+I18n.getString("propBulMissed"), getMissed()));
      props.add(new Property<String>(prefix+I18n.getString("propBulHitRatio"), percentify(getHits(), getShots())));
    }
  }

  private static class BulletObjectPair
          implements Comparable<BulletObjectPair>
  {
    private final Bullet bullet;
    private final MovingGameObject object;
    private final double distSquared;

    BulletObjectPair(Bullet bullet, MovingGameObject object, double distSquared)
    {
      this.bullet = bullet;
      this.object = object;
      this.distSquared = distSquared;
    }

    public Bullet getBullet()
    {
      return bullet;
    }

    public MovingGameObject getObject()
    {
      return object;
    }

    /**
     * Compares this object with the specified object for order.  Returns a
     * negative integer, zero, or a positive integer as this object is less
     * than, equal to, or greater than the specified object.<p>
     * <p/>
     * In the foregoing description, the notation
     * <tt>sgn(</tt><i>expression</i><tt>)</tt> designates the mathematical
     * <i>signum</i> function, which is defined to return one of <tt>-1</tt>,
     * <tt>0</tt>, or <tt>1</tt> according to whether the value of <i>expression</i>
     * is negative, zero or positive.
     * <p/>
     * The implementor must ensure <tt>sgn(x.compareTo(y)) ==
     * -sgn(y.compareTo(x))</tt> for all <tt>x</tt> and <tt>y</tt>.  (This
     * implies that <tt>x.compareTo(y)</tt> must throw an exception iff
     * <tt>y.compareTo(x)</tt> throws an exception.)<p>
     * <p/>
     * The implementor must also ensure that the relation is transitive:
     * <tt>(x.compareTo(y)&gt;0 &amp;&amp; y.compareTo(z)&gt;0)</tt> implies
     * <tt>x.compareTo(z)&gt;0</tt>.<p>
     * <p/>
     * Finally, the implementer must ensure that <tt>x.compareTo(y)==0</tt>
     * implies that <tt>sgn(x.compareTo(z)) == sgn(y.compareTo(z))</tt>, for
     * all <tt>z</tt>.<p>
     * <p/>
     * It is strongly recommended, but <i>not</i> strictly required that
     * <tt>(x.compareTo(y)==0) == (x.equals(y))</tt>.  Generally speaking, any
     * class that implements the <tt>Comparable</tt> interface and violates
     * this condition should clearly indicate this fact.  The recommended
     * language is "Note: this class has a natural ordering that is
     * inconsistent with equals."
     *
     * @param o the Object to be compared.
     * @return a negative integer, zero, or a positive integer as this object
     *         is less than, equal to, or greater than the specified object.
     * @throws ClassCastException if the specified object's type prevents it
     *                            from being compared to this Object.
     */
    public int compareTo(BulletObjectPair o)
    {
      return Double.compare(distSquared, o.distSquared);
    }
  }

  /** Bullets which are fired by ship. */
  private BulletStats friendly = new BulletStats();
  /** Bullets which are fired by ufo. */
  private BulletStats unfriendly = new BulletStats();

  private BulletStats getStatsForBullet(Bullet bullet)
  {
    return bullet.isFriendly() == Boolean.TRUE  ?
            friendly :
            unfriendly;
  }

  /**
   * Analyse the frames.
   *
   * @param infos frame key infos to analyse
   */
  public void analyse(Collection<FrameKeyInfo> infos)
  {
    FrameKeyInfo lastInfo = null;
    List<Bullet> missingBullets           = new LinkedList<Bullet>(); // reused
    List<MovingGameObject> missingObjects = new LinkedList<MovingGameObject>(); // reused
    for (FrameKeyInfo info: infos) {
      missingBullets.clear();
      missingObjects.clear();
      if (lastInfo != null) {
        // collect missing bullets
        FrameInfo lastFrameInfo = lastInfo.getFrameInfo();
        for (Bullet bullet: lastFrameInfo.getBullets()) {
          if (bullet.getReincarnation() == null) {
            missingBullets.add(bullet);
          }
        }
        if (!missingBullets.isEmpty()) {
          for (Asteroid ast: lastFrameInfo.getAsteroids()) {
            if (ast.getReincarnation() == null) {
              missingObjects.add(ast);
            }
          }
          if (lastFrameInfo.getUfo() != null  &&  info.getFrameInfo().getUfo() == null) {
            missingObjects.add(lastFrameInfo.getUfo());
          }
          if (lastFrameInfo.getSpaceShip() != null  &&  info.getFrameInfo().getSpaceShip() == null  &&
              lastFrameInfo.getNrShips() > info.getFrameInfo().getNrShips()) { // todo: maybe check for ship explosion
            missingObjects.add(lastFrameInfo.getSpaceShip());
          }
          List<BulletObjectPair> possibleHits = new LinkedList<BulletObjectPair>();
          for (Bullet bullet: missingBullets) {
            for (MovingGameObject object: missingObjects) {
              double dist = Tools.getSquaredLength(bullet.getCorrectedDelta(object));
              if (dist < Tools.square(bullet.getSize() + object.getSize())) {
                possibleHits.add(new BulletObjectPair(bullet, object, dist));
              }
            }
          }
          Collections.sort(possibleHits);
          while (!possibleHits.isEmpty()) {
            BulletObjectPair pair = possibleHits.get(0);
            pair.getObject().visitedBy(getStatsForBullet(pair.getBullet()));
            for (ListIterator<BulletObjectPair> it = possibleHits.listIterator();  it.hasNext();  ) {
              BulletObjectPair bop = it.next();
              if (pair.getBullet() == bop.getBullet()  ||
                      pair.getObject() == bop.getObject()) {
                it.remove();
              }
            }
            missingBullets.remove(pair.getBullet());
          }
          for (Bullet bullet: missingBullets) {
            getStatsForBullet(bullet).addMissed();
          }
        }
      }
      lastInfo = info;
    }
  }

  /**
   * Get a title for this statistics.
   *
   * @return statistics title
   */
  public String getTitle()
  {
    return I18n.getString("titleBulletStat");
  }

  /**
   * Does this statistic need prepared frames?
   *
   * @return the answer
   */
  public boolean needPreparation()
  {
    return true;
  }

  /**
   * Get the properties of this object.
   *
   * @return collection of properties
   */
  public Collection<Property> getProperties()
  {
    Collection<Property> props = new LinkedList<Property>();
    friendly.addToProperties(props, I18n.getString("propPrefixBulFriendly")+" ");
    unfriendly.addToProperties(props, I18n.getString("propPrefixBulUnfriendly")+" ");
    return props;
  }
}
