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

import javax.swing.*;
import java.awt.*;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.geom.AffineTransform;
import java.awt.geom.GeneralPath;
import java.util.Collection;
import java.util.LinkedList;
import java.util.ArrayList;

/**
 *  Frame display with additional selection.
 *
 *  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 EnhancedFrameDisplay
        extends FrameDisplay
        implements SelectionAreaListener,
                   DumpFileChangeListener
{
  /** Size of messages. */
  private static final int MESSAGE_SIZE = 4;
  private static final Color SELECTION_COLOR = Color.magenta;

  private class Animation
          implements Runnable,
                     Drawable,
                     GameData
  {
    private AnniversaryAnimation anim;
    private boolean running;

    private Animation()
    {
      anim = new AnniversaryAnimation("25 JAHRE C'T");
    }

    public synchronized boolean isRunning()
    {
      return running;
    }

    public synchronized void stop()
    {
      running = false;
    }

    /**
     * When an object implementing interface <code>Runnable</code> is used
     * to create a thread, starting the thread causes the object's
     * <code>run</code> method to be called in that separately executing
     * thread.
     * <p/>
     * The general contract of the method <code>run</code> is that it may
     * take any action whatsoever.
     *
     * @see Thread#run()
     */
    public void run()
    {
      running = true;
      while (isRunning()) {
        if (message == null) {
          anim.nextStep();
          repaint();
        }
        try {
          Thread.sleep(1000/FRAMES_PER_SECOND);
        } catch (InterruptedException e) {
        }
      }
    }

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

  private Rectangle[] selections = new Rectangle[0];
  private TimeLine timeLine;
  private Animation animation;
  private boolean   hadDumpFile;
  private Collection<Action> additionalActions = new LinkedList<Action>();
  /** Message to display. */
  private String    message;

  /**
   * Constructor.
   * @param timeLine time line
   */
  public EnhancedFrameDisplay(TimeLine timeLine)
  {
    this(0, timeLine, null);
  }

  /**
   * Constructor.
   * @param timeLine time line
   * @param fid      displayer of infos of clicked objects
   */
  public EnhancedFrameDisplay(TimeLine timeLine, final FrameKeyInfoDisplay fid)
  {
    this(0, timeLine, fid);
  }

  /**
   * Constructor.
   * @param width    the fiy width of this component or <code>null</code> for a variable size
   * @param timeLine time line
   * @param fid      displayer of infos of clicked objects
   */
  public EnhancedFrameDisplay(int width, TimeLine timeLine, final FrameKeyInfoDisplay fid)
  {
    super(width);
    this.timeLine = timeLine;
    if (fid != null) {
      addMouseListener(new MouseAdapter() {
        /**
         * {@inheritDoc}
         */
        @Override
        public void mouseClicked(MouseEvent e)
        {
          if (e.getButton() == MouseEvent.BUTTON1) {
            Collection<GameObject> gameObjects = pickAt(e.getPoint(), 2);
            fid.setSelectedUserObjects(gameObjects.toArray(new GameObject[gameObjects.size()]));
          }
        }

        /**
         * Invoked when a mouse button has been pressed on a component.
         */
        @Override
        public void mousePressed(MouseEvent e)
        {
          if (e.isPopupTrigger()) {
            showPopup(e.getX(), e.getY());
          }
        }
      });
    }
  }

  private void showPopup(int x, int y)
  {
    if (timeLine.hasFrames()) {
      JPopupMenu popupMenu = AbstractBasicDumpFileStatistics.getStatisticsMenu(timeLine.getDumpFile());
      if (!additionalActions.isEmpty()) {
        popupMenu.addSeparator();
        for (Action action: new ArrayList<Action>(additionalActions)) {
          popupMenu.add(action);
        }
      }
      popupMenu.show(this, x, y);
    }
  }

  /**
   * Invoked by Swing to draw components.
   * Applications should not invoke <code>paint</code> directly,
   * but should instead use the <code>repaint</code> method to
   * schedule the component for redrawing.
   * <p/>
   * This method actually delegates the work of painting to three
   * protected methods: <code>paintComponent</code>,
   * <code>paintBorder</code>,
   * and <code>paintChildren</code>.  They're called in the order
   * listed to ensure that children appear on top of component itself.
   * Generally speaking, the component and its children should not
   * paint in the insets area allocated to the border. Subclasses can
   * just override this method, as always.  A subclass that just
   * wants to specialize the UI (look and feel) delegate's
   * <code>paint</code> method should just override
   * <code>paintComponent</code>.
   *
   * @param g the <code>Graphics</code> context in which to paint
   * @see #paintComponent
   * @see #paintBorder
   * @see #paintChildren
   * @see #getComponentGraphics
   * @see #repaint
   */
  @Override
  public void paint(Graphics g)
  {
    super.paint(g);
    if (message != null) {
      // display the message
      Graphics2D g2 = createAsteroidSpaceGraphics(g);
      g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
                          RenderingHints.VALUE_ANTIALIAS_ON);
      g2.setStroke(new BasicStroke(3, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
      g2.setColor(SELECTION_COLOR);
      
      String[] lines = message.split("\n");

      int lineHeight = 15*MESSAGE_SIZE;
      int upperLineOffset = (lines.length - 1) * (lineHeight/2);
      for (int l = 0;  l < lines.length;  ++l) {
        String line = lines[l];
        Rectangle r = Text.getTextBounds(line, MESSAGE_SIZE);
        Graphics2D localG = (Graphics2D)g2.create();
        localG.translate(((EXTENT_X - r.width)/2 - r.x),
                         ((EXTENT_Y - r.height)/2 - r.y + MIN_Y +
                         upperLineOffset - l*lineHeight/2));
        Text.drawText(localG, line, MESSAGE_SIZE);
      }
    }
    else {
      // display markings or animation
      Graphics2D g2 = (Graphics2D)g.create();
      g2.setStroke(new BasicStroke(2));
      g2.setColor(SELECTION_COLOR);
      AffineTransform trafo = getTrafo();
      for (Rectangle rect: selections) {
        GeneralPath p = new GeneralPath();
        p.moveTo((float)rect.getMinX(), (float)rect.getMinY());
        p.lineTo((float)rect.getMaxX(), (float)rect.getMinY());
        p.lineTo((float)rect.getMaxX(), (float)rect.getMaxY());
        p.lineTo((float)rect.getMinX(), (float)rect.getMaxY());
        p.closePath();
        p.transform(trafo);
        g2.draw(p);
      }
      if (timeLine.getDumpFile() == null) {
        if (!hadDumpFile) {
          if (animation == null) {
            animation = new Animation();
            new Thread(animation, "Anniversary Animation").start();
          }
          animation.draw(createAsteroidSpaceGraphics(g));
        }
      }
      else {
        if (animation != null) {
          animation.stop();
          animation = null;
        }
        hadDumpFile = true;
      }
    }
  }

  /**
   *  Called if selection areas have changed.
   *  @param areas new areas
   */
  public void selectionAreasChanged(Rectangle[] areas)
  {
    selections = areas;
    repaint();
  }

  /**
   * Called if the dump file has (possibly) changed.
   */
  public void dumpFileChange()
  {
    repaint();
  }

  /**
   * Get the duration since the last game start.
   *
   * @param frame frame for which the duration is requested
   * @return time since the current game started in seconds
   */
  @Override
  protected int getSessionTime(FrameInfo frame)
  {
    return timeLine.getSessionTime(frame.getIndex());
  }

  /**
   *  Get the displayed message.
   *  @return displayed message or <code>null</code> if no message is displayed
   */
  public String getMessage()
  {
    return message;
  }

  /**
   *  Set the displayed message.
   *
   *  If there is a message to display, no model nor animation is displayed.
   *  @param message message to display
   */
  public void setMessage(String message)
  {
    if (message != null) {
      this.message = FrameInfo.canonize(message).trim();
    }
    else {
      this.message = null;
    }
    repaint();
  }

  /**
   *  Clear the message.
   *  Displays either the model or the animation.
   */
  public void clearMessage()
  {
    setMessage(null);
  }

  /**
   *  Add an action to the popup menu.
   *  @param action action to add
   */
  public void addPopupAction(Action action)
  {
    additionalActions.add(action);
  }

  /**
   *  Remove an action from the popup menu.
   *  @param action action to remove
   *  @return was the action found?
   */
  public boolean removeAction(Action action)
  {
    return additionalActions.remove(action);
  }
}
