// ============================================================================
// 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.asteroid.analysis.statistics.AbstractBasicDumpFileStatistics;
import de.caff.asteroid.analysis.FrameKeyInfo;
import de.caff.asteroid.analysis.DumpFile;
import de.caff.util.Tools;

import java.util.*;
import java.awt.geom.Point2D;
import java.io.IOException;
import java.io.FileWriter;

/**
 *  Find out what it means that a bullet hits an object.
 */
public class HitAnalysis
        extends AbstractBasicDumpFileStatistics
{
  public static class HitInfo
  {
    private final Bullet bullet;
    private final MovingGameObject object;
    private final int              frameIndex;

    HitInfo(Bullet bullet, MovingGameObject object, int frameIndex)
    {
      this.bullet = bullet;
      this.object = object;
      this.frameIndex = frameIndex;
    }

    public Bullet getBullet()
    {
      return bullet;
    }

    public MovingGameObject getObject()
    {
      return object;
    }

    public int getFrameIndex()
    {
      return frameIndex;
    }

    public Point2D getPreDestructionDelta()
    {
      return object.getCorrectedDelta(bullet);
    }

    public Point2D getDestructionDelta()
    {
      Point2D nextPosBullet = bullet.getCorrectedNextLocation();
      Point2D nextPosObject = object.getCorrectedNextLocation();
      return GameObject.getTorusDelta(nextPosBullet, nextPosObject);
    }

    public double getPreDestructionDistance()
    {
      return Tools.getLength(getPreDestructionDelta());
    }

    public double getDestructionDistance()
    {
      return Tools.getLength(getDestructionDelta());
    }
  }
  private class HitInfoDispatcher
          implements GameObjectVisitor
  {
    private HitInfo toDispatch;

    public void dispatch(HitInfo info)
    {
      toDispatch = info;
      info.getObject().visitedBy(this);
    }

    /**
     * Handle an asteroid.
     *
     * @param asteroid asteroid to handle
     */
    public void handle(Asteroid asteroid)
    {
      switch (asteroid.getScore()) {
      case Asteroid.SCORE_SMALL_ASTEROID: // small
        smallAsteroidsInfo.add(toDispatch);
        break;

      case Asteroid.SCORE_MIDDLE_ASTEROID: // middle
        middleAsteroidsInfo.add(toDispatch);
        break;

      case Asteroid.SCORE_LARGE_ASTEROID: // large
        largeAsteroidsInfo.add(toDispatch);
        break;
      }
    }

    /**
     * Handle a bullet.
     *
     * @param bullet bullet to handle
     */
    public void handle(Bullet bullet)
    {
      // nothing
    }

    /**
     * Handle an explosion.
     *
     * @param explosion explosion to handle
     */
    public void handle(Explosion explosion)
    {
      // nothing
    }

    /**
     * Handle a ship explosion.
     *
     * @param explosion ship explosion to handle
     */
    public void handle(ShipExplosion explosion)
    {
      // nothing
    }

    /**
     * Handle a space ship.
     *
     * @param ship space ship to handle
     */
    public void handle(SpaceShip ship)
    {
      shipsInfo.add(toDispatch);
    }

    /**
     * Handle a text.
     *
     * @param text text to handle
     */
    public void handle(Text text)
    {
      // nothing
    }

    /**
     * Handle an ufo.
     *
     * @param ufo ufo to handle
     */
    public void handle(Ufo ufo)
    {
      switch (ufo.getScore())
      {
      case Ufo.SCORE_SMALL_UFO:
        smallUfosInfo.add(toDispatch);
        break;
      case Ufo.SCORE_BIG_UFO:
        largeUfosInfo.add(toDispatch);
      }
    }
  }
  private List<HitInfo> smallAsteroidsInfo   = new LinkedList<HitInfo>();
  private List<HitInfo> middleAsteroidsInfo  = new LinkedList<HitInfo>();
  private List<HitInfo> largeAsteroidsInfo   = new LinkedList<HitInfo>();
  private List<HitInfo> smallUfosInfo        = new LinkedList<HitInfo>();
  private List<HitInfo> largeUfosInfo        = new LinkedList<HitInfo>();
  private List<HitInfo> shipsInfo            = new LinkedList<HitInfo>();
  private Collection<List<HitInfo>> hitInfoLists = new LinkedList<List<HitInfo>>();
  {
    hitInfoLists.add(smallAsteroidsInfo);
    hitInfoLists.add(middleAsteroidsInfo);
    hitInfoLists.add(largeAsteroidsInfo);
    hitInfoLists.add(smallUfosInfo);
    hitInfoLists.add(largeUfosInfo);
    hitInfoLists.add(shipsInfo);
  }
  private HitInfoDispatcher dispatcher = new HitInfoDispatcher();


  public static final Comparator<MovingGameObject> ID_COMPARATOR = new Comparator<MovingGameObject>()
  {
    public int compare(MovingGameObject o1, MovingGameObject o2)
    {
      return o1.getIdentity().intValue() - o2.getIdentity().intValue();
    }
  };

  public static final Comparator<HitInfo> PRE_DESTRUCTION_COMPARATOR = new Comparator<HitInfo>()
  {
    public int compare(HitInfo o1, HitInfo o2)
    {
      double delta = o1.getPreDestructionDistance() - o2.getPreDestructionDistance();
      return delta > 0 ? 1 : (delta < 0) ? -1 : 0;
    }
  };

  public static final Comparator<HitInfo> DESTRUCTION_COMPARATOR = new Comparator<HitInfo>()
  {
    public int compare(HitInfo o1, HitInfo o2)
    {
      double delta = o1.getDestructionDistance() - o2.getDestructionDistance();
      return delta > 0 ? 1 : (delta < 0) ? -1 : 0;
    }
  };

  public static final Comparator<HitInfo> FRAME_COMPARATOR = new Comparator<HitInfo>()
  {
    public int compare(HitInfo o1, HitInfo o2)
    {
      return o1.getFrameIndex() - o2.getFrameIndex();
    }
  };

  private static final Comparator<HitInfo> HITINFO_COMPARATOR = DESTRUCTION_COMPARATOR;

  private boolean collecting;

  public HitAnalysis()
  {
    this(false);
  }

  public HitAnalysis(boolean collecting)
  {
    this.collecting = collecting;
  }

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

  /**
   * Analyse the frames.
   *
   * @param infos frame key infos to analyse
   */
  public void analyse(Collection<FrameKeyInfo> infos)
  {
    if (!collecting) {
      for (List<HitInfo> list: hitInfoLists) {
        list.clear();
      }
    }

    List<Asteroid> oldAsteroids = null;
    List<Bullet>   oldBullets   = null;
    int            oldShipCount = 0;
    SpaceShip      oldShip      = null;
    Ufo            oldUfo       = null;
    boolean first = true;  // First frame has no IDs
    for (FrameKeyInfo fkInfo: infos) {
      if (first) {
        first = false;
        continue;
      }
      FrameInfo info = fkInfo.getFrameInfo();
      List<Asteroid> newAsteroids = new ArrayList<Asteroid>(info.getAsteroids());
      List<Bullet>   newBullets   = new ArrayList<Bullet>(info.getBullets());
      Collections.sort(newAsteroids, ID_COMPARATOR);
      Collections.sort(newBullets,   ID_COMPARATOR);
      if (oldAsteroids != null) {
        List<Bullet> vanishedBullets = new LinkedList<Bullet>();
        Iterator<Bullet> newBull = newBullets.iterator();
        Integer newId = newBull.hasNext() ? newBull.next().getIdentity() : null;
        for (Bullet bull: oldBullets) {
          if (bull.getIdentity().equals(newId)) {
            newId = newBull.hasNext() ? newBull.next().getIdentity() : null;
          }
          else {
            vanishedBullets.add(bull);
          }
        }
        if (!vanishedBullets.isEmpty()) {
          List<MovingGameObject> vanishedObjects = new LinkedList<MovingGameObject>();
          Iterator<Asteroid> newAst = newAsteroids.iterator();
          newId = newAst.hasNext()  ?  newAst.next().getIdentity()  :  null;
          for (Asteroid ast: oldAsteroids) {
            if (ast.getIdentity().equals(newId)) {
              newId = newAst.hasNext()  ?  newAst.next().getIdentity()  :  null;
            }
            else {
              vanishedObjects.add(ast);
            }
          }
          if (oldShipCount > info.getNrShips()  &&  oldShip != null  &&  info.getSpaceShip() == null) {
            vanishedObjects.add(oldShip);
          }
          if (oldUfo != null  &&  info.getUfo() == null) {
            vanishedObjects.add(oldUfo);
          }
          List<HitInfo> possibleHits = new ArrayList<HitInfo>(vanishedBullets.size() * vanishedObjects.size());
          for (Bullet bull: vanishedBullets) {
            for (MovingGameObject obj: vanishedObjects) {
              possibleHits.add(new HitInfo(bull, obj, info.getIndex()));
            }
          }
          Collections.sort(possibleHits, HITINFO_COMPARATOR);
          while (!possibleHits.isEmpty()) {
            HitInfo hitInfo = possibleHits.remove(0);
            if (hitInfo.getDestructionDistance() <= 2*hitInfo.getObject().getSize()) {
              dispatcher.dispatch(hitInfo);
            }
            // remove all occurences of bullet and object
            for (ListIterator<HitInfo> it = possibleHits.listIterator();  it.hasNext();  ) {
              HitInfo i = it.next();
              if (i.getBullet() == hitInfo.getBullet()  ||
                  i.getObject() == hitInfo.getObject()) {
                it.remove();
              }
            }
          }
        }
      }
      oldAsteroids = newAsteroids;
      oldBullets   = newBullets;
      oldShipCount = info.getNrShips();
      oldShip      = info.getSpaceShip();
      oldUfo       = info.getUfo();
    }

    for (List<HitInfo> list: hitInfoLists) {
      Collections.sort(list, HITINFO_COMPARATOR);
      Collections.reverse(list);
    }
  }

  /**
   * Get the properties of this object.
   *
   * @return collection of properties
   */
  public Collection<Property> getProperties()
  {
    List<Property> props = new LinkedList<Property>();
    for (List<HitInfo> list: hitInfoLists) {
      for (HitInfo info: list) {
        props.add(new Property<String>(String.format("=== %s size %d ===", info.getObject().getObjectType(), info.getObject().getSize()),
                                       String.format("%s / %s", info.getDestructionDelta(), info.getPreDestructionDelta())));
      }
    }
    return props;
  }

  private static void appendListTable(StringBuilder b, List<HitInfo> list)
  {
    appendOpenTag(b, TAG_TABLE, attr("border", 1));

    appendOpenTag(b, TAG_TABLE_ROW);
    appendTaggedText(b, "Pre Delta",   TAG_TABLE_HEAD);
    appendTaggedText(b, "Pre Dist",    TAG_TABLE_HEAD);
    appendTaggedText(b, "Delta",       TAG_TABLE_HEAD);
    appendTaggedText(b, "Dist",        TAG_TABLE_HEAD);
    appendTaggedText(b, "Bullet   LT", TAG_TABLE_HEAD);
    appendTaggedText(b, "Asteroid LT", TAG_TABLE_HEAD);
    appendTaggedText(b, "Frame",       TAG_TABLE_HEAD);
    appendCloseTag(b, TAG_TABLE_ROW);

    for (HitInfo info: list) {
      appendOpenTag(b, TAG_TABLE_ROW);
      appendTaggedText(b, info.getPreDestructionDelta().toString(),          TAG_TABLE_CELL);
      appendTaggedText(b, Double.toString(info.getPreDestructionDistance()), TAG_TABLE_CELL);
      appendTaggedText(b, info.getDestructionDelta().toString(),             TAG_TABLE_CELL);
      appendTaggedText(b, Double.toString(info.getDestructionDistance()),    TAG_TABLE_CELL);
      appendTaggedText(b, Integer.toString(info.getBullet().getLifetime()),  TAG_TABLE_CELL);
      appendTaggedText(b, Integer.toString(info.getObject().getLifetime()),  TAG_TABLE_CELL);
      appendTaggedText(b, Integer.toString(info.getFrameIndex()),            TAG_TABLE_CELL);
      appendCloseTag(b, TAG_TABLE_ROW);
    }

    appendCloseTag(b, TAG_TABLE);
  }

  /**
   * Append the summary report of this statistics to the document.
   *
   * @param html HTML document to which the report is added
   */
  @Override
  public void appendToHtml(StringBuilder html)
  {
    appendToHtml(html, TAG_HEADER_SECTION, TAG_HEADER_SUBSECTION);
  }

  private void appendToHtml(StringBuilder html, String sectionTag, String subsectionTag)
  {
    appendTaggedText(html, "Hit Positions", sectionTag);
    appendTaggedText(html, "Small Asteroids", subsectionTag);
    appendListTable(html, smallAsteroidsInfo);
    appendTaggedText(html, "Middle Asteroids", subsectionTag);
    appendListTable(html, middleAsteroidsInfo);
    appendTaggedText(html, "Large Asteroids", subsectionTag);
    appendListTable(html, largeAsteroidsInfo);
    appendTaggedText(html, "Small Ufos", subsectionTag);
    appendListTable(html, smallUfosInfo);
    appendTaggedText(html, "Large Ufos", subsectionTag);
    appendListTable(html, largeUfosInfo);
    appendTaggedText(html, "Ships", subsectionTag);
    appendListTable(html, shipsInfo);

  }

  /**
   * Append the inner part of the HTML body.
   *
   * @param b string builder to append
   */
  @Override
  protected void appendHtmlBody(StringBuilder b)
  {
    appendToHtml(b, TAG_HEADER_MAIN, TAG_HEADER_SECTION);
  }

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

  public Collection<HitInfo> getSmallAsteroidsInfo()
  {
    return Collections.unmodifiableCollection(smallAsteroidsInfo);
  }

  public Collection<HitInfo> getMiddleAsteroidsInfo()
  {
    return Collections.unmodifiableCollection(middleAsteroidsInfo);
  }

  public Collection<HitInfo> getLargeAsteroidsInfo()
  {
    return Collections.unmodifiableCollection(largeAsteroidsInfo);
  }

  public Collection<HitInfo> getSmallUfosInfo()
  {
    return Collections.unmodifiableCollection(smallUfosInfo);
  }

  public Collection<HitInfo> getLargeUfosInfo()
  {
    return Collections.unmodifiableCollection(largeUfosInfo);
  }

  public Collection<HitInfo> getShipsInfo()
  {
    return Collections.unmodifiableCollection(shipsInfo);
  }

  public static void main(String[] args) throws IOException
  {
    HitAnalysis ana = new HitAnalysis(true);
    for (String arg: args) {
      DumpFile df = new DumpFile(arg);
      df.runPreparerDirectly(new ImprovedVelocityPreparer());
      ana.analyse(df.getInfos());
    }
    FileWriter writer = new FileWriter("hitanalysis.html");
    writer.write(ana.getHtmlReport());
    writer.close();
  }

}
