// ============================================================================
// 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.i18n.I18n;
import de.caff.util.KnockOffListener;
import de.caff.util.Worker;
import de.caff.util.debug.Debug;

import javax.swing.*;
import java.applet.Applet;
import java.awt.*;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedList;

/**
 *  Internal part of analysis applet or frame started by analysis applet.
 */
public class AnalysisAppletComponent
        extends JPanel
        implements AnalysisAppletDisplay,
                   GameData
{
  static {
    I18n.addAppResourceBase("de.caff.asteroid.analysis.resources.AnalysisAppletComponent");
  }

  private static final String CARD_STANDARD = "standard";
  private static final String CARD_PROGRESS = "progress";

  /**
   *  Listener for newly loaded dump files.
   */
  public static interface DumpFileListener
  {
    /**
     *  Called if a new dump file is loaded.
     *  @param dumpFile new dump file, maybe <code>null</code> if loading failed
     */
    void dumpFileLoaded(DumpFile dumpFile);
  }

  private class MinimalInfo
          extends JPanel
          implements FrameListener,
                     AnimationListener
  {
    private JLabel fileInfo  = new JLabel();
    private JLabel frameInfo = new JLabel();
    private JLabel keyInfo   = new JLabel();
    private boolean displayKeys = true;

    MinimalInfo()
    {
      super(new BorderLayout());
      Box box = Box.createHorizontalBox();

      box.add(fileInfo);
      box.add(frameInfo);
      box.add(Box.createHorizontalGlue());
      box.add(keyInfo);
      add(box, BorderLayout.CENTER);
    }
    /**
     * Called each time a frame is received.
     * <p/>
     * <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)
    {
      if (frame == null) {
        frameInfo.setText("???");
        keyInfo.setText("???");
      }
      else {
        frameInfo.setText(String.format(" #%d", frame.getIndex()));
        if (!displayKeys) {
          // no need to flicker
          keyInfo.setText("[ANIM]");
        }
        else {
          FrameKeyInfo info = timeLine.getCurrentInfo();
          java.util.List<FrameKeyInfo.ButtonInfo> buttons = info.getButtons();
          StringBuilder b = new StringBuilder();
          if (!buttons.isEmpty()) {
            Buttons keys = buttons.get(buttons.size() - 1).getButtons();
            if (keys.isFirePressed()) {
              b.append("<FIRE>");
            }
            if (keys.isHyperspacePressed()) {
              b.append("<HYPER>");
            }
            if (keys.isThrustPressed()) {
              b.append("<THRUST>");
            }
            if (keys.isLeftPressed()) {
              b.append("<LEFT>");
            }
            if (keys.isRightPressed()) {
              b.append("<RIGHT>");
            }
            if (keys.isStartPressed()) {
              b.append("<START>");
            }
          }
          keyInfo.setText(b.length() == 0  ?  "<>"  :  b.toString());
        }
      }
    }

    /**
     *  Refresh dump file information.
     */
    void refreshDumpFileInfo()
    {
      DumpFile dumpFile = timeLine.getDumpFile();
      if (dumpFile == null) {
        fileInfo.setText("???");
        frameReceived(null);
      }
      else {
        fileInfo.setText(dumpFile.getShortInfo());
        frameReceived(timeLine.getCurrentInfo().getFrameInfo());
      }
    }

    /**
     * Called when the animation is about to start.
     */
    public synchronized void animationStarting()
    {
      displayKeys = false;
    }

    /**
     * Called when the animation has ended.
     */
    public synchronized void animationEnded()
    {
      displayKeys = true;
      SwingUtilities.invokeLater(new Runnable()
      {
        public void run()
        {
          refreshDumpFileInfo();
        }
      });
    }
  }
  
  private Applet applet;
  private CardLayout cardLayout;
  private JPanel     cardPanel;
  private TimeLine timeLine;
  private ProgressTimeLine progress;
  private MinimalInfo infoLine;
  private Collection<DumpFileListener> dumpFileListeners = new LinkedList<DumpFileListener>();
  private FrameKeyInfoDisplay frameKeyInfoDisplay;


  /**
   * Constructor.
   * @param applet the applet which is showing this component
   */
  public AnalysisAppletComponent(Applet applet)
  {
    this.applet = applet;
    timeLine = new TimeLine(null);
    progress = new ProgressTimeLine();
    infoLine = new MinimalInfo();
    timeLine.addFrameListener(infoLine);
    infoLine.setBorder(BorderFactory.createEtchedBorder());
    frameKeyInfoDisplay = new FrameKeyInfoDisplay(timeLine, new SimpleVelocityPreparer(), null, true);
    frameKeyInfoDisplay.addAnimationListener(infoLine);

    cardLayout = new CardLayout();
    cardPanel = new JPanel(cardLayout);
    cardPanel.add(timeLine, CARD_STANDARD);
    cardPanel.add(progress, CARD_PROGRESS);

    setLayout(new BorderLayout());
    add(cardPanel, BorderLayout.SOUTH);
    add(infoLine, BorderLayout.NORTH);
    add(frameKeyInfoDisplay);
  }

  /**
   *  Set the currently displayed dump file.
   *  @param filename dump file name
   *  @param autoStart start automatically after file is loaded?
   */
  public void showDumpFileDirect(final String filename, final boolean autoStart)
  {
    stopAnimation();
    frameKeyInfoDisplay.showMessage(I18n.getString("msgLoading"));
    timeLine.setDumpFile(null);
    infoLine.refreshDumpFileInfo();
    KnockOffListener ko = new KnockOffListener()
    {
      public void knockedOff(Worker worker)
      {

        setCursor(Cursor.getDefaultCursor());
        cardLayout.show(cardPanel, CARD_STANDARD);
        try {
          worker.rethrow();

          DumpLoader loader = (DumpLoader)worker;
          timeLine.setDumpFile(loader.getDumpFile());
          infoLine.refreshDumpFileInfo();
          applet.getAppletContext().showStatus(I18n.format("msgLoaded", filename));
          if (autoStart) {
            startAnimation();
          }
        } catch (Throwable x) {
          Debug.error(x);
          JOptionPane.showMessageDialog(AnalysisAppletComponent.this,
                                        new Object[] {
                                                I18n.format("errNoOpen", filename),
                                                x.getLocalizedMessage()
                                        },
                                        I18n.getString("titleReadError"),
                                        JOptionPane.ERROR_MESSAGE);
        }
        frameKeyInfoDisplay.clearMessage();
        informDumpFileListeners(timeLine.getDumpFile());
      }
    };
    try {
      URL fileURL = new URL(applet.getDocumentBase(), filename);
      Worker worker = new DumpLoader(fileURL.openStream(),
                                     filename,
                                     ko,
                                     progress);
      setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
      cardLayout.show(cardPanel, CARD_PROGRESS);
      new Thread(worker, "Loader").start();
    } catch (Throwable e) {
      Debug.error(e);
      JOptionPane.showMessageDialog(this,
                                    new Object[] {
                                            I18n.format("errNoOpen", filename),
                                            e.getLocalizedMessage()
                                    },
                                    I18n.getString("titleReadError"),
                                    JOptionPane.ERROR_MESSAGE);
      if (e instanceof OutOfMemoryError) {
        System.out.println(String.format("Free memory:  %10d", Runtime.getRuntime().freeMemory()));
        System.out.println(String.format("Total memory: %10d", Runtime.getRuntime().totalMemory()));
        System.out.println(String.format("Max memory:   %10d", Runtime.getRuntime().maxMemory()));
      }
    }
  }

  /**
   *  Load a new dump file.
   *  Invoke this method from JavaScript as a workaround to bug #6669818.
   *  @param dumpFile dump file
   *  @param autoStart start automatically after file is loaded?
   */
  public void showDumpFile(final String dumpFile, final boolean autoStart)
  {
    SwingUtilities.invokeLater(new Runnable()
    {
      public void run()
      {
        showDumpFileDirect(dumpFile, autoStart);
      }
    });
  }

  /**
   * Start animation if it is not running.
   */
  public void startAnimation()
  {
    frameKeyInfoDisplay.startAnimation();
  }

  /**
   * Stop animation if it is running.
   */
  public void stopAnimation()
  {
    frameKeyInfoDisplay.stopAnimation();
  }

  /**
   *  Add a dump file listener.
   *  @param listener listener to add
   */
  public void addDumpFileListener(DumpFileListener listener)
  {
    synchronized (dumpFileListeners) {
      dumpFileListeners.add(listener);
    }
  }

  /**
   *  Remove a dump file listener.
   *  @param listener listener to remove
   *  @return <code>true</code> if the listener was remove, <code>false</code> if the listener was not found
   */
  public boolean removeDumpFileListener(DumpFileListener listener)
  {
    synchronized (dumpFileListeners) {
      return dumpFileListeners.remove(listener);
    }
  }

  /**
   *  Inform dump file listeners that a new dump file was loaded.
   *  @param dumpFile new dump file
   */
  private void informDumpFileListeners(DumpFile dumpFile)
  {
    Collection<DumpFileListener> listeners;
    synchronized (dumpFileListeners) {
      listeners = new ArrayList<DumpFileListener>(dumpFileListeners);
    }
    for (DumpFileListener listener: listeners) {
      listener.dumpFileLoaded(dumpFile);
    }
  }
}
