/*
 * Open Source Physics software is free software as described near the bottom of this code file.
 *
 * For additional information and documentation on Open Source Physics please see:
 * <http://www.opensourcephysics.org/>
 */

package org.opensourcephysics.media.core;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Image;
import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.Box;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JComboBox;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JToolBar;
import javax.swing.SwingUtilities;
import javax.swing.WindowConstants;
import org.opensourcephysics.controls.XML;
import org.opensourcephysics.display.Drawable;
import org.opensourcephysics.media.gif.GifVideoRecorder;
import org.opensourcephysics.tools.VideoCaptureTool;

/**
 * A video capture utility using media classes.
 *
 * @author Douglas Brown
 * @version 1.0
 */
public class VideoGrabber extends VideoCaptureTool {
  /**
   * A shared video capture tool.
   */
  public static VideoGrabber VIDEO_CAPTURE_TOOL = null;
  static Dimension defaultSize = new Dimension(320, 240);
  BufferedImage scratch;
  VideoRecorder recorder;
  VideoType videoType;
  VideoPanel recorderPanel;
  VideoPanel playerPanel;
  String playerFileName;
  JFrame recorderFrame;
  JFrame playerFrame;
  Action clearAction;
  Action saveAsAction;
  Action recordAction;
  Action vidTypeAction;
  Action fpsAction;
  JButton clearButton;
  JButton saveAsButton;
  JCheckBox recordCheckBox;
  JCheckBox loopCheckBox;
  JComboBox vidTypeDropDown;
  JLabel fpsLabel;
  JComboBox fpsDropDown;
  boolean recording = true;
  boolean saved = false;
  int frameCount = 0;
  Dimension imageSize;
  int[] pixels = new int[1];
  Map<String, VideoType> vidTypes = new HashMap<String, VideoType>();
  boolean previewAll = false;

  /**
   * Constructor that uses default video dimensions.
   */
  public VideoGrabber() {
    this(defaultSize);
  }

  /**
   * Constructor that sets the video dimensions.
   *
   * @param dim the dimension
   */
  public VideoGrabber(Dimension dim) {
    super(false);
    imageSize = dim;
    createGUI();
    vidTypeAction.actionPerformed(null);
    setRecording(false);
  }

  /**
   * Gets the shared VideoGrabber.
   *
   * @return the shared VideoGrabber
   */
  public static VideoGrabber getTool() {
    if(VIDEO_CAPTURE_TOOL==null) {
      VIDEO_CAPTURE_TOOL = new VideoGrabber();
    }
    return VIDEO_CAPTURE_TOOL;
  }

  /**
   * Gets the shared VideoGrabber and sets the video dimensions.
   *
   * @param dim the dimension
   * @return the shared VideoGrabber
   */
  public static VideoGrabber getTool(Dimension dim) {
    if(VIDEO_CAPTURE_TOOL==null) {
      VIDEO_CAPTURE_TOOL = new VideoGrabber(dim);
    } else {
      VIDEO_CAPTURE_TOOL.imageSize = dim;
      VIDEO_CAPTURE_TOOL.recorderPanel.setPreferredSize(dim);
      VIDEO_CAPTURE_TOOL.recorderFrame.pack();
    }
    return VIDEO_CAPTURE_TOOL;
  }

  /**
   * Clear the video from the tool in preparation for a new video.
   */
  public void clear() {
    //     setRecording(false);
    clearAction.actionPerformed(null);
  }

