/*
 * @(#)ConsoleWindow.java    2.4.0   26 May 2005
 *
 * Copyright 2005
 * College of Computer and Information Science
 * Northeastern University
 * Boston, MA  02115
 *
 * The Java Power Tools software may be used for educational
 * purposes as long as this copyright notice is retained intact
 * at the top of all source files.
 *
 * To discuss possible commercial use of this software, 
 * contact Richard Rasala at Northeastern University, 
 * College of Computer and Information Science,
 * 617-373-2462 or rasala@ccs.neu.edu.
 *
 * The Java Power Tools software has been designed and built
 * in collaboration with Viera Proulx and Jeff Raab.
 *
 * Should this software be modified, the words "Modified from 
 * Original" must be included as a comment below this notice.
 *
 * All publication rights are retained.  This software or its 
 * documentation may not be published in any media either
 * in whole or in part without explicit permission.
 *
 * This software was created with support from Northeastern 
 * University and from NSF grant DUE-9950829.
 */

package edu.neu.ccs.console;

import edu.neu.ccs.gui.*;
import edu.neu.ccs.util.*;

import java.awt.*;
import java.awt.event.*;
import java.io.*;
import java.util.*;
import javax.swing.*;
import javax.swing.text.*;

/**
 * <P>A floating window containing a console text pane,
 * with a menu providing access to functionality
 * for the "activated" console object.
 *
 * This class is used internally by the JPT
 * and should not need to be used
 * outside of the JPT console package.</P>
 *
 * @author  Jeff Raab
 * @author  Richard Rasala
 * @author  Jason Jay Rodrigues
 * @version 2.4.0
 * @since   1.0
 * @see ConsoleTextPane
 * @see ConsoleGateway
 */
