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

import java.awt.*;
import java.awt.event.*;
import java.awt.geom.GeneralPath;
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;

/**
 *  Component to access the frames using a time line.
 *
 *  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 TimeLine
        extends AbstractBasicTimeLine
        implements GameData
{
  /** Color for the current frame marker. */
  private static final Color MARKER_COLOR = Color.black;

  /** The displayed dump file. */
  private DumpFile dumpFile;
  /** The current index into the dump file. */
  private int currentIndex = 0;
  /** Registered frame listeners. */
  private java.util.List<FrameListener> frameListeners = new LinkedList<FrameListener>();
  /** The list of change listeners. */
  private List<DumpFileChangeListener> changeListeners = new LinkedList<DumpFileChangeListener>();
  /** The list of listeners for dump file loads. */
  private List<DumpFileLoadedListener> loadedListeners = new LinkedList<DumpFileLoadedListener>();
  /** Mouse wheel listener. */
  private MouseWheelListener mouseWheelListener = new MouseWheelListener()
  {
    public void mouseWheelMoved(MouseWheelEvent e)
    {
      if ((e.getModifiersEx() & MouseWheelEvent.SHIFT_DOWN_MASK) != 0) {
        int n = e.getWheelRotation();
        if (n > 0) {
          while (n-- > 0) {
            DumpFile.Mark mark = getPreviousMarker(getCurrentIndex());
            if (mark != null) {
              setCurrentIndex(mark.getFrameIndex());
            }
          }
        }
        else {
          while (n++ < 0) {
            DumpFile.Mark mark = getNextMarker(getCurrentIndex());
            if (mark != null) {
              setCurrentIndex(mark.getFrameIndex());
            }
          }
        }
      }
      else {
        setCurrentIndex(getCurrentIndex() - e.getWheelRotation());
      }
    }
  };

  /**
   *  Constructor.
   *  @param dumpFile dump file to display
   */
  public TimeLine(DumpFile dumpFile)
  {
    setMinimumSize(new Dimension(100, 32));
    setPreferredSize(getMinimumSize());
    setOpaque(true);

    addMouseListener(new MouseAdapter() {
      /**
       * {@inheritDoc}
       */
      @Override
      public void mousePressed(MouseEvent e)
      {
        if (e.getButton() == MouseEvent.BUTTON1) {
          setCurrentFrom(e);
        }
      }
    });

    addMouseMotionListener(new MouseMotionAdapter()
    {
      /**
       * {@inheritDoc}
       */
      @Override
      public void mouseDragged(MouseEvent e)
      {
        if ((e.getModifiersEx() & MouseEvent.BUTTON1_DOWN_MASK) != 0) {
          setCurrentFrom(e);
        }
      }
    });

    addMouseWheelListener(mouseWheelListener);

    setDumpFile(dumpFile);
  }

  /**
   *  Shot the given dump file.
   *  @param dumpFile dump file
   */
  public void setDumpFile(DumpFile dumpFile)
  {
    if (this.dumpFile != null) {
      for (DumpFileChangeListener l: changeListeners) {
        this.dumpFile.removeDumpFileChangeListener(l);
      }
    }
    this.dumpFile = dumpFile;
    if (dumpFile != null) {
      for (DumpFileChangeListener l: changeListeners) {
        dumpFile.addDumpFileChangeListener(l);
      }
    }
    currentIndex = -1;
    setCurrentIndex(0);
    for (DumpFileLoadedListener listener: loadedListeners) {
      listener.loadedDumpfile(dumpFile);
    }
  }

  /**
   *  Set the current index from the position of a mouse event.
   *  @param e mouse event
   */
  private void setCurrentFrom(MouseEvent e)
  {
    int index = getIndexAt(e.getPoint());
    if (index >= 0) {
      DumpFile.Mark mark = getNearestMarker(index);
      int screenX = indexToScreen(mark.getFrameIndex());
      if (Math.abs(screenX - e.getX()) < 2) {
        index = mark.getFrameIndex();
      }
      setCurrentIndex(index);
    }
  }

  /**
   *  Set the current index.
   *  Does nothing on a timeline without frames.
   *  @param frameIndex new current index (will be mapped to existing border if too high or too low)
   *  @see #hasFrames()
   */
  public void setCurrentIndex(int frameIndex)
  {
    if (hasFrames()) {
      if (frameIndex < 0) {
        frameIndex = 0;
      }
      else if (frameIndex >= dumpFile.getInfos().size()) {
        frameIndex = dumpFile.getInfos().size() - 1;
      }
      if (frameIndex != currentIndex) {
        currentIndex = frameIndex;
        informFrameListeners(dumpFile.getInfos().get(frameIndex).getFrameInfo());
        repaint();
      }
    }
  }

  /**
   *  Does this timeline contain frames?
   *  @return <code>true</code>: this timeline is useful because it contains at least one frame<br>
   *          <code>false</code>: this timeline is invalid
   */
  public boolean hasFrames()
  {
    return dumpFile != null  &&  !dumpFile.getInfos().isEmpty();
  }

  /**
   *  Get the index under a given point.
   *  @param p point
   *  @return the index or <code>-1</code> if this timeline has no frames
   *  @see #hasFrames()
   */
  public int getIndexAt(Point p)
  {
    return isValid() ? screenToIndex(p.x) : -1;
  }

  /**
   *  Get the currently marked index.
   *  @return marked index
   */
  public int getCurrentIndex()
  {
    return currentIndex;
  }

  /**
   *  Get the currently displayed info.
   *  @return current info or <code>null</code>
   */
  public FrameKeyInfo getCurrentInfo()
  {
    return hasFrames() ? dumpFile.getInfos().get(getCurrentIndex()) : null;
  }

  /**
   *  Get the nearest marker.
   *  @param index index from which to search
   *  @return nearest marker to index or <code>null</code> if
   */
  public DumpFile.Mark getNearestMarker(int index)
  {
    DumpFile.Mark nearest = null;
    if (hasFrames()) {
      int minDistance = Integer.MAX_VALUE;
      for (DumpFile.Mark mark: getMarks()) {
        int dist = Math.abs(mark.getFrameIndex() - index);
        if (dist < minDistance) {
          minDistance = dist;
          nearest = mark;
        }
        else if (dist > minDistance) {
          break;
        }
      }
    }
    return nearest;
  }

  /**
   *  Get the marker following a given frame index.
   *  Even if index is at a marker position that marker is not returned.
   *  @param index frame index
   *  @return next marker or <code>null</code> if there is no marker following
   */
  public DumpFile.Mark getNextMarker(int index)
  {
    DumpFile.Mark next = null;
    if (hasFrames()) {
      for (DumpFile.Mark mark: getMarks()) {
        if (mark.getFrameIndex() > index) {
          next = mark;
          break;
        }
      }
    }
    return next;
  }

  /**
   *  Get the marker before a given frame index.
   *  Even if index is at a marker position that marker is not returned.
   *  @param index frame index
   *  @return next marker or <code>null</code> if there is no marker before the postition
   */
  public DumpFile.Mark getPreviousMarker(int index)
  {
    DumpFile.Mark prev = null;
    if (hasFrames()) {
      DumpFile.Mark last = null;
      for (DumpFile.Mark mark: getMarks()) {
        if (mark.getFrameIndex() >= index) {
          prev = last;
          break;
        }
        last = mark;
      }
    }
    return prev;
  }

  /**
   *  Get the dump file.
   *  @return dump file
   */
  public DumpFile getDumpFile()
  {
    return dumpFile;
  }

  /**
   *  Map a screen position to an index.
   *  @param screenX x position on this component
   *  @return associated index
   */
  private int screenToIndex(int screenX)
  {
    return isValid() ? (int)((screenX - MARKER_BORDER_X) / getScale()) : -1;
  }

  /**
   *  Add a frame listener which is called on every frame received.
   *  @param listener listener to add
   */
  public void addFrameListener(FrameListener listener)
  {
    frameListeners.add(listener);
  }

  /**
   *  Remove a frame listener.
   *  @param listener listener to remove
   *  @return <code>true</code> if the listener was removed<br>
   *          <code>false</code> otherwise
   */
  public boolean removeFrameListener(FrameListener listener)
  {
    return frameListeners.remove(listener);
  }

  /**
   *  Inform all registered frame listeners.
   *  @param frameInfo frame info received
   */
  private void informFrameListeners(FrameInfo frameInfo)
  {
    for (FrameListener listener: frameListeners) {
      listener.frameReceived(frameInfo);
    }
  }

  /**
   *  Add a dump file loaded listener which is called every time the underlaying dumpfile changes.
   *  @param listener listener to add
   */
  public void addDumpFileLoadedListener(DumpFileLoadedListener listener)
  {
    loadedListeners.add(listener);
  }

  /**
   *  Remove a dumpfile loaded listener.
   *  @param listener listener to remove
   *  @return <code>true</code> if the listener was removed<br>
   *          <code>false</code> otherwise
   */
  public boolean removeDumpFileLoadedListener(DumpFileLoadedListener listener)
  {
    return loadedListeners.remove(listener);
  }

  /**
   *  Get the number of frames in this time line.
   *  @return number of frames
   */
  public int getFrameCount()
  {
    return dumpFile != null ?
            dumpFile.getInfos().size() :
            0;
  }

  /**
   *  Get the time a session is running.
   *  This time begins at game start.
   *  @param frameIndex frame index for which the time is requested
   *  @return seconds the game is running or <code>0</code> if unknown
   */
  public int getSessionTime(int frameIndex)
  {
    if (hasFrames()) {
      if (frameIndex < 0) {
        frameIndex = 0;
      }
      else if (frameIndex >= getFrameCount()) {
        frameIndex = getFrameCount() - 1;
      }
      DumpFile.Mark prevMarker = getPreviousMarker(frameIndex);
      while (prevMarker != null) {
        if (prevMarker.getMarkType() == DumpFile.MarkType.GAME_START) {
          return (frameIndex - prevMarker.getFrameIndex()) / FRAMES_PER_SECOND;
        }
        prevMarker = getPreviousMarker(prevMarker.getFrameIndex());
      }
      return frameIndex / FRAMES_PER_SECOND;
    }
    return 0;
  }

  /**
   *  Get the current session time.
   *  @return current session time
   */
  public int getCurrentSessionTime()
  {
    return getSessionTime(getCurrentIndex());
  }

  /**
   *  Add a dump file change listener called if the dumpe file content changes.
   *  @param listener listener to add
   */
  public void addDumpFileChangeListener(DumpFileChangeListener listener)
  {
    changeListeners.add(listener);
    if (dumpFile != null) {
      dumpFile.addDumpFileChangeListener(listener);
    }
  }

  /**
   *  Remove a dump file change listener.
   *  @param listener listener to remove
   *  @return <code>true</code> if the listener was removed<br>
   *          <code>false</code> otherwise
   */
  public boolean removeDumpFileChangeListener(DumpFileChangeListener listener)
  {
    boolean result = changeListeners.remove(listener);
    if (result  &&  dumpFile != null) {
      dumpFile.removeDumpFileChangeListener(listener);
    }
    return result;
  }

  /**
   *  Get mouse wheel listener.
   *  The return listener steps through the frames or, if SHIFT is pressed, markers.
   *  @return mouse wheel listener
   */
  public MouseWheelListener getMouseWheelListener()
  {
    return mouseWheelListener;
  }

  /**
   *  Get the marks to display.
   *  @return marks
   */
  protected Collection<DumpFile.Mark> getMarks()
  {
    return dumpFile.getMarks();
  }

  /**
   * Paints the component.
   *
   * @param g the <code>Graphics</code> object to protect
   * @see #paint
   * @see javax.swing.plaf.ComponentUI
   */
  @Override
  protected void paintComponent(Graphics g)
  {
    super.paintComponent(g);
    if (getFrameCount() > 0) {
      Graphics2D g2 = (Graphics2D)g;
      g2.setColor(MARKER_COLOR);
      int markerPos = indexToScreen(currentIndex);
      g2.drawLine(markerPos, 0, markerPos, getHeight());
      GeneralPath triangles = new GeneralPath();
      triangles.moveTo(markerPos, MARKER_BORDER_Y);
      triangles.lineTo(markerPos + MARKER_BORDER_Y /2, 1);
      triangles.lineTo(markerPos - MARKER_BORDER_Y /2, 1);
      triangles.closePath();
      int y = getHeight() - 1;
      triangles.moveTo(markerPos, y - MARKER_BORDER_Y);
      triangles.lineTo(markerPos + MARKER_BORDER_Y /2, y);
      triangles.lineTo(markerPos - MARKER_BORDER_Y /2, y);
      triangles.closePath();
      g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
      g2.fill(triangles);
    }

  }
}
