package resample;

import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.GridLayout;
import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ComponentEvent;
import java.awt.event.ComponentListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionListener;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.text.DecimalFormat;

import javax.imageio.ImageIO;
import javax.swing.BorderFactory;
import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JComboBox;
import javax.swing.JComponent;
import javax.swing.JFileChooser;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextField;
import javax.swing.SwingConstants;
import javax.swing.border.CompoundBorder;
import javax.swing.border.EtchedBorder;

import resample.function.BSplineFilter;
import resample.function.BoxFilter;
import resample.function.CatmullRomCubicFilter;
import resample.function.FilterFunction;
import resample.function.FilterOperations;
import resample.function.GaussianFilter;
import resample.function.MitchellNetravaliCubicFilter;
import resample.function.TentFilter;
import resample.function.UnSharpFilter;
import resample.image.Color;
import resample.image.Image;
import resample.image.ResizableImage;

/**
 * Main class define the application UI and answers all the action call backs to
 * the UI. The details of this class are specific to the UI and should generally
 * not be modified.
 * 
 * @author arbree Aug 30, 2005 MainFrame.java Copyright 2005 Program of Computer
 *         Graphics, Cornell University
 */
public class MainFrame extends JFrame implements ActionListener, ComponentListener, MouseMotionListener {

  //String constants defining the menu elements
  private static final String MENU_NAME = "File";
  private static final String MENU_FILE_OPEN_TEXT = "Open";
  private static final String MENU_FILE_SAVE_AS_TEXT = "Save As...";
  private static final String MENU_FILE_EXIT_TEXT = "Exit";
  private static final String CHCK_BX_MODE = "Auto-Resample Mode";
  private static final String BTN_FILTER_TEXT = "Filter";
  private static final String BTN_APPLY_TEXT = "Apply";
  private static final String LBL_FILTER_TIME_TEXT = "Time Spent Filtering: ";
  private static final String COMBO_BOX_FILTER = "Filter Box";
  
  //Default size of the image window
  private static final int DEFAULT_WIDTH = 600;
  private static final int DEFAULT_HEIGHT = 800;

  //The screen size
  private static final Dimension SCREEN_SIZE = Toolkit.getDefaultToolkit().getScreenSize();

  //UI Components  
  private JFileChooser fileChooser;
  private JComponent imageRendererContainer;
  private JScrollPane imageRendererScroller;
  private ScrollingPanel scrollingAgent;
  private JLabel imageRenderer;
  private JLabel timeDisplay;
  private JLabel colorDisplay;
  private JComponent filterOptionsPanel;
  private JPanel leftPanel;
  private JPanel filterRightPanel;
  private JPanel resampleRightPanel;
  private JPanel topPanel;
  private JCheckBox holdAspectRatio;
  private JCheckBox fastFilter;
  private JPanel optionsPanel;
  private JComboBox filterFunctionComboBox;
  private JTextField widthField;
  private JTextField heightField;
  
  //Defines the active UI mode
  private boolean inFilterMode = true;

  /** The current original image */
  private Image sourceImage;
  
  /** The image displayed on the screen, either a filtered or resampled version of sourceImage */
  private ResizableImage displayImage;
  
  //Filtering Functions
  private FilterFunction[] functions = new FilterFunction[] {
      new BoxFilter(),
      new TentFilter(),
      new GaussianFilter(),
      new BSplineFilter(),
      new CatmullRomCubicFilter(),
      new MitchellNetravaliCubicFilter(),
      new UnSharpFilter()
      };

  //The color of the pixel under the mouse
  private final Color mouseColor = new Color();

  /** Default constructor builds the UI. */
  public MainFrame() {

    //Define basic window parameters
    super("Resampler");
    setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    Container cp = getContentPane();
    cp.setLayout(new BorderLayout());
    imageRendererContainer = (JComponent) cp;
    setSize(Math.min(DEFAULT_WIDTH, SCREEN_SIZE.width), Math.min(DEFAULT_HEIGHT, SCREEN_SIZE.height));
    setLocation(SCREEN_SIZE.width / 2 - getWidth() / 2, SCREEN_SIZE.height / 2 - getHeight() / 2);
    
    //Create a file chooser to use to open the input files
    fileChooser = new JFileChooser();
    fileChooser.setCurrentDirectory(new File("."));
    addComponentListener(this);

    //Create the UI elements
    setJMenuBar(createJMenuBar());
    cp.add(createCenterPanel(), BorderLayout.CENTER);
    cp.add(createSouthPanel(), BorderLayout.SOUTH);

  }

  /**
   * Main method creates the UI and displays it
   * @param args
   */
  public static final void main(String[] args) {

    new MainFrame().show();
    
  }
  
  ////////////////////////////////////////////////////////////////////////////////////////////////////
  // Methods to create the GUI
  ////////////////////////////////////////////////////////////////////////////////////////////////////
  