final class ConsoleWindow 
    extends JPTFrame 
    implements ConsoleAware, JPTConstants, Serializable 
{
    /** Constant index for the output stream. */
    public static final int OUT = 0;
                                
    /** Constant index for the error stream. */
    public static final int ERR = 1;
                                
    /** Constant index for the input stream. */
    public static final int IN  = 2;
    
    /** The minimum font size for the font menu. */
    private static int minFontSize = ConsoleTextPane.minFontSize;
    
    /** The maximum font size for the font menu. */
    private static int maxFontSize = ConsoleTextPane.maxFontSize;
    
    /** The amount to deduct from the screen width  when sizing. */
    private static int widthDeduction  = 200;
    
    /** The amount to deduct from the screen height when sizing. */
    private static int heightDeduction = 150;
    
    /** Table of available actions for the GUI. */
    private Hashtable actions = null;
    
    /** The "save transcript" action. */
    private Action save = null;
    
    /** The "start transcript record" action. */
    private Action transcript = null;
    
    /** The output stream for an open transcript record. */
    private transient FileOutputStream scriptStream = null;
    
    /** The file object for an open transcript record. */
    private File scriptFile = null;
    
    /** Scroll pane housing the console pane. */
    private JScrollPane scroll = null;
    
    /** Pane used for rendering console input and output. */
    private ConsoleTextPane tablet = null;
  
	/**
	 * Mutex that is used to prevent input mode changes 
	 * while fireConsoleInputPerformed is activated.
	 */
  	private Object inputmode_mutex = new Object();
    
    
    //////////////////
    // Constructors //
    //////////////////

    /**
     * Constructs a console window.
     */
    public ConsoleWindow() {
        super("Text console");

        setDefaultCloseOperation(DO_NOTHING_ON_CLOSE);
        addWindowListener(new WindowAdapter() {
            public void windowClosing(WindowEvent evt) {
                close();
            }
        });

        // initialize menu with text actions for use with the text pane
        tablet = new ConsoleTextPane(this);
        actions = createActionsTable();
        setJMenuBar(createMenuBar());
        save.setEnabled(false);
        
        // set up scrolling pane
        scroll = new JScrollPane(tablet);
        scroll.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
        scroll.setVerticalScrollBarPolicy  (JScrollPane.VERTICAL_SCROLLBAR_ALWAYS);
        
        // set up visualization
        setContentPane(scroll);
        
        // set size
        Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
        
        int width  = screenSize.width  - widthDeduction;
        int height = screenSize.height - heightDeduction;
        
        setSize(width, height);
        
        // set resizable
        setResizable(true);
        
        
    }
    
    
    //////////////////
    // Serializable //
    //////////////////
    
    /**
     * Extends the default deserialization process 
     * to restore transient data members upon instantiation.
     *
     * @param in the deserialization input stream
     */
    private void readObject(ObjectInputStream in) 
        throws IOException, ClassNotFoundException 
    {
        in.defaultReadObject();
        
        // restore transient fields if possible
        try {
            scriptStream = new FileOutputStream(scriptFile);
        } catch (Exception ex) {
            scriptFile = null;
            scriptStream = null;
            transcript.putValue(Action.NAME, "Start transcript");
        }
    }
    
    
    ////////////////
    // Public API //
    ////////////////
    
    /**
     * Handles input gathered from the console visualization.
     *
     * @param text the input text gathered from the stream
     */
    public void consoleInputPerformed(String text) {
        save.setEnabled(true);
        fireConsoleInputPerformed(text);
    }
    
    
    /**
     * Handles output for the text console.
     *
     * @param text the output text
     * @param stream the output stream being "written"
     * @see ConsoleInputListener
     */
    public void consoleOutputPerformed(String text, int stream) {
        save.setEnabled(true);

        tablet.append(text, stream);
        if (scriptStream != null) {
            try {
                scriptStream.write(text.getBytes());
            } catch (IOException ex) {
                JOptionPane.showMessageDialog(
                    null,
                    "There was an error writing to the transcript file.",
                    "Warning!",
                    JOptionPane.WARNING_MESSAGE);
            }
        }
    }
    
    
    /**
     * <p>Sets whether or not the console is in input mode
     * to the given value.</p> 
     * 
     * <p>If the console is set to input mode,
     * the console window is set to normal in case it had
     * been iconified
     * and the output streams are temporarily blocked.</p>
     *
     * @param input whether or not the console is in input mode
     */
    public void setInputMode(boolean input) {
		synchronized(inputmode_mutex) {
			if (input) {
				tablet.setStream(ConsoleTextPane.IN);
				normal();
		    }
			else
				tablet.setStream(ConsoleTextPane.OUT);
		}
    }
    
    
    /**
     * Sets the input color for the console
     * to the given color.
     *
     * @param c the desired input color
     * @see #getInputColor()
     */
    public void setInputColor(Color c) {
        tablet.setInputColor(c);
    }
    
    
    /**
     * Returns the input color for the console.
     *
     * @see #setInputColor(Color)
     */
    public Color getInputColor() {
        return tablet.getInputColor();
    }
    
    
    /**
     * Sets the output color for the console
     * to the given color.
     *
     * @param c the desired output color
     * @see #getOutputColor()
     */
    public void setOutputColor(Color c) {
        tablet.setOutputColor(c);
    }
    
    
    /**
     * Returns the output color for the console.
     *
     * @see #setOutputColor(Color)
     */
    public Color getOutputColor() {
        return tablet.getOutputColor();
    }
    
    
    /**
     * Sets the error color for the console
     * to the given color.
     *
     * @param c the desired error color
     * @see #getErrorColor()
     */
    public void setErrorColor(Color c) {
        tablet.setErrorColor(c);
    }
    
    
    /**
     * Returns the error color for the console.
     * @see #setErrorColor(Color)
     */
    public Color getErrorColor() {
        return tablet.getErrorColor();
    }
    
    
    /** Returns the font family name of the console font. */
    public final String getFontFamilyName() {
        return tablet.getFontFamilyName();
    }
    
    
    /**
     * <p>Returns the actual font size last set by the method
     * <code>setFontSize</code> after any adjustments.</p>
     */
    public final int getFontSize() {
        return tablet.getFontSize();
    }
    
    
    /**
     * Get the minimum font size that may be set for the
     * console window.
     */
    public static final int getMinimumFontSize() {
        return minFontSize;
    }
    
    
    /**
     * Get the maximum font size that may be set for the
     * console window.
     */
    public static final int getMaximumFontSize() {
        return maxFontSize;
    }
    
    
    /**
     * <p>Sets the font size for the console to the given size.</p>
     *
     * <p>Point sizes smaller than 10 are set to 10 points.</p>
     *
     * <p>Point sizes larger  than 72 are set to 72 points.</p>
     *
     * <p>Keep in mind that very large font sizes will lead to
     * very few characters per line in the console window.</p> 
     *
     * @param size the desired font size
     */
    public final void setFontSize(int size) {
        tablet.setFontSize(size);
    }
    
    
    /**
     * Performs the housekeeping necessary 
     * to close the window, 
     * then disposes of the window resources.
     */
    public void close() {
        
        // check for need to save transcript
        if ((save.isEnabled()) &&
            (JOptionPane.showConfirmDialog(
 	               this,
    	            "Do you want to save a transcript " + 
    	            	"of the console contents?",
        	        "Save contents",
            	    JOptionPane.YES_NO_OPTION) == 
            	JOptionPane.YES_OPTION)) 
        {
        	save();
        }
        
        console.setActivated(false);
        
        // dispose of resources
        setVisible(false);
        dispose();
        if (getJPTFrameCount() == 0)
            System.exit(0);
    }
    
    
    /////////////////////
    // Package methods //
    /////////////////////

    /**
     * Returns the text pane contained by this window.
     */
    ConsoleTextPane getTextPane() {
        return tablet;
    }
    
    
    /////////////////////
    // Private methods //
    /////////////////////

    /**
     * Notifies registered listeners 
     * that an input <CODE>String</CODE> 
     * was gathered by console input.
     *
     * @param text the gathered input <CODE>String</CODE>
     * @see ConsoleInputListener
     */
    private void fireConsoleInputPerformed(String text) {
		synchronized(inputmode_mutex) {
			console.consoleInputPerformed(text);
		}
    }
    
    
    /**
     * Creates and returns the table of available GUI actions.
     */
    private Hashtable createActionsTable() {
        Hashtable table = new Hashtable();

        Action[] a = tablet.getActions();
        for (int i = 0; i < a.length; i++)
            table.put((String)a[i].getValue(Action.NAME), a[i]);
        
        return table;
    }
    
    
    /**
     * Returns the action with the given name 
     * retrieved from the table of available actions.
     *
     * @param name the name of the action to retrieve
     */
    private Action getAction(String name) {
        return (Action)actions.get(name);
    }
    
    
    /**
     * Creates a menubar containing the available GUI actions.
     */
    private JMenuBar createMenuBar() {
        JMenuBar menubar = new JMenuBar();
        
        menubar.add(createFileMenu());
        menubar.add(createEditMenu());
        menubar.add(createFontMenu());
        
        return menubar;    
    }
    
    
    /** Returns the file menu. */
    private JMenu createFileMenu() {
        JMenu file = new JMenu("File");
        
        file.add(save = new SimpleAction("Save contents") {
            public void perform() {
                save();
            }
        });
        
        file.add(transcript = new SimpleAction("Start transcript") {
            public void perform() {
                transcript();
            }
        });
        
        file.addSeparator();
        
        file.add(new SimpleAction("Close window") {
            public void perform() {
                close();
            }
        });
        
        return file;
    }
    
    
    /** Returns the edit menu. */
    private JMenu createEditMenu() {
        JMenu edit = new JMenu("Edit");
        
        edit.add(new SimpleAction("Cut") {
            public void perform() {
                getTextPane().cut();
            }
        });
        
        edit.add(new AbstractAction("Copy") {
            public void actionPerformed(ActionEvent evt) {
                Action a = getAction(DefaultEditorKit.copyAction);
                if (a != null)
                    a.actionPerformed(evt);
            }
        });
        
        edit.add(new SimpleAction("Paste") {
            public void perform() {
                getTextPane().paste();
            }
        });
        
        edit.addSeparator();
        
        edit.add(new AbstractAction("Select all") {
            public void actionPerformed(ActionEvent evt) {
                Action a = getAction(DefaultEditorKit.selectAllAction);
                if (a != null)
                    a.actionPerformed(evt);
            }
        });
        
        return edit;
    }
    
    /** Returns the font menu. */
    private JMenu createFontMenu() {
        JMenu font = new JMenu("Font size");
        
        for (int size = 10; size <= 36; size += 2) {
            font.add(setFontAction(size));
        }
        
        for (int size = 48; size <= 72; size += 12) {
            font.add(setFontAction(size));
        }
        
        return font;
    }
    
    
    /**
     * Returns a menu item action to set the console
     * to the given size.
     *
     * @param size the desired font size for the font menu
     */
    private SimpleAction setFontAction(final int size) {
        return new SimpleAction(size + " point") {
            public void perform() {
                setFontSize(size);
            }
        };
    }
    
    
    /**
     * Saves a transcript of the console IO session.
     *
     * @see #createMenuBar()
     */
    private void save() {
    
    	// create a filechooser object
        JFileChooser fc = new JFileChooser();
        fc.setDialogTitle("Save contents");
        fc.setFileFilter(new javax.swing.filechooser.FileFilter() {
            public boolean accept(File f) {
                return true;
            }
            
            public String getDescription() {
                return "All files (*.*)";
            }
        });
        
        // as long as the user chooses to save the file
        while (fc.showSaveDialog(this) == JFileChooser.APPROVE_OPTION) {

			// attempt to write the file
			try {
                try {
                    FileUtilities.writeFile(fc.getSelectedFile(), tablet.getText(), false);
                    save.setEnabled(false);
                    break;
                    
                // if the file exists
                } catch (FileExistsException ex) {
                
    				// first ask the user for overwrite permission
                    int result = JOptionPane.showConfirmDialog(
                        null, 
                        "A file with that name already exists.  " + 
                        	"Do you want to overwrite it?",
                        "Warning!",
                        JOptionPane.YES_NO_CANCEL_OPTION,
                        JOptionPane.WARNING_MESSAGE);
                        
                    // if it is OK to overwrite the existing file
                    if (result == JOptionPane.YES_OPTION) {
                        FileUtilities.writeFile(fc.getSelectedFile(), tablet.getText(), true);
      	                save.setEnabled(false);
           	            break;
                    }
                    
                    // otherwise skip saving the file
                    else if (result == JOptionPane.CANCEL_OPTION) {
                        break;
                    }
                }
            }

			// display an error message
			// if the file cannot be written                    
            catch (IOException ex) {
                JOptionPane.showMessageDialog(
                    this,
                    "The file could not be saved.",
                    "Error!",
                    JOptionPane.ERROR_MESSAGE);
                break;
            }
        }
    }
    
    
    /**
     * Opens a transcript record file
     * that is updated as IO operations are performed.
     *
     * @see #createMenuBar()
     */
    private void transcript() {
    
    	// if the user chooses to open a new transcript
        if (scriptStream == null) {
        
        	// build a filechooser object
            JFileChooser fc = new JFileChooser();
            fc.setDialogTitle("Start transcript");
            fc.setFileFilter(new javax.swing.filechooser.FileFilter() {
                public boolean accept(File f) {
                    return true;
                }
                
                public String getDescription() {
                    return "All files (*.*)";
                }
            });
            
            // as long as the user chooses a path and file to use
            while (fc.showDialog(this, "Select") == 
            	   JFileChooser.APPROVE_OPTION) 
            {
            
            	// if the file already does not exist
            	// or the user shows that it is OK to overwrite
                File file = fc.getSelectedFile();
                if (!file.exists() ||
                    (JOptionPane.showConfirmDialog(
                    	    null, 
                        	"A file with that name already exists.  " + 
                    	    	"Do you want to overwrite it?",
                        	"Warning!",
                     	    JOptionPane.YES_NO_CANCEL_OPTION,
                       	 	JOptionPane.WARNING_MESSAGE) == 
                    	JOptionPane.YES_OPTION)) 
                {
                	// attempt to open the file
	                try {
    	                scriptFile = fc.getSelectedFile();

                        scriptStream = new FileOutputStream(
                        	scriptFile);

                        transcript.putValue(
                        	Action.NAME, 
                        	"Stop transcript");

                        break;
                        
                    // display an error message
                    // if the file can't be opened
                    } catch (IOException ex) {
	                    JOptionPane.showMessageDialog(
	                        null,
                            "The transcript could not be opened.",
                            "Error!",
                            JOptionPane.ERROR_MESSAGE);
                        break;
                    }
                }
            }
        }
    
		// if the user chooses to close the open transcript
        else {
        
        	// confirm that the user wants to close it
            if (JOptionPane.showConfirmDialog(
		                null,
        		        "Do you want to close the transcript file?",
                		"Stop transcript",
                		JOptionPane.YES_NO_OPTION) == 
                	JOptionPane.YES_OPTION) 
            {

				// close the file
                try {
                    scriptStream.close();
                } 
                
                // ignore any thrown exceptions
                catch (IOException ex) {}
                
                // enable action to open a new transcript
                finally {
                    scriptFile = null;
                    scriptStream = null;

                    transcript.putValue(
                    	Action.NAME, 
                    	"Start transcript");
                }
            }
        }
    }
    
}

