// ============================================================================
// 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.Communication;
import de.caff.asteroid.FrameInfo;
import de.caff.asteroid.FrameListener;

import java.io.FileWriter;
import java.io.IOException;
import java.io.Writer;
import java.util.Arrays;
import java.util.Random;

/**
 */
public class SimulatedAnnealingPlayer
        implements FrameListener
{
  private static final int TESTS_PER_COEFF = 10;
  private static final double COOLING = 0.7;
  private static final int BEST_SURVIVERS = 6;
  static final int FRAMES_COUNT = 3 * 60 * 60;
  /** The communication used. */
  private Communication com;
  /** The current asteroid player. */
  private AsteroidPlayer asteroidPlayer;
  /** The suicide player to end the game. */
  private SuicidePlayer suicidePlayer;
  /** The start frame. */
  private int startFrame;

  private static class Coeff
    implements Comparable<Coeff>
  {
    private final double dangerFrames;
    private final double dangerScore;
    private final double nearnessBase;
    private final double rotationBase;
    private int    score;
    private int    count;

    Coeff(double dangerFrames, double dangerScore, double nearnessBase, double rotationBase)
    {
      this.dangerFrames = dangerFrames;
      this.dangerScore = dangerScore;
      this.nearnessBase = nearnessBase;
      this.rotationBase = rotationBase;
    }

    public void addScore(int score)
    {
      this.score += score;
      ++count;
    }

    public double getDangerFrames()
    {
      return dangerFrames;
    }

    public double getDangerScore()
    {
      return dangerScore;
    }

    public double getNearnessBase()
    {
      return nearnessBase;
    }

    public double getRotationBase()
    {
      return rotationBase;
    }

    public double getAverageScore()
    {
      return count == 0 ? 0.0 : score/(double)count;
    }

    public int getCount()
    {
      return count;
    }

    /**
     * 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>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 implementor 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."
     * <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.
     *
     * @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(Coeff o)
    {
      int here = (int)getAverageScore();
      int there = (int)o.getAverageScore();
      return there - here;
    }
  }

  private Coeff[] coeffs = new Coeff[BEST_SURVIVERS*BEST_SURVIVERS];
  private double dangerFramesVar = DefaultScorer.DANGER_FRAMES;
  private double dangerScoreVar  = DefaultScorer.DANGER_SCORE;
  private double nearnessBaseVar = DefaultScorer.NEARNESS_BASE;
  private double rotationBaseVar = DefaultScorer.ROTATION_BASE;
  private Random random = new Random();
  private int currentCoeff = 0;
  private Writer writer;

  public SimulatedAnnealingPlayer(Communication com)
  {
    try {
      writer = new FileWriter("sim.out");
    } catch (IOException e) {
      e.printStackTrace();
    }
    this.com = com;
    suicidePlayer = null;
    Arrays.fill(coeffs, new Coeff(dangerFramesVar, dangerScoreVar, nearnessBaseVar, rotationBaseVar));
    coeffs[1] = new Coeff(0, 0, 0, 0);
    for (int i = 2;  i < 6;  ++i) {
      coeffs[i] = new Coeff((i==2 ? 4 : 2)*random.nextDouble()*dangerFramesVar,
                            (i==3 ? 4 : 2)*random.nextDouble()*dangerScoreVar,
                            (i==4 ? 4 : 2)*random.nextDouble()*nearnessBaseVar,
                            (i==5 ? 4 : 2)*random.nextDouble()*rotationBaseVar);
    }
    initializeCoeffs();
  }

  private void initializeCoeffs()
  {
    storeLine("dangerFramesVar="+dangerFramesVar);
    storeLine("dangerScoreVar ="+dangerScoreVar);
    storeLine("nearnessBaseVar="+nearnessBaseVar);
    storeLine("---------------------------------------------------");

    for (int c = BEST_SURVIVERS;  c < coeffs.length;  ++c) {
      int base = c % BEST_SURVIVERS;
      coeffs[c] = new Coeff(coeffs[base].getDangerFrames() + random.nextGaussian()*dangerFramesVar,
                            coeffs[base].getDangerScore()  + random.nextGaussian()*dangerScoreVar,
                            coeffs[base].getNearnessBase() + random.nextGaussian()*nearnessBaseVar,
                            coeffs[base].getRotationBase() + random.nextGaussian()*rotationBaseVar);
    }

    currentCoeff = 0;

    dangerFramesVar *= COOLING;
    dangerScoreVar  *= COOLING;
    nearnessBaseVar *= COOLING;
  }

  private void storeResults()
  {
    Arrays.sort(coeffs);
    for (int c = 0;  c < coeffs.length;  ++c) {
      storeLine(String.format("%f: %f;%f;%f",
                              coeffs[c].getAverageScore(),
                              coeffs[c].getDangerFrames(),
                              coeffs[c].getDangerScore(),
                              coeffs[c].getNearnessBase()));
    }
    storeLine("===================================================");
  }

  private void storeLine(String line)
  {
    System.out.println(line);
    try {
      writer.append(line);
      writer.append("\n");
      writer.flush();
    } catch (IOException e) {
      e.printStackTrace();
    }
  }

  private AsteroidPlayer getNextPlayer()
  {
    while (currentCoeff < coeffs.length) {
      if (coeffs[currentCoeff].getCount() < TESTS_PER_COEFF) {
        return new AsteroidPlayer(com, new DefaultScorer(coeffs[currentCoeff].getDangerFrames(),
                                                         coeffs[currentCoeff].getDangerScore(),
                                                         coeffs[currentCoeff].getNearnessBase(),
                                                         coeffs[currentCoeff].getRotationBase()));
      }
      ++currentCoeff;
    }
    storeResults();
    initializeCoeffs();
    return getNextPlayer();
  }

  /**
   * Called each time a frame is received.
   * <p/>
   * <b>ATTENTION:</b> this is called from the communication thread!
   * Implementing classes must be aware of this and take care by synchronization or similar!
   *
   * @param frame the received frame
   */
  public void frameReceived(FrameInfo frame)
  {
    if (frame.isGameEndDisplayed()) {
      if (asteroidPlayer != null) {
        // premature end
        com.removeFrameListener(asteroidPlayer);
        asteroidPlayer = null;
        coeffs[currentCoeff].addScore(frame.getScore());
        System.out.println("\tScore: "+frame.getScore());
      }
      else if (suicidePlayer != null) {
        com.removeFrameListener(suicidePlayer);
      }
    }
    else if (frame.isGameRunning()) {
      if (asteroidPlayer != null  &&  frame.getIndex() - startFrame >= FRAMES_COUNT) {
        com.removeFrameListener(asteroidPlayer);
        coeffs[currentCoeff].addScore(frame.getScore());
        System.out.println("\tScore: "+frame.getScore());
        asteroidPlayer = null;
        suicidePlayer = new SuicidePlayer(com);
        com.addFrameListener(suicidePlayer);
      }
    }
    else {
      startFrame = frame.getIndex();
      if (asteroidPlayer == null) {
        asteroidPlayer = getNextPlayer();
        com.addFrameListener(asteroidPlayer);
      }
    }
  }
}