  /**
   * Create the menu
   * @return the menu bar object
   */
  private JMenuBar createJMenuBar() {

    JMenuBar toReturn = new JMenuBar();
    JMenu menu;
    JMenuItem item;

    //Add all the menu elements and set their action listener to this
    menu = new JMenu(MENU_NAME);

    item = new JMenuItem(MENU_FILE_OPEN_TEXT);
    item.addActionListener(this);
    menu.add(item);

    item = new JMenuItem(MENU_FILE_SAVE_AS_TEXT);
    item.addActionListener(this);
    menu.add(item);

    menu.addSeparator();

    item = new JMenuItem(MENU_FILE_EXIT_TEXT);
    item.addActionListener(this);
    menu.add(item);

    toReturn.add(menu);

    return toReturn;
  }

  /**
   * Create the scrollable image panel
   * @return the image window object
   */
  private Component createCenterPanel() {

    //Create the scrolling object
    scrollingAgent = new ScrollingPanel(new BorderLayout());

    //Define the attributes of the window and add to UI
    imageRenderer = new JLabel();
    imageRenderer.setVerticalAlignment(SwingConstants.TOP);
    imageRenderer.addMouseMotionListener(this);
    scrollingAgent.add(imageRenderer, BorderLayout.CENTER);
    imageRendererScroller = new JScrollPane(scrollingAgent);

    return imageRendererScroller;
  }

  /**
   * This method is a convenience GUI method. It takes in a GUI component and
   * some text and returns a labelled version of the GUI component.
   * 
   * @param component The component to be labelled.
   * @param text The text to go in the label.
   * @return A new GUI component constructed as described above.
   */
  public static JComponent addLabelLeft(JComponent component, String text) {

    BorderLayout bl = new BorderLayout();
    bl.setHgap(5);
    JPanel toReturn = new JPanel(bl);

    toReturn.add(new JLabel(text, JLabel.TRAILING), BorderLayout.WEST);
    toReturn.add(component, BorderLayout.CENTER);

    return toReturn;
    
  }
  
  /**
   * Create the elements of the bottom panel
   * @return the bottom panel object
   */
  private Component createSouthPanel() {

    //Setup the outer panel
    optionsPanel = new JPanel(new BorderLayout());

    //Define the info bar
    JPanel infoBar = new JPanel(new GridLayout(1,2));
    CompoundBorder labelBorder = new CompoundBorder(BorderFactory.createLoweredBevelBorder(), BorderFactory.createEmptyBorder(2,4,2,2));
    timeDisplay = new JLabel(LBL_FILTER_TIME_TEXT + "    ");
    colorDisplay = new JLabel("Pixel (0, 0) = ");
    timeDisplay.setBorder(labelBorder);
    colorDisplay.setBorder(labelBorder);
    infoBar.add(timeDisplay);
    infoBar.add(colorDisplay);
    
    //Create the top panel
    GridLayout gl = new GridLayout(1,2);
    gl.setHgap(10);
    topPanel = new JPanel(gl);
    topPanel.setBorder(new CompoundBorder(BorderFactory.createEtchedBorder(EtchedBorder.LOWERED), BorderFactory.createEmptyBorder(2,2,2,2)));
    
    //Create the checkbox panel
    JPanel checkBoxPanel = new JPanel(new GridLayout(2,1));
    JCheckBox modeBox = new JCheckBox(CHCK_BX_MODE);
    modeBox.addActionListener(this);
    fastFilter = new JCheckBox("Fast Filter Mode");
    checkBoxPanel.add(modeBox);
    checkBoxPanel.add(fastFilter);
    
    //Create the left panel
    BorderLayout leftLayout = new BorderLayout();
    leftLayout.setVgap(2);
    leftPanel = new JPanel(leftLayout);
    leftPanel.setBorder(BorderFactory.createEmptyBorder(2,2,2,2));
    filterFunctionComboBox = new JComboBox(functions);
    filterFunctionComboBox.addActionListener(this);
    filterFunctionComboBox.setActionCommand(COMBO_BOX_FILTER);
    leftPanel.add(addLabelLeft(filterFunctionComboBox, "Function: "), BorderLayout.NORTH);
    leftPanel.add(checkBoxPanel, BorderLayout.SOUTH);
    setOptionsPanel();
    
    //Setup the button panel
    JButton filterButton = new JButton(BTN_FILTER_TEXT);
    filterButton.addActionListener(this);
    JButton applyButton = new JButton(BTN_APPLY_TEXT);
    applyButton.addActionListener(this);
    JPanel buttonPanel = new JPanel(new FlowLayout());
    buttonPanel.add(filterButton);
    buttonPanel.add(applyButton);
    
    //Size panel
    JComponent sizePanel = new Box(BoxLayout.Y_AXIS);
    widthField = new JTextField("1.0");
    heightField = new JTextField("1.0");
    sizePanel.add(addLabelLeft(widthField, "Width Scale:"));
    sizePanel.add(addLabelLeft(heightField, "Height Scale:"));
    
    //Setup the filter mode panel
    filterRightPanel = new JPanel(new BorderLayout());
    filterRightPanel.add(sizePanel, BorderLayout.NORTH);
    filterRightPanel.add(buttonPanel,BorderLayout.CENTER);
    
    //Setup resample mode panel
    resampleRightPanel = new JPanel(new BorderLayout());
    holdAspectRatio = new JCheckBox("Hold Aspect Ratio");
    resampleRightPanel.add(holdAspectRatio, BorderLayout.NORTH);
    
    //Add elements to the whole bottom panel
    topPanel.add(leftPanel);
    topPanel.add(filterRightPanel);
    optionsPanel.add(topPanel);
    optionsPanel.add(infoBar, BorderLayout.SOUTH);

    return optionsPanel;
  }

