// ============================================================================
// 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.FramePreparer;
import de.caff.asteroid.MovingGameObject;
import de.caff.gimmicks.swing.ResourcedAction;
import de.caff.i18n.I18n;
import de.caff.i18n.swing.RJCheckBox;

import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedList;

/**
 *  Some buttons to go forward and backward and start replaying.
 *
 *  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 ButtonBar
        extends JToolBar
        implements DumpFileLoadedListener
{
  static {
    I18n.addAppResourceBase("de.caff.asteroid.analysis.resources.ButtonBar");
  }
  /** Default speed: Running slightly faster because 16*60 = 960, but the rendering takes additional time. */
  private static final int DEFAULT_ANIMATION_DELAY = 16;

  private static enum AnimationSpeed
  {
    SIXTEENTH_SPEED("1/16x", 16.0),
    EIGTH_SPEED("1/8x",       8.0),
    QUARTER_SPEED("1/4x",     4.0),
    HALF_SPEED("1/2x",        2.0),
    FULL_SPEED("1x",          1.0),
    DOUBLE_SPEED("2x",        0.5),
    FOUR_TIMES_SPEED("4x",    0.25);

    private String displayString;
    private double factor;

    AnimationSpeed(String display, double factor)
    {
      displayString = display;
      this.factor = factor;
    }

    int getDelay()
    {
      return (int)Math.floor(DEFAULT_ANIMATION_DELAY * factor + 0.5);
    }

    /**
     * String representation.
     * @return the name of this enum constant
     */
    @Override
    public String toString()
    {
      return displayString;
    }
  }
  /** The location of the Java Look&Feel graphics icons, download via
   *  <a href="http://java.sun.com/developer/techDocs/hi/repository/">this repository</a>.
   */
  private static final String MEDIA_BUTTON_ICON_LOCATION = "/toolbarButtonGraphics/media/";
  /** Size to use for media buttons. */
  private static final String MEDIA_BUTTON_ICON_EXTENSION = "16.gif";

  private static Icon getIcon(String iconName)
  {
    return new ImageIcon(ButtonBar.class.getResource(MEDIA_BUTTON_ICON_LOCATION + iconName + MEDIA_BUTTON_ICON_EXTENSION));
  }

  private AbstractAction playAction;
  private ResourcedAction skipBackAction;
  private ResourcedAction frameBackAction;
  private ResourcedAction frameForwardAction;
  private ResourcedAction skipForwardAction;
  private JCheckBox velocityCheckbox;
  private JCheckBox timeCheckBox;
  private JCheckBox antialiasCheckbox;
  private boolean running;
  private TimeLine timeLine;
  private Collection<AnimationListener> animationListeners = new LinkedList<AnimationListener>();
  private int animationDelay = DEFAULT_ANIMATION_DELAY;
  private FramePreparer velocityPreparer;

  public ButtonBar(final TimeLine timeLine, final EnhancedFrameDisplay frameDisplay, FramePreparer velocityPreparer)
  {
    this.timeLine = timeLine;
    this.velocityPreparer = velocityPreparer;
    skipBackAction = new ResourcedAction("actSkipBack")
    {
      public void actionPerformed(ActionEvent e)
      {
        DumpFile.Mark marker = timeLine.getPreviousMarker(timeLine.getCurrentIndex());
        if (marker != null) {
          timeLine.setCurrentIndex(marker.getFrameIndex());
        }
      }

      @Override
      protected Object clone() throws CloneNotSupportedException
      {
        return super.clone();
      }
    };
    add(skipBackAction);

    frameBackAction = new ResourcedAction("actFrameBack")
    {
      public void actionPerformed(ActionEvent e)
      {
        timeLine.setCurrentIndex(timeLine.getCurrentIndex() - 1);
      }

      @Override
      protected Object clone() throws CloneNotSupportedException
      {
        return super.clone();
      }
    };
    add(frameBackAction);
    addSeparator();

    playAction = new ResourcedAction("actPlay")
    {
      public void actionPerformed(ActionEvent e)
      {
        toggleRunning();
      }

      @Override
      protected Object clone() throws CloneNotSupportedException
      {
        return super.clone();
      }
    };
    playAction.putValue(Action.SMALL_ICON, getIcon("Play"));
    add(playAction);
    addSeparator();

    frameForwardAction = new ResourcedAction("actFrameForward")
    {
      public void actionPerformed(ActionEvent e)
      {
        timeLine.setCurrentIndex(timeLine.getCurrentIndex() + 1);
      }

      @Override
      protected Object clone() throws CloneNotSupportedException
      {
        return super.clone();
      }
    };
    add(frameForwardAction);
    skipForwardAction = new ResourcedAction("actSkipForward")
    {
      public void actionPerformed(ActionEvent e)
      {
        DumpFile.Mark marker = timeLine.getNextMarker(timeLine.getCurrentIndex());
        if (marker != null) {
          timeLine.setCurrentIndex(marker.getFrameIndex());
        }
      }

      @Override
      protected Object clone() throws CloneNotSupportedException
      {
        return super.clone();
      }
    };
    add(skipForwardAction);

    addSeparator(new Dimension(16, 16));
    velocityCheckbox = new RJCheckBox("cbVelocities", false);
    add(velocityCheckbox);
    velocityCheckbox.addItemListener(new ItemListener()
    {
      public void itemStateChanged(ItemEvent e)
      {
        runVelocityPreparer(timeLine.getDumpFile());
      }
    });
    timeCheckBox = new RJCheckBox("cbTime", true);
    add(timeCheckBox);
    timeCheckBox.addItemListener(new ItemListener()
    {
      public void itemStateChanged(ItemEvent e)
      {
        frameDisplay.setShowingSessionTime(timeCheckBox.isSelected());
      }
    });
    antialiasCheckbox = new RJCheckBox("cbAntialias", false);
    add(antialiasCheckbox);
    antialiasCheckbox.addItemListener(new ItemListener()
    {
      public void itemStateChanged(ItemEvent e)
      {
        frameDisplay.setUsingAntialising(antialiasCheckbox.isSelected());
      }
    });

    addSeparator(new Dimension(16, 16));
    final JComboBox speeds = new JComboBox(AnimationSpeed.values());
    speeds.setSelectedItem(AnimationSpeed.FULL_SPEED);
    speeds.setToolTipText(I18n.getString("tttSpeeds"));
    speeds.setMaximumSize(new Dimension(100, speeds.getPreferredSize().height));
    add(speeds);
    speeds.addItemListener(new ItemListener()
    {
      public void itemStateChanged(ItemEvent e)
      {
        animationDelay = ((AnimationSpeed)speeds.getSelectedItem()).getDelay();
      }
    });

    timeLine.addDumpFileLoadedListener(this);
    loadedDumpfile(timeLine.getDumpFile());
  }

  private void runVelocityPreparer(DumpFile dumpFile)
  {
    if (dumpFile != null) {
      if (velocityCheckbox.isSelected()) {
        dumpFile.runPreparer(this.velocityPreparer);
      }
      else {
        // remove all velocities
        dumpFile.runPreparer(new FramePreparer()
        {
          public void prepareFrames(LinkedList<FrameInfo> frameInfos)
          {
            FrameInfo last = frameInfos.getLast();
            for (MovingGameObject mgo: last.getMovingGameObjects()) {
              mgo.setVelocity(0, 0);
            }
          }
        });
      }
    }
  }

  /** Toggle running the animation. */
  private void toggleRunning()
  {
    synchronized (playAction) {
      if (running) {
        stopRunning();
      }
      else {
        startRunning();
      }
    }
  }

  /**
   *  Stop the animation if it is running.
   */
  public void stopAnimation()
  {
    synchronized (playAction) {
      stopRunning();
    }
  }

  /**
   *  Start the animation if it is not running.
   */
  public void startAnimation()
  {
    synchronized (playAction) {
      startRunning();
    }
  }

  /** Internal method used to start running the animation. */
  private void startRunning()
  {
    if (!running  &&  timeLine.getDumpFile() != null) {
      running = true;
      playAction.putValue(Action.SMALL_ICON, getIcon("Stop"));
      Thread thread = new Thread(new Runnable() {
        public void run()
        {
          while (true) {
            synchronized (playAction) {
              if (!running) {
                return;
              }
            }
            final int current = timeLine.getCurrentIndex();
            if (current+1 == timeLine.getFrameCount()) {
              toggleRunning();
            }
            if (current >= 0) {
              SwingUtilities.invokeLater(new Runnable() {
                public void run()
                {
                  timeLine.setCurrentIndex(current + 1);
                }
              });
            }
            else {
              toggleRunning();
            }
            try {
              Thread.sleep(animationDelay);
            } catch (InterruptedException e) {
              e.printStackTrace();
            }
          }
        }
      });
      informAnimationListeners(true);
      thread.start();
    }
  }

  /** Internal method used to stop running the animation. */
  private void stopRunning()
  {
    if (running) {
      running = false;
      informAnimationListeners(false);
      playAction.putValue(Action.SMALL_ICON, getIcon("Play"));
    }
  }

  /**
   *  Is the animation running?
   *  @return the answer
   */
  public boolean isAnimationRunning()
  {
    synchronized (playAction) {
      return running;
    }
  }

  /**
   *  Add an animation listener which is informed when the animation starts or stops.
   *  @param listener listener to add
   */
  public void addAnimationListener(AnimationListener listener)
  {
    synchronized (animationListeners) {
      animationListeners.add(listener);
    }
  }

  /**
   *  Remove an animation listener.
   *  @param listener listener to remove
   *  @return was the listener found and removed?
   */
  public boolean removeAnimationListener(AnimationListener listener)
  {
    synchronized (animationListeners) {
      return animationListeners.remove(listener);
    }
  }

  /**
   *  Inform the animation listeners.
   *  @param starting animation starting?
   */
  private void informAnimationListeners(boolean starting)
  {
    Collection<AnimationListener> listeners;
    synchronized (animationListeners) {
      listeners = new ArrayList<AnimationListener>(animationListeners);
    }
    for (AnimationListener listener: listeners) {
      if (starting) {
        listener.animationStarting();
      }
      else {
        listener.animationEnded();
      }
    }
  }

  /**
   * Called when a dump file was loaded.
   *
   * @param dumpFile dump file (may be <code>null</code> when loading starts)
   */
  public void loadedDumpfile(DumpFile dumpFile)
  {
    boolean active = dumpFile != null;
    for (Action action: new Action[] {
        skipBackAction, frameBackAction, playAction, frameForwardAction, skipForwardAction
    }) {
      action.setEnabled(active);
    }
    runVelocityPreparer(dumpFile);
  }
}
