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

import de.caff.asteroid.*;
import de.caff.util.Tools;

import java.awt.*;
import java.util.*;
import java.util.List;

/**
 *  Animation for c't anniversary.
 *
 *  All coordinates are in the Asteroid space.
 */
public class AnniversaryAnimation
        implements GameData,
                   Drawable
{
  private static interface AnimatedObject
    extends Drawable
  {
    /**
     *  Go to next animation step.
     *  @param currentObjects the currently known objects
     * @param collector      collector for objects which are still alive in the next step
     */
    void nextStep(Collection<AnimatedObject> currentObjects,
                  List<AnimatedObject> collector);

    /**
     *  Is this object hit by a bullet.
     *  @param bullet te bullet
     *  @return the answer
     */
    boolean isHit(Bullet bullet);

    /**
     *  Is this object a possible target.
     *  @return the answer
     */
    boolean isTarget();
  }

  /** First an asteroid, later a letter, with an explosion. */
  private static class AsteroidLetter
    implements AnimatedObject
  {
    /** The scaling of letters as a power of 2. */
    private static final int LETTER_SCALE = 2;

    private static final int EXPLOSION_REPEATS = 6;
    private static final Color[] EXPLOSION_COLORS = new Color[] {
            Color.white,
            Color.red,
            Color.yellow,
            Color.magenta,
            Color.orange,
            Color.blue,
            Color.cyan,
            Color.green,
            Color.pink,
    };
    /** The rotation. */
    private final double deltaRot;
    /** The current rotation. */
    private double rotation;
    /** The asteroid. */
    private Asteroid asteroid;
    /** The position. */
    private Point position;
    /** The list of explosions to be drawn after a hit. */
    private java.util.List<Explosion> explosions;
    /** Text (with just on letter). */
    private Text text;
    /** CText color in final state. */
    private Color textColor;

    /**
     *  Create an asteroid letter.
     *  @param ch letter
     *  @param x  pos x
     *  @param y  pos y
     */
    AsteroidLetter(char ch, int x, int y)
    {
      position = new Point(x, y);
      asteroid = new Asteroid(0, 0, 0, 0, random.nextInt(4));
      rotation = random.nextDouble() * 2 * Math.PI;
      deltaRot = random.nextDouble() * Math.PI/60 - Math.PI/120;
      String txt = Character.toString(ch);
      Rectangle txtBounds = Text.getTextBounds(txt, Math.pow(2.0, LETTER_SCALE));
      int centerX = (int)txtBounds.getCenterX();
      int centerY = (int)txtBounds.getCenterY();
      text = new Text(txt, -centerX, -centerY, LETTER_SCALE);
    }

    /**
     *  Is this hit by a bullet?
     *  @param bullet the bullet
     *  @return <code>true</code> if the asteroid is hit
     */
    public boolean isHit(Bullet bullet)
    {
      if (asteroid != null) {
        if (Tools.getLength(bullet.getDelta(position)) < asteroid.getSize()) {
          asteroid = null;
          explosions = new LinkedList<Explosion>();
          for (Explosion explosion: new Explosion[] {
             new Explosion(0, 0, 11, Explosion.Type.S),
             new Explosion(0, 0, 12, Explosion.Type.S),
             new Explosion(0, 0, 13, Explosion.Type.S),
             new Explosion(0, 0, 14, Explosion.Type.S),
             new Explosion(0, 0, 15, Explosion.Type.S),
             new Explosion(0, 0,  0, Explosion.Type.S),
             new Explosion(0, 0, 11, Explosion.Type.L),
             new Explosion(0, 0, 12, Explosion.Type.L),
             new Explosion(0, 0, 13, Explosion.Type.L),
             new Explosion(0, 0, 14, Explosion.Type.L),
             new Explosion(0, 0, 15, Explosion.Type.L),
             new Explosion(0, 0,  0, Explosion.Type.L),
             new Explosion(0, 0, 11, Explosion.Type.XL),
             new Explosion(0, 0, 12, Explosion.Type.XL),
             new Explosion(0, 0, 13, Explosion.Type.XL),
             new Explosion(0, 0, 14, Explosion.Type.XL),
             new Explosion(0, 0, 15, Explosion.Type.XL),
             new Explosion(0, 0,  0, Explosion.Type.XL),
             new Explosion(0, 0, 11, Explosion.Type.XXL),
             new Explosion(0, 0, 12, Explosion.Type.XXL),
             new Explosion(0, 0, 13, Explosion.Type.XXL),
             new Explosion(0, 0, 14, Explosion.Type.XXL),
             new Explosion(0, 0, 15, Explosion.Type.XXL),
             new Explosion(0, 0,  0, Explosion.Type.XXL),
          }) {
            for (int r = EXPLOSION_REPEATS;  r > 0;  --r) {
              explosions.add(explosion);
            }
          }
          return true;
        }
      }
      return false;
    }

    /**
     * Go to next animation step.
     *
     * @param currentObjects the currently known objects
     * @param collector      collector for objects which are still alive in the next step
     */
    public void nextStep(Collection<AnimatedObject> currentObjects,
                         List<AnimatedObject> collector)
    {
      rotation += deltaRot;
      if (explosions != null) {
        explosions.remove(0);
        if (explosions.isEmpty()) {
          explosions = null;
          textColor = EXPLOSION_COLORS[random.nextInt(EXPLOSION_COLORS.length)];
        }
      }
      collector.add(this);
    }

    /**
     * Draw the object.
     *
     * @param g graphics context
     */
    public void draw(Graphics2D g)
    {
      Graphics2D g2 = (Graphics2D)g.create();
      g2.translate(position.x, position.y);
      if (asteroid != null) {
        g2.rotate(rotation);
        asteroid.draw(g2);
      }
      else {
        if (explosions != null) {
          text.draw(g2);
          g2.scale(2, 2);
          g2.setStroke(new BasicStroke(2));
          g2.setColor(EXPLOSION_COLORS[random.nextInt(EXPLOSION_COLORS.length)]);
          g2.draw(explosions.get(0).getExplosionShape());
        }
        else {
          g2.translate(text.getX(), text.getY());
          g2.setColor(textColor);
          Text.drawText(g2, text.getText(), Math.pow(2, LETTER_SCALE));
        }
      }
    }

    /**
     * Is this object a possible target.
     *
     * @return the answer
     */
    public boolean isTarget()
    {
      return asteroid != null;
    }
  }

  /** The animated space ship. */
  private static class AnimatedShip
          implements AnimatedObject
  {
    private static final int DIR_LENGTH = 10000;
    private double direction;
    private SpaceShip ship;
    private boolean firedInLastStep;
    private boolean finished;

    AnimatedShip(int x, int y, double direction)
    {
      this.direction = direction;
      Point v = getDirectionVector(DIR_LENGTH);
      ship = new SpaceShip(x, y, v.x, v.y);
    }

    private Point getDirectionVector(int length)
    {
      return new Point((int)(length * Math.cos(direction)),
                       (int)(length * Math.sin(direction)));
    }

    /**
     *  Are there any targets left?
     *  @param objects objects to search for targets
     *  @return the answer
     */
    private static boolean haveTargets(Collection<AnimatedObject> objects)
    {
      for (AnimatedObject obj: objects) {
        if (obj.isTarget()) {
          return true;
        }
      }
      return false;
    }

    /**
     * Go to next animation step.
     *
     * @param currentObjects the currently known objects
     * @param collector      collector for objects which are still alive in the next step
     */
    public void nextStep(Collection<AnimatedObject> currentObjects,
                         List<AnimatedObject> collector)
    {
      if (!finished) {
        if (!firedInLastStep) {
          if (random.nextDouble() < 0.1) {
            // fire a bullet
            Point d = getDirectionVector(2*ship.getSize());
            Point v = getDirectionVector(8);
            AnimatedBullet bullet = new AnimatedBullet(ship.getX() + d.x,
                                                       ship.getY() + d.y,
                                                       v.x, v.y);
            collector.add(0, bullet);  // insert at the head so hiting works more gracefully
            firedInLastStep = true;
          }
        }
        else {
          firedInLastStep = false;
        }
        if (!haveTargets(currentObjects)) {
          finished = true;
          collector.add(new AnimatedCongratulation());
        }
      }
      if (random.nextDouble() < 0.5) {
        direction += random.nextDouble() * Math.PI/20 - Math.PI/30;   // slightly asymm.
        Point v = getDirectionVector(DIR_LENGTH);
        ship = new SpaceShip(ship.getX(), ship.getY(), v.x, v.y);
      }
      collector.add(this);
    }

    /**
     * Is this object hit by a bullet.
     *
     * @param bullet te bullet
     * @return the answer
     */
    public boolean isHit(Bullet bullet)
    {
      // shouldn't happen
      return false;
    }

    /**
     * Draw the object.
     *
     * @param g graphics context
     */
    public void draw(Graphics2D g)
    {
      ship.draw((Graphics2D)g.create());
    }

    /**
     * Is this object a possible target.
     *
     * @return the answer
     */
    public boolean isTarget()
    {
      return false;
    }
  }

  /**
   *  Two animated ufos dragging a congrats text.
   */
  private static class AnimatedCongratulation
          implements AnimatedObject
  {
    private static final String CONGRATS_TEXT = "HERZLICHEN GLUECKWUNSCH";
    private static final int    CONGRATS_SCALE = 1;
    private static final String ABOUT_TEXT    = String.format("ANALYZER APPLET VERSI0N %s",
                                                              AnalysisApplet.VERSION.replaceAll("\\.", " D0T "));
    private static final int    FOOTNOTE_BORDER = 10;
    private static final int    ABOUT_SCALE   = 0;
    private static final Point  ABOUT_POS     = new Point(FOOTNOTE_BORDER, MIN_Y+FOOTNOTE_BORDER);
    private static final String ORIGIN_TEXT   = "2008 RAMMI AT CAFF D0T DE";
    private static final int    ORIGIN_SCALE  = 0;
    private static final Point  ORIGIN_POS    = new Point(EXTENT_X - FOOTNOTE_BORDER - Text.getTextBounds(ORIGIN_TEXT,
                                                                                                          Math.pow(2, ORIGIN_SCALE)).width,
                                                          MIN_Y+FOOTNOTE_BORDER);
    private static final int    LANE_Y = MIN_Y + EXTENT_Y/4;
    private static final int    GAP = 8;
    private static final int    SPEED = 2;

    private final int endX;
    private int x;
    private final int textWidth;
    private final int textOffsetY;

    AnimatedCongratulation()
    {
      Rectangle bounds = Text.getTextBounds(CONGRATS_TEXT, Math.pow(2, CONGRATS_SCALE));
      textWidth = bounds.width;
      textOffsetY = -(int)bounds.getCenterY();
      endX = (EXTENT_X - textWidth - Ufo.BIG_SIZE - Ufo.SMALL_SIZE)/2 - GAP;
      x = EXTENT_X + AnimatedBullet.BULLET_LIFE * SPEED + 20;
    }

    /**
     * Go to next animation step.
     *
     * @param currentObjects the currently known objects
     * @param collector      collector for objects which are still alive in the next step
     */
    public void nextStep(Collection<AnimatedObject> currentObjects,
                         List<AnimatedObject> collector)
    {
      if (x > endX) {
        x -= SPEED;
      }
      collector.add(this);
    }

    /**
     * Is this object hit by a bullet.
     *
     * @param bullet te bullet
     * @return the answer
     */
    public boolean isHit(Bullet bullet)
    {
      return false;
    }

    /**
     * Is this object a possible target.
     *
     * @return the answer
     */
    public boolean isTarget()
    {
      // not here, here we are all friends :-)
      return false;
    }

    /**
     * Draw the object.
     *
     * @param g graphics context
     */
    public void draw(Graphics2D g)
    {
      Graphics2D g2 = (Graphics2D)g.create();

      Ufo bigUfo = new Ufo(x, LANE_Y, 15);
      Ufo smallUfo = new Ufo(x+textWidth+Ufo.BIG_SIZE+Ufo.SMALL_SIZE+2*GAP, LANE_Y, 14);
      bigUfo.draw(g2);
      smallUfo.draw(g2);
      new Text(CONGRATS_TEXT, x + Ufo.BIG_SIZE + GAP, LANE_Y + textOffsetY, CONGRATS_SCALE).draw(g2);
      if (x <= endX) {
        new Text(ABOUT_TEXT,  ABOUT_POS.x,  ABOUT_POS.y,  ABOUT_SCALE).draw(g2);
        new Text(ORIGIN_TEXT, ORIGIN_POS.x, ORIGIN_POS.y, ORIGIN_SCALE).draw(g2);
      }
    }
  }

  private static class AnimatedBullet
          implements AnimatedObject
  {
    static final int BULLET_LIFE = 90;
    private Bullet bullet;
    private Point  velocity;

    AnimatedBullet(int x, int y, int vx, int vy)
    {
      bullet = new Bullet(0, x, y);
      bullet.setLifetime(BULLET_LIFE);
      velocity = new Point(vx, vy);
    }

    /**
     * Go to next animation step.
     *
     * @param currentObjects the currently known objects
     * @param collector      collector for objects which are still alive in the next step
     */
    public void nextStep(Collection<AnimatedObject> currentObjects,
                         java.util.List<AnimatedObject> collector)
    {
      Bullet old = bullet;
      Point next = new Point(old.getX() + velocity.x,
                             old.getY() + velocity.y);
      if (next.x < 0) {
        next.x += EXTENT_X;
      }
      else if (next.x >= EXTENT_X) {
        next.x -= EXTENT_X;
      }
      if (next.y < MIN_Y) {
        next.y += MAX_Y - MIN_Y;
      }
      else if (next.y >= MAX_Y) {
        next.y -= MAX_Y - MIN_Y;
      }
      bullet = new Bullet(old.getIndex(),
                          next.x,
                          next.y);
      bullet.setLifetime(old.getLifetime() - 1);
      for (AnimatedObject obj: currentObjects) {
        if (obj.isHit(bullet)) {
          bullet = null;
          break;
        }
      }
      if (bullet != null  &&   bullet.getLifetime() > 0) {
        collector.add(this);
      }
    }

    /**
     * Is this object hit by a bullet.
     *
     * @param bullet te bullet
     * @return the answer
     */
    public boolean isHit(Bullet bullet)
    {
      return false;  // bullets cannot hit each other
    }

    /**
     * Draw the object.
     *
     * @param g graphics context
     */
    public void draw(Graphics2D g)
    {
      if (bullet != null) {
        bullet.draw(g);
      }
    }

    /**
     * Is this object a possible target.
     *
     * @return the answer
     */
    public boolean isTarget()
    {
      return false;
    }
  }

  /** Comonly used random number creator. */
  private static final Random random = new Random();

  private java.util.List<AnimatedObject> animatedObjects = new LinkedList<AnimatedObject>();

  /**
   *  Create animation.
   *  @param text text to display
   */
  AnniversaryAnimation(String text)
  {
    // put letters on arc, CW
    int centerX = EXTENT_X/2;
    int centerY = 0;
    int startX = 0;
    int startY = EXTENT/2;
    final int offset  = 2;
    final int noise   = 4;
    double len        = Tools.getLength(centerX - startX, centerY - startY);
    double startAngle = Math.atan2(startY - centerY, startX - centerX);
    double endAngle   = Math.PI - startAngle;
    double deltaAngle = (endAngle - startAngle) / (text.length() + 2*offset - 1);
    for (int c = 0;  c < text.length();  ++c) {
      animatedObjects.add(new AsteroidLetter(text.charAt(c),
                                             centerX+(int)(len*Math.cos(startAngle + (c+offset)*deltaAngle)),
                                             centerY+(int)(len*Math.sin(startAngle + (c+offset)*deltaAngle) + noise*random.nextGaussian())));
    }
    animatedObjects.add(new AnimatedShip(EXTENT/2, 3*EXTENT/8, random.nextDouble() * 2 * Math.PI));
  }

  /**
   * Draw the object.
   *
   * @param g graphics context
   */
  public void draw(Graphics2D g)
  {
    for (AnimatedObject obj: animatedObjects) {
      obj.draw(g);
    }
  }

  /** Go forward to next animation step. */
  public void nextStep()
  {
    List<AnimatedObject> futureList = new LinkedList<AnimatedObject>();
    for (AnimatedObject obj: animatedObjects) {
      obj.nextStep(animatedObjects, futureList);
    }
    animatedObjects = futureList;
  }
}