  ////////////////////////////////////////////////////////////////////////////////////////////////////
  // Methods that handle GUI actions (and some helpers)
  ////////////////////////////////////////////////////////////////////////////////////////////////////
  
  /**
   * Sets the options panel for the given object
   */
  private void setOptionsPanel() {
    
    FilterFunction currFilter = (FilterFunction) filterFunctionComboBox.getSelectedItem();
    filterOptionsPanel = currFilter.getOptionsPanel();
    if(filterOptionsPanel != null)
      leftPanel.add(filterOptionsPanel, BorderLayout.CENTER);
    
  }
  
  /**
   * Copied the displayed working image into the buffered image behind the image window.
   */
  private void refreshCurrentImage() {

    imageRenderer.setIcon(new ImageIcon(displayImage.asBufferedImage()));
    
  }

  /**
   * Dispatch all actions to the appropriate methods
   * @see ActionListener#actionPerformed(ActionEvent)
   */
  public void actionPerformed(ActionEvent ae) {

    try {
      String cmd = ae.getActionCommand();
      
      if (cmd == null) {
        return;
      }
      else if (cmd.equals(MENU_FILE_OPEN_TEXT)) {
        openAction();
      }
      else if (cmd.equals(MENU_FILE_SAVE_AS_TEXT)) {
        saveAsAction();
      }
      else if (cmd.equals(MENU_FILE_EXIT_TEXT)) {
        exitAction();
      }
      else if (cmd.equals(BTN_FILTER_TEXT)) {
        filterAction();
      }
      else if (cmd.equals(CHCK_BX_MODE)) {
        switchModes();
      }
      else if (cmd.equals(BTN_APPLY_TEXT)) {
        applyAction();
      }
      else if (cmd.equals(COMBO_BOX_FILTER)) {
        filterBoxAction();
      }
    }
    catch (Exception e) {
      JOptionPane.showMessageDialog(this, e.toString());
    }
  }

  /**
   * Handles changes to the filter box
   */
  private void filterBoxAction() {
    
    if(filterOptionsPanel != null)
      leftPanel.remove(filterOptionsPanel);
    setOptionsPanel();
    leftPanel.revalidate();
    repaint();
    
  }
  
  /**
   * Handle the open file action
   */
  private void openAction() {

    //Open the file chooser
    int choice = fileChooser.showOpenDialog(this);

    if (choice != JFileChooser.APPROVE_OPTION) {
      return;
    }

    //Try and read the input file
    BufferedImage inputImage;
    try {
      File imageFile = fileChooser.getSelectedFile();
      inputImage = ImageIO.read(imageFile);
      if (inputImage == null) {
        JOptionPane.showMessageDialog(this, "Inavlid image file!");
        return;
      }
    }
    catch (IOException ioe) {
      JOptionPane.showMessageDialog(this, ioe.toString());
      return;
    }
    
    //Set the image viewer
    sourceImage = Image.createFromBufferedImage(Image.class, inputImage);
    displayImage = new ResizableImage(sourceImage);
    imageRenderer.setIcon(new ImageIcon(displayImage.asBufferedImage()));
  }

  /**
   * Handle the file save action
   */
  private void saveAsAction() {

    //Choose the output file
    int choice = fileChooser.showSaveDialog(this);

    if (choice != JFileChooser.APPROVE_OPTION) {
      return;
    }

    //Attempt to write the output
    File imageFile = fileChooser.getSelectedFile();
    displayImage.write(imageFile.getAbsolutePath());
    
  }

  /**
   * Handle exit
   */
  private void exitAction() {

    System.exit(0);
  }