  /**
   * Adds a frame to the video if it is recording.
   *
   * @param image the frame to be added
   * @return true if frame was added
   */
  public boolean addFrame(BufferedImage image) {
    if(isRecording()) {
      try {
        int w = image.getWidth();
        int h = image.getHeight();
        if(pixels.length!=w*h) {
          pixels = new int[w*h];
        }
        boolean newScratch = false;
        if(previewAll) {
          scratch = null;
        } else if((scratch==null)||(scratch.getWidth()!=w)||(scratch.getHeight()!=h)) {
          scratch = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB);
          newScratch = true;
        }
        BufferedImage copy = previewAll ? 
        		new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB) : scratch;
        image.getRaster().getDataElements(0, 0, w, h, pixels);
        copy.getRaster().setDataElements(0, 0, w, h, pixels);
        Video video = recorderPanel.getVideo();
        if(video==null) {                                     // first frame added
          recorderPanel.setVideo(new ImageVideo(copy));
          recorderPanel.getPlayer().setReadoutTypes("frame", null); //$NON-NLS-1$
          if(recorder instanceof GifVideoRecorder) {
            int i = loopCheckBox.isSelected() ? 0 : 1;
            ((GifVideoRecorder) recorder).getGifEncoder().setRepeat(i);
          }
        } else if(previewAll) {
          ImageVideo imageVid = (ImageVideo) video;
          imageVid.insert(new Image[] {copy}, imageVid.getFrameCount(), null);
        } else if(newScratch) {
          recorderPanel.setVideo(new ImageVideo(copy));
          recorderPanel.getPlayer().setReadoutTypes("frame", null); //$NON-NLS-1$         
        }
        recorder.addFrame(copy);
        frameCount++;
        String item = (String) fpsDropDown.getSelectedItem();
        double dt = 1000.0/Double.parseDouble(item);
        recorderPanel.getPlayer().getClipControl().setFrameDuration(dt);
        recorderPanel.getPlayer().getVideoClip().setStepCount(frameCount);
        recorderPanel.getPlayer().setStepNumber(frameCount-1);
        if(video==null) {                                     // first frame added
          imageSize = new Dimension(image.getWidth(), image.getHeight());
          // give recorderPanel extra room so whole video is visible
          final Dimension dim = new Dimension(image.getWidth()+4, image.getHeight()+4);
          Runnable runner = new Runnable() {
            public void run() {
              recorderPanel.setPreferredSize(dim);
              recorderFrame.pack();
            }

          };
          SwingUtilities.invokeLater(runner);
          refreshGUI();
        }
        return true;
      } catch(IOException ex) {
        return false;
      }
    }
    return false;
  }

  /**
   * Sets the visibility.
   *
   * @param visible true to set this visible
   */
  public void setVisible(boolean visible) {
    recorderFrame.setVisible(visible);
  }

  /**
   * Gets the visibility.
   *
   * @return true if visible
   */
  public boolean isVisible() {
    return recorderFrame.isVisible();
  }

  /**
   * Sets the recording flag.
   *
   * @param record true to record rendered images
   */
  public void setRecording(boolean record) {
    recording = record;
    refreshGUI();
  }

  /**
   * Gets the recording flag.
   *
   * @return true if recording rendered images
   */
  public boolean isRecording() {
    return recording&&(recorder!=null);
  }

  /**
   * Sets the video type.
   *
   * @param type the video type
   */
  public void setVideoType(VideoType type) {
    if((type==null)||(type==videoType)) {
      return;
    }
    videoType = type;
    recorder = type.getRecorder();
    clearAction.actionPerformed(null);
  }

  /**
   * Sets the frame rate.
   *
   * @param fps the frame rate in frames per second
   */
  public void setFrameRate(double fps) {
    fps = Math.max(fps, 1);
    fps = Math.min(fps, 60);
    fps = Math.round(100*fps)/100; // round to nearest .01 fps
    int n = fpsDropDown.getItemCount();
    for(int i = 0; i<n; i++) {
      String item = (String) fpsDropDown.getItemAt(i);
      double dropdownValue = Double.parseDouble(item);
      if(fps==dropdownValue) {
        fpsDropDown.setSelectedIndex(i);
        return;
      }
      if(fps>dropdownValue) {
        String s = String.valueOf(fps);
        fpsDropDown.insertItemAt(s, i);
        fpsDropDown.setSelectedItem(s);
        return;
      }
    }
  }

  /**
   * Saves the video to a file and returns the file name.
   *
   * @return the name of the file, or null if not saved
   */
  public String saveVideoAs() {
    if(recorder!=null) {
      try {
        return recorder.saveVideoAs();
      } catch(IOException ex) {
        JOptionPane.showMessageDialog(null, ex.getMessage(), "File Not Saved", JOptionPane.WARNING_MESSAGE); //$NON-NLS-1$
      }
    }
    return null;
  }

  /**
   * Gets the video recorder.
   *
   * @return the VideoRecorder
   */
  public VideoRecorder getRecorder() {
    return recorder;
  }

  /**
   * Creates the GUI.
   */
  protected void createGUI() {
    createActions();
    // create buttons and dropdowns
    clearButton = new JButton(clearAction);
    saveAsButton = new JButton(saveAsAction);
    recordCheckBox = new JCheckBox(recordAction);
    recordCheckBox.setOpaque(false);
    loopCheckBox = new JCheckBox();
    loopCheckBox.setOpaque(false);
    // fps label and dropdown
    fpsLabel = new JLabel();
    String[] rates = {"30", "29.97", "25", "20", "15",   //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$
                      "12", "10", "8", "6",     //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$
                      "5", "4", "3", "2", "1"}; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$
    fpsDropDown = new JComboBox(rates) {
      public Dimension getMaximumSize() {
        return getMinimumSize();
      }

    };
    fpsDropDown.addActionListener(fpsAction);
    // video type dropdown
    vidTypeDropDown = new JComboBox() {
      public Dimension getMaximumSize() {
        return getMinimumSize();
      }

    };
    // add gif video type
    VideoType vidType;
    String desc;
    // add gif video type using reflection
    try {
      String name = "org.opensourcephysics.media.gif.GifVideoType"; //$NON-NLS-1$
      Class<?> gifClass = Class.forName(name);
      vidType = (VideoType)gifClass.newInstance();
      desc = vidType.getDescription();
      vidTypes.put(desc, vidType);
      vidTypeDropDown.addItem(desc);
    } catch(Exception ex) {
    } catch(Error err) {
    }
    // add Xuggle video type, if available
    try {
      String name = "org.opensourcephysics.media.xuggle.XuggleVideoType"; //$NON-NLS-1$
      Class<?> xuggleClass = Class.forName(name);
      vidType = (VideoType) xuggleClass.newInstance();
      desc = vidType.getDescription();
      vidTypes.put(desc, vidType);
      vidTypeDropDown.addItem(desc);
    } catch(Exception ex) {
    } catch(Error err) {
    }
    // add QT video type, if available
    // modified by W. Christian to use reflection
    try {
      String name = "org.opensourcephysics.media.quicktime.QTVideoType"; //$NON-NLS-1$
      Class<?> qtClass = Class.forName(name);
      vidType = (VideoType) qtClass.newInstance();
      desc = vidType.getDescription();
      vidTypes.put(desc, vidType);
      vidTypeDropDown.addItem(desc);
    } catch(Exception ex) {
    } catch(Error err) {
    }
    // add image video type
    vidType = new ImageVideoType();
    desc = vidType.getDescription();
    vidTypes.put(desc, vidType);
    vidTypeDropDown.addItem(desc);
    vidTypeDropDown.addActionListener(vidTypeAction);
    // create and assemble frame
    recorderFrame = new JFrame();
    recorderFrame.setDefaultCloseOperation(WindowConstants.HIDE_ON_CLOSE);
    recorderFrame.setName("VideoCaptureTool"); //$NON-NLS-1$
    recorderFrame.addWindowListener(new WindowAdapter() {
      public void windowClosing(WindowEvent e) {
        if(recorder instanceof ScratchVideoRecorder) {
          ScratchVideoRecorder svr = (ScratchVideoRecorder) recorder;
          if(svr.scratchFile!=null) {
            try {
              svr.saveScratch();
              svr.scratchFile.delete();
            } catch(IOException ex) {

            /** empty block */
            }
          }
        }
      }

    });
    JPanel contentPane = new JPanel(new BorderLayout());
    recorderFrame.setContentPane(contentPane);
    recorderPanel = new FixedSizeVideoPanel();
    recorderPanel.setPreferredSize(imageSize);
    // bottom panel contains player and buttonBar
    JPanel bottomPanel = new JPanel(new BorderLayout());
    contentPane.add(bottomPanel, BorderLayout.SOUTH);
    JToolBar playerBar = new JToolBar();
    playerBar.setFloatable(false);
    recorderPanel.getPlayer().setBorder(null);
    recorderPanel.setPlayerVisible(false);
    recorderPanel.getPlayer().setLoopingButtonVisible(false);
    contentPane.add(recorderPanel, BorderLayout.CENTER);
    JToolBar buttonBar = new JToolBar();
    buttonBar.setFloatable(false);
    if(previewAll) {
      playerBar.add(recorderPanel.getPlayer());
      bottomPanel.add(playerBar, BorderLayout.CENTER);
    } else {
      buttonBar.add(recorderPanel.getPlayer().readout);
    }
    bottomPanel.add(buttonBar, BorderLayout.SOUTH);
    buttonBar.add(recordCheckBox);
    buttonBar.add(Box.createHorizontalGlue());
    buttonBar.add(clearButton);
    buttonBar.add(saveAsButton);
    // topBar contains other components
    JToolBar topBar = new JToolBar();
    topBar.setFloatable(false);
    contentPane.add(topBar, BorderLayout.NORTH);
    topBar.add(vidTypeDropDown);
    topBar.addSeparator();
    topBar.add(fpsLabel);
    topBar.add(fpsDropDown);
    topBar.addSeparator();
    topBar.add(loopCheckBox);
    topBar.add(Box.createHorizontalGlue());
    recorderFrame.pack();
    // position recorderFrame in top center of screen
    Dimension dim = Toolkit.getDefaultToolkit().getScreenSize();
    int x = (dim.width-recorderFrame.getBounds().width)/2;
    recorderFrame.setLocation(x, 0);
  }

  /**
   * Creates the actions.
   */
  protected void createActions() {
    clearAction = new AbstractAction() {
      public void actionPerformed(ActionEvent e) {
        recorderPanel.setVideo(null);
        recorderPanel.getPlayer().getVideoClip().setStepCount(1);
        if(recorder!=null) {
          String item = (String) fpsDropDown.getSelectedItem();
          double dt = 1000.0/Double.parseDouble(item);
          recorder.setFrameDuration(dt);
          try {
            recorder.createVideo();
            frameCount = 0;
            saved = false;
            recorderPanel.setVideo(null);
            recorderPanel.getPlayer().setReadoutTypes("frame", null); //$NON-NLS-1$         
            recorderPanel.getPlayer().getVideoClip().setStepCount(1);
            refreshGUI();
          } catch(IOException ex) {
            ex.printStackTrace();
          }
        }
        System.gc();                                            // recover resources used by previous ImageVideo
      }

    };
    saveAsAction = new AbstractAction() {
      public void actionPerformed(ActionEvent e) {
        if(recorder!=null) {
          try {
            String name = recorder.saveVideoAs();
            if(name!=null) {
              Video video = videoType.getVideo(name);
              if(playerPanel==null) {
                playerPanel = new FixedSizeVideoPanel();
                playerFrame = new VideoFrame(playerPanel);
                int w = imageSize.width+4;
                int h = imageSize.height+recorderPanel.getPlayer().height+4;
                playerPanel.setPreferredSize(new Dimension(w, h));
                playerFrame.pack();
              }
              playerPanel.setVideo(video);
              if(loopCheckBox.isVisible()&&loopCheckBox.isSelected()) {
                playerPanel.getPlayer().setLooping(true);
                playerPanel.getPlayer().play();
              }
              playerFrame.setVisible(true);
              saved = true;
              playerFileName = name;
              clearAction.actionPerformed(null);
            }
          } catch(IOException ex) {
            ex.printStackTrace();
          }
        }
      }

    };
    recordAction = new AbstractAction() {
      public void actionPerformed(ActionEvent e) {
        recording = !recording;
        refreshGUI();
      }

    };
    vidTypeAction = new AbstractAction() {
      public void actionPerformed(ActionEvent e) {
        Object desc = vidTypeDropDown.getSelectedItem();
        VideoType vidType = vidTypes.get(desc);
        if(vidType!=null) {
          setVideoType(vidType);
        }
      }

    };
    fpsAction = new AbstractAction() {
      public void actionPerformed(ActionEvent e) {
        clearAction.actionPerformed(null);
      }

    };
  }

  /**
   * Refreshes the GUI.
   */
  protected void refreshGUI() {
    recordCheckBox.setSelected(isRecording());
    clearButton.setEnabled(frameCount!=0);
    saveAsButton.setEnabled(frameCount!=0);
    vidTypeDropDown.setEnabled(frameCount==0);
    vidTypeDropDown.setSelectedItem(videoType.getDescription());
    fpsDropDown.setEnabled(frameCount==0);
    fpsDropDown.setVisible(!(videoType instanceof ImageVideoType));
    fpsLabel.setVisible(!(videoType instanceof ImageVideoType));
    loopCheckBox.setEnabled(frameCount==0);
    recorderPanel.getPlayer().readout.setEnabled(frameCount!=0);
    recordCheckBox.setText(MediaRes.getString("VideoGrabber.Action.Capture")); //$NON-NLS-1$
    loopCheckBox.setText(MediaRes.getString("VideoGrabber.Action.Loop"));      //$NON-NLS-1$
    loopCheckBox.setVisible(videoType.getClass().getSimpleName().equals("GifVideoType")); //$NON-NLS-1$
    fpsLabel.setText(MediaRes.getString("VideoGrabber.Label.PlayRate")+" ");   //$NON-NLS-1$ //$NON-NLS-2$
    clearButton.setText(MediaRes.getString("VideoGrabber.Action.Clear"));      //$NON-NLS-1$
    saveAsButton.setText(MediaRes.getString("VideoGrabber.Action.SaveAs"));    //$NON-NLS-1$
    if(recordCheckBox.isSelected()) {
      recorderFrame.setTitle(MediaRes.getString("VideoGrabber.Title.Capturing") //$NON-NLS-1$
                             +videoType.getDescription());
    } else {
      recorderFrame.setTitle(videoType.getDescription());
    }
    if((playerFrame!=null)&&playerFrame.isVisible()) {
      playerFrame.setTitle(MediaRes.getString("VideoGrabber.Title.Saved") //$NON-NLS-1$
                           +XML.getName(playerFileName));
    }
  }

  private class FixedSizeVideoPanel extends VideoPanel {
    FixedSizeVideoPanel() {
      setBackground(Color.black);
      setDrawingInImageSpace(true);
      setShowCoordinates(false);
    }

    protected void scale(ArrayList<Drawable> drawables) {
      // set image border so video size remains fixed
      double w = imageWidth;
      double wBorder = (getWidth()-w-1)*0.5/w;
      double h = imageHeight;
      double hBorder = (getHeight()-h-1)*0.5/h;
      double border = Math.min(wBorder, hBorder);
      super.setImageBorder(border);
      super.scale(drawables);
    }

  }

}

/*
 * Open Source Physics software is free software; you can redistribute
 * it and/or modify it under the terms of the GNU General Public License (GPL) as
 * published by the Free Software Foundation; either version 2 of the License,
 * or(at your option) any later version.

 * Code that uses any portion of the code in the org.opensourcephysics package
 * or any subpackage (subdirectory) of this package must must also be be released
 * under the GNU GPL license.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston MA 02111-1307 USA
 * or view the license online at http://www.gnu.org/copyleft/gpl.html
 *
 * Copyright (c) 2007  The Open Source Physics project
 *                     http://www.opensourcephysics.org
 */
