// ============================================================================
// 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 javax.swing.*;
import java.awt.*;
import java.awt.geom.AffineTransform;
import java.awt.geom.NoninvertibleTransformException;
import java.awt.geom.Rectangle2D;
import java.util.Collection;
import java.util.Collections;

/**
 *  Component to display a frame as received from MAME.
 *
 *  Because this an example of a frame listener note the usage of synchronized access to
 *  the frameInfo object passed between threads and the usage of repaint() to inform the AWT
 *  thread to start drawing.
 *
 *  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 FrameDisplay
  extends JComponent
  implements FrameListener,
             GameData
{
  /** Color used for game duration. */
  private static final Color TIME_COLOR = new Color(0x00, 0xFF, 0x00, 0x80);
  /** The position where to display the pending ships (lives). */
  private static final Point PENDING_SHIPS_POSITION = new Point(160, 852);
  /** The delta for pending ships display. */
  private static final int PENDING_SHIPS_DELTA_X = 20;
  /** Border around session time (upper right corner). */
  private static final int TIME_BORDER = 2;

  /** The basic width (if 0 then adapt). */
  private int baseWidth;
  /** The currently displayed frame info. */
  private FrameInfo frameInfo;
  /** The transformation from frame space into component space. */
  private AffineTransform trafo;
  /** Provider for additional drawables in MAME space. */
  private DrawableProvider drawableProvider;
  /** Start frame. */
  private int startFrame = -1;
  /** Show session time? */
  private boolean showingSessionTime = true;
  /** Using antialiasing. */
  private boolean usingAntialising;

  /**
   *  Constructor.
   *
   *  The component is always constructed with a aspect ratio of 4:3.
   *  @param width width to use for this component
   */
  public FrameDisplay(int width)
  {
    baseWidth = width;
    if (width > 0) {
      Dimension size = new Dimension(width, 3*width/4);
      setMaximumSize(size);
      setMinimumSize(size);
      setPreferredSize(size);
      setSize(size);
      calculateScaling();
    }
    else {
      setMinimumSize(new Dimension(200, 150));
      setPreferredSize(new Dimension(640, 480));
    }
    setOpaque(true);
    setDoubleBuffered(true);
  }

  /**
   *  Calculate the scaling from the current size.
   */
  private void calculateScaling()
  {
    int width = baseWidth <= 0  ?
            Math.min(getWidth(), 4*getHeight()/3) :
            baseWidth;
    int height = (3*width)/4;
    // Asteroid actually maps a little more on screen then the vector ram size,
    // 21 pixel in each direction
    double scalingX = width/(double)(SCREEN_WIDTH);
    double scalingY = -height/(double)(SCREEN_HEIGHT);
    // NOTE: take care of y pointing upwards in Asteroids, but downwards on screen
    trafo = AffineTransform.getTranslateInstance((SCREEN_WIDTH - EXTENT_X)/2,
                                                 MIN_Y-EXTENT-(SCREEN_HEIGHT - EXTENT_Y)/2);
    trafo.preConcatenate(AffineTransform.getScaleInstance(scalingX, scalingY));
  }

  /**
   *  Get the correct size keeping the aspect ratio.
   *  @return correct size
   */
  private Dimension getCorrectSize()
  {
    int width = baseWidth <= 0  ?
            Math.min(getWidth(), 4*getHeight()/3) :
            baseWidth;
    return new Dimension(width, 3*width/4);
  }

  /**
   * 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)
  {
    g.setColor(getBackground());
    g.fillRect(0, 0, getWidth(), getHeight());
    g.setColor(Color.black);
    Dimension size = getCorrectSize();
    g.fillRect(0, 0, size.width, size.height);
    g.setClip(0, 0, size.width, size.height);
    FrameInfo info;
    synchronized (this) {
      info = frameInfo;
    }
    if (info != null) {
      Graphics2D g2 = createAsteroidSpaceGraphics(g);
      info.draw(g2);
      if (drawableProvider != null) {
        drawableProvider.draw(g2, info);
      }
      // SHIPs
      {
        Graphics2D localG = (Graphics2D)g2.create();
        localG.setColor(Color.gray);
        localG.translate(PENDING_SHIPS_POSITION.x, PENDING_SHIPS_POSITION.y);
        for (int s = info.getNrShips() - 1;  s >= 0;  --s) {
          SpaceShip.SHIP_GLYPH.drawAndTranslate(localG, SpaceShip.SHIP_SCALING);
        }
      }
      g2.setColor(Color.gray);
      for (Text txt: info.getTexts()) {
        txt.draw(g2);
      }
      // Fix score if necessary
      if (info.getScore() >= GAME_SCORE_WRAP) {
        String score = Integer.toString(info.getScore());
        int prefix = score.length() - 5;
        Glyph zero = Text.getGlyph('0');
        Point pos = new Point(info.isGameRunning() ?
                SCORE_LOCATION_GAME  :
                SCORE_LOCATION_OTHER);
        final int scale = 2;
        Point offset = zero.getOffset();
        pos.x -= prefix * scale * offset.x;
        //pos.y -= prefix * scale * offset.y;
        Graphics2D localG = (Graphics2D)g2.create();
        localG.translate(pos.x, pos.y);
        int c;
        for (c = 0;  c < prefix;  ++c) {
          Text.getGlyph(score.charAt(c), Text.SPACE_GLYPH).drawAndTranslate(localG, scale);
        }
        while (c < score.length()- 2 &&  score.charAt(c) == '0') {
          zero.drawAndTranslate(localG, scale);
          ++c;
        }
      }

      if (showingSessionTime) {
        // Session time
        int duration = getSessionTime(info);
        g.setFont(Text.FONT);
        g.setColor(TIME_COLOR);
        String time = String.format("%02d:%02d:%02d",
                                    (duration/60/60) % 100,
                                    (duration/60) % 60,
                                    duration % 60);
        Rectangle2D bounds = g.getFontMetrics().getStringBounds(time, g);
        g.drawString(time,
                     (int)(size.width - bounds.getWidth() - TIME_BORDER),
                     (int)(TIME_BORDER - bounds.getMinY()));
      }
    }
  }

  /**
   *  Create a graphics in asteroid space.
   *  @param g component graphics
   *  @return graphics in asteroid space (independent of <code>g</code>)
   */
  protected Graphics2D createAsteroidSpaceGraphics(Graphics g)
  {
    if (baseWidth == 0) {
      calculateScaling();
    }
    Graphics2D g2 = (Graphics2D)g.create();
    g2.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON);
    g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
                          usingAntialising ?
                                RenderingHints.VALUE_ANTIALIAS_ON  :
                                RenderingHints.VALUE_ANTIALIAS_OFF);
    g2.setRenderingHint(RenderingHints.KEY_RENDERING,
                          usingAntialising ?
                                RenderingHints.VALUE_RENDER_QUALITY  :
                                RenderingHints.VALUE_RENDER_SPEED);
    g2.transform(trafo);
    return g2;
  }

  /**
   *  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
   */
  protected int getSessionTime(FrameInfo frame)
  {
    if (frame.isGameRunning()) {
      if (startFrame == -1L) {
        startFrame = frame.getIndex();
      }
      return (frame.getIndex() - startFrame) / FRAMES_PER_SECOND;
    }
    else {
      startFrame = frame.getIndex();
    }
    return 0;
  }

  /**
   *  Called each time a frame is received.
   *
   *  <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)
  {
    synchronized (this) {
      frameInfo = frame;
    }
    repaint();
  }

  /**
   * Return objects at a given position.
   * @param p          pick position
   * @param pickRadius pick radius
   * @return collection of game objects found at the given position
   */
  public Collection<GameObject> pickAt(Point p, int pickRadius)
  {
    synchronized (this) {
      if (frameInfo == null) {
        return Collections.emptyList();
      }
    }
    Point min = new Point(p.x - pickRadius, p.y + pickRadius); // using knowledge of negative y scaling
    Point max = new Point(p.x + pickRadius, p.y - pickRadius);
    try {
      AffineTransform inverse = new AffineTransform(trafo).createInverse();
      inverse.transform(min, min);
      inverse.transform(max, max);
      Rectangle hitRect = new Rectangle(min.x, min.y, max.x - min.x, max.y - min.y);
      synchronized (this) {
        return frameInfo.getOverlappingObjects(hitRect);
      }
    } catch (NoninvertibleTransformException e) {
      e.printStackTrace();
    }
    return Collections.emptyList();
  }

  /**
   *  Get the displayed frame info.
   *  @return frame info
   */
  public FrameInfo getFrameInfo()
  {
    return frameInfo;
  }

  /**
   *  Get the transformation from MAME space to screen.
   *  @return transformation
   */
  protected AffineTransform getTrafo()
  {
    return trafo;
  }

  /**
   *  Is the session time displayed?
   *  @return the answer
   */
  public boolean isShowingSessionTime()
  {
    return showingSessionTime;
  }

  /**
   *  Set wether the session time is displayed.
   *  @param showingSessionTime display session time if true
   */
  public void setShowingSessionTime(boolean showingSessionTime)
  {
    if (this.showingSessionTime != showingSessionTime) {
      this.showingSessionTime = showingSessionTime;
      repaint();
    }
  }

  /**
   *  Is antialising used?
   *  @return the answer
   */
  public boolean isUsingAntialising()
  {
    return usingAntialising;
  }

  /**
   *  Set whether antialising is used for the display.
   *  Using antialiasing is nicer but slower.
   *  @param usingAntialising <code>true</code>: use antialising<br>
   *                          <code>false</code>: don't use antialiasing
   */
  public void setUsingAntialising(boolean usingAntialising)
  {
    if (this.usingAntialising != usingAntialising) {
      this.usingAntialising = usingAntialising;
      repaint();
    }
  }

  /**
   *  Get the drawable provider used to display additional information.
   *  @return drawable provider or <code>null</code> if no drawable provider is set
   */
  public DrawableProvider getDrawableProvider()
  {
    return drawableProvider;
  }

  /**
   *  Set the drawable provider used to display additional information.
   *  @param drawableProvider drawable provider or <code>null</code> to remove drawable provider
   */
  public void setDrawableProvider(DrawableProvider drawableProvider)
  {
    this.drawableProvider = drawableProvider;
  }
}