  /**
   * Handle the filter button press
   */
  private void filterAction() {

    //Do nothing if there is no image loaded
    if(sourceImage == null)
      return;
    
    FilterFunction filter = (FilterFunction) filterFunctionComboBox.getSelectedItem();
    displayImage.resize(sourceImage.getWidth(), sourceImage.getHeight());
    double width = Double.parseDouble(widthField.getText());
    double height = Double.parseDouble(heightField.getText());
    long startTime = System.currentTimeMillis();
    if(fastFilter.isSelected())
      FilterOperations.fastFilter(sourceImage, displayImage, width, height, filter);
    else FilterOperations.filter(sourceImage, displayImage, width, height, filter);
    timeDisplay.setText(LBL_FILTER_TIME_TEXT + ((System.currentTimeMillis() - startTime) / 1000.0) + " seconds    ");
    refreshCurrentImage();
    
  }

  /**
   * Handle apply action, overright the displayed image with the backing simple image
   *
   */
  private void applyAction() {

    sourceImage = new Image(displayImage);
    refreshCurrentImage();
  }

  /**
   * Resizing automatically filters at the give size
   * @see ComponentListener#componentResized(ComponentEvent)
   */
  public void componentResized(ComponentEvent ce) {

    //If we are in filter mode do nothing
    if(inFilterMode)
      return;
    
    //Do nothing if no image is loaded
    if (sourceImage == null)
      return;

    //Calculate the new image size
    int width = imageRenderer.getWidth();
    int height = imageRenderer.getHeight();
    if(holdAspectRatio.isSelected()) {
      
      if(width < height)
        height = (int) (width*(((float) sourceImage.getHeight())/sourceImage.getWidth()));
      else width = (int) (height*(((float) sourceImage.getWidth())/sourceImage.getHeight()));
      
    }
    
    //Apply the resampler and update
    displayImage.resize(width, height);
    long startTime = System.currentTimeMillis();
    if(fastFilter.isSelected())
      FilterOperations.fastResample(sourceImage, displayImage, (FilterFunction) filterFunctionComboBox.getSelectedItem());
    else FilterOperations.resample(sourceImage, displayImage, (FilterFunction) filterFunctionComboBox.getSelectedItem());
    timeDisplay.setText(LBL_FILTER_TIME_TEXT + ((System.currentTimeMillis() - startTime) / 1000.0) + " seconds    ");
    refreshCurrentImage();

  }

  /**
   * Handle the switch between filter and resample modes
   */
  private void switchModes() {

    //Switch components
    if (inFilterMode) {
      
      //From filter to resampling
      topPanel.remove(filterRightPanel);
      topPanel.add(resampleRightPanel);
      imageRendererContainer.remove(imageRendererScroller);
      imageRendererContainer.add(imageRenderer, BorderLayout.CENTER);
      
    }
    else {
      
      //From resampling to filter
      topPanel.remove(resampleRightPanel);
      topPanel.add(filterRightPanel);
      imageRendererContainer.remove(imageRenderer);
      imageRendererContainer.add(imageRendererScroller, BorderLayout.CENTER);
      scrollingAgent.add(imageRenderer, BorderLayout.CENTER);
      
    }
    
    //Rebuild the UI
    inFilterMode = !inFilterMode;
    topPanel.revalidate();
    dispatchEvent(new ComponentEvent(this, ComponentEvent.COMPONENT_RESIZED));
    repaint();
    
  }

  /**
   * Update the pixel color display when the mouse moves
   * @see MouseMotionListener#mouseMoved(MouseEvent)
   */
  public void mouseMoved(MouseEvent e) {

    int x = e.getX();
    int y = e.getY();
    if(x < displayImage.getWidth() && y < displayImage.getHeight()) {

      displayImage.getPixelColor(mouseColor, x, y);
      
    }

    DecimalFormat df = new DecimalFormat("0.0000");
    colorDisplay.setText("Pixel (" + x + ", " + y + ") = " + df.format(mouseColor.r) + ", " + df.format(mouseColor.g) + ", " + df.format(mouseColor.b) + "   ");
    
  }

  ////////////////////////////////////////////////////////////////////////////////////////////////////
  // Methods that must be implemented for the various listener interfaces, but 
  // do nothing so their implementations are empty.
  ////////////////////////////////////////////////////////////////////////////////////////////////////
  
  /**
   * @see ComponentListener#componentHidden(ComponentEvent)
   */
  public void componentHidden(ComponentEvent e) {

  }

  /**
   * @see ComponentListener#componentMoved(ComponentEvent)
   */
  public void componentMoved(ComponentEvent e) {

  }

  /**
   * @see ComponentListener#componentShown(ComponentEvent)
   */
  public void componentShown(ComponentEvent e) {

  }

  /**
   * @see MouseMotionListener#mouseDragged(MouseEvent)
   */
  public void mouseDragged(MouseEvent e) {

  }
}
