// ============================================================================
// 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.Pair;

import java.util.*;

/**
 *  Helper class which tries to add velocities to game objects and lifetime to bullets.
 *  This is done in a too simple-minded way by just comparing the last two screens and connecting nearest objects
 *  (using the velocities calculated for the previous frame).
 *
 *  This class is part of a solution for a
 *  <a href="http://www.heise.de/ct/creativ/08/02/details/">competition by the German computer magazine c't</a>.
 */
public class SimpleVelocityPreparer
        implements FramePreparer
{
  /** Maximum squared distance we assume an asteroid may move between frames. */
  private static final int MAX_SQUARED_ASTEROID_VELOCITY = 6*6;
  /** Maximum squared distance we assume a bullet may move between frames. */
  protected static final int MAX_SQUARED_BULLET_VELOCITY = 16*16;

  /**
   *  Helper class to presort asteroids to allow easier compare between frames.
   */
  protected static class AsteroidSelector
  {

    /**
     *  Helper class: key for mapping asteroids.
     */
    private static class Key
    {
      /** The asteroid's type. */
      private final int type;
      /** The asteroids size. */
      private final int size;

      /**
       * Constrictor.
       * @param ast  asteroid
       */
      private Key(Asteroid ast)
      {
        this.type = ast.getType();
        this.size = ast.getSize();
      }

      public boolean equals(Object o)
      {
        if (this == o) {
          return true;
        }
        if (o == null || getClass() != o.getClass()) {
          return false;
        }

        Key key = (Key)o;

        if (size != key.size) {
          return false;
        }
        if (type != key.type) {
          return false;
        }

        return true;
      }

      public int hashCode()
      {
        int result;
        result = type;
        result = 31 * result + size;
        return result;
      }
    }
    /** Presorted mapping of asteroids. */
    private Map<Key, List<Asteroid>> sorted = new HashMap<Key, List<Asteroid>>();

    /**
     *  Constructor.
     *  @param asteroids asteroids to be presorted
     */
    public AsteroidSelector(Collection<Asteroid> asteroids)
    {
      for (Asteroid ast: asteroids) {
        Key key = new Key(ast);
        List<Asteroid> list = sorted.get(key);
        if (list == null) {
          list = new LinkedList<Asteroid>();
          sorted.put(key, list);
        }
        list.add(ast);
      }
    }

    /**
     *  Get the best matching asteroid assuming the keys collected here are from the previous
     *  frame while the given asteroid is from the current frame.
     *  @param asteroid current frame astreroid
     *  @return best match or <code>null</code>
     */
    public Asteroid getBestMatch(Asteroid asteroid)
    {
      Key key = new Key(asteroid);
      List<Asteroid> list = sorted.get(key);
      double minDist2 = MAX_SQUARED_ASTEROID_VELOCITY;
      Asteroid nearest = null;
      if (list != null  &&  !list.isEmpty()) {
        if (asteroid.getVelocityX() == 0  &&  asteroid.getVelocityY() == 0) {
          if (list.size() == 1  &&  list.get(0).getSquaredDistance(asteroid) <= MAX_SQUARED_ASTEROID_VELOCITY) {
            nearest = list.get(0);
          }
          else {
            List<Asteroid> possibleCandidates = new LinkedList<Asteroid>();
            for (Asteroid a: list) {
              if (a.getIdentity() == null  &&  a.getSquaredDistance(asteroid) <= MAX_SQUARED_ASTEROID_VELOCITY) {
                possibleCandidates.add(a);
              }
            }
            if (!possibleCandidates.isEmpty()) {
              if (possibleCandidates.size() > 1) {
                Collections.sort(possibleCandidates, new Comparator<Asteroid>() {
                  public int compare(Asteroid o1, Asteroid o2)
                  {
                    return o1.getIndex() - o2.getIndex();
                  }
                });
              }
              nearest = possibleCandidates.get(0);
            }
          }
        }
        else {
          double futureX = asteroid.getX() + asteroid.getVelocityX();
          double futureY = asteroid.getY() + asteroid.getVelocityY();
          for (Asteroid a: list) {
            double dist2 = a.getSquaredDistance(futureX, futureY);
            if (dist2 < minDist2) {
              nearest = a;
              minDist2 = dist2;
            }
          }
        }
      }
      return nearest;
    }
  }

  /**
   *  Prepare the ufo.
   *  Called if there are at least two frames.
   *  Overwrite to change the default behavior
   *  @param frameInfos all frame infos
   *  @param prevFrame  the second to last frame
   *  @param currFrame  the last frame
   */
  protected void prepareUfo(LinkedList<FrameInfo> frameInfos,
                            FrameInfo prevFrame,
                            FrameInfo currFrame)
  {
    Ufo ufo = currFrame.getUfo();
    if (ufo != null) {
      ufo.setVelocityFromDelta(prevFrame.getUfo());
    }
  }

  /**
   *  Prepare the space ship.
   *  Called if there are at least two frames.
   *  Overwrite to change the default behavior
   *  @param frameInfos all frame infos
   *  @param prevFrame  the second to last frame
   *  @param currFrame  the last frame
   */
  protected void prepareSpaceShip(LinkedList<FrameInfo> frameInfos,
                                  FrameInfo prevFrame,
                                  FrameInfo currFrame)
  {
    SpaceShip ship = currFrame.getSpaceShip();
    if (ship != null) {
      ship.setVelocityFromDelta(prevFrame.getSpaceShip());
    }
  }

  /**
   *  Prepare the asteroids.
   *  Called if there are at least two frames.
   *  Overwrite to change the default behavior
   *  @param frameInfos all frame infos
   *  @param prevFrame  the second to last frame
   *  @param currFrame  the last frame
   */
  protected void prepareAsteroids(LinkedList<FrameInfo> frameInfos,
                                  FrameInfo prevFrame,
                                  FrameInfo currFrame)
  {
    AsteroidSelector selector = new AsteroidSelector(currFrame.getAsteroids());
    for (Asteroid ast: prevFrame.getAsteroids()) {
      Asteroid candidate = selector.getBestMatch(ast);
      if (candidate != null) {
        candidate.setVelocityFromDelta(ast);
        candidate.inheret(ast);
      }
    }
  }

  /**
   *  Prepare the bullets.
   *  Called if there are at least two frames.
   *  Overwrite to change the default behavior
   *  @param frameInfos all frame infos
   *  @param prevFrame  the second to last frame
   *  @param currFrame  the last frame
   */
  protected void prepareBullets(LinkedList<FrameInfo> frameInfos,
                                FrameInfo prevFrame,
                                FrameInfo currFrame)
  {
    for (Bullet oldBullet: prevFrame.getBullets()) {
      double futureX = oldBullet.getX() + oldBullet.getVelocityX();
      double futureY = oldBullet.getY() + oldBullet.getVelocityY();

      SortedMap<Double, Pair<Bullet>> result = new TreeMap<Double, Pair<Bullet>>();
      for (Bullet bullet: currFrame.getBullets()) {
        double dist2 = bullet.getSquaredDistance(futureX, futureY);
        if (dist2 < MAX_SQUARED_BULLET_VELOCITY) {
          result.put(dist2, new Pair<Bullet>(oldBullet, bullet));
        }
      }
      LinkedList<Pair<Bullet>> pairs = new LinkedList<Pair<Bullet>>(result.values());
      while (!pairs.isEmpty()) {
        Pair<Bullet> pair = pairs.remove(0);
        pair.second.setVelocityFromDelta(pair.first);
        pair.second.inheret(pair.first);
        for (ListIterator<Pair<Bullet>> it = pairs.listIterator();  it.hasNext();  ) {
          Pair<Bullet> p = it.next();
          if (p.first.equals(pair.first)  ||  p.second.equals(pair.second)) {
            it.remove();
          }
        }
      }
    }
  }

  /**
   *  Prepare frame info if there is only a single frame available.
   *  @param frame frame info
   */
  protected void prepareSingleFrame(FrameInfo frame)
  {
    // set bullet lifetimes to 1 to indicate lifetimes are calculated
    for (Bullet bullet: frame.getBullets()) {
      bullet.setLifetime(1);
    }
  }

  /**
   * Prepare the frame(s).
   *
   * @param frameInfos the collected frame infos
   */
  public void prepareFrames(LinkedList<FrameInfo> frameInfos)
  {
    if (frameInfos.size() >= 2) {
      FrameInfo currFrame = frameInfos.getLast();
      FrameInfo prevFrame = frameInfos.get(frameInfos.size() - 2);

      prepareUfo(frameInfos, prevFrame, currFrame);

      prepareSpaceShip(frameInfos, prevFrame, currFrame);

      prepareAsteroids(frameInfos, prevFrame, currFrame);

      prepareBullets(frameInfos, prevFrame, currFrame);

    }
    else if (!frameInfos.isEmpty()) {
      prepareSingleFrame(frameInfos.getLast());
    }
  }
}
