/*
 * @(#)ConsoleTextPane.java    2.3.3  4 January 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.quick.*;
import edu.neu.ccs.util.*;

import java.awt.*;
import java.awt.event.*;
import java.io.Serializable;
import javax.swing.*;
import javax.swing.event.*;
import javax.swing.text.*;

/**
 * <P>A text pane for styled input and output
 * of console text.
 *
 * 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
 * @version 2.3.3
 * @since   1.0
 * @see ConsoleWindow
 * @see ConsoleGateway
 * @see ConsoleInputListener
 */
final class ConsoleTextPane 
    extends JTextPane 
    implements 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;
                                
    /** Platform dependent line separator sequence. */
    private transient String endl = null;

    /** 
     * Caret position indicating the beginning 
     * of the current input text. 
     */
    private int start = 0;
    
    /** Style context for styles used in the text pane. */
    private StyleContext context = null;

    /** Styled document used as the data model for the text pane. */
    private DefaultStyledDocument doc = null;
    
    /** Base style from which all other styles are created. */
    private Style base = null;
    
    /** Rendering colors used for the three streams. */
    private Color[] color = new Color[]
         { Color.black, Color.black, Color.black };
    
    /** Console window containing this text pane. */
    private ConsoleWindow window = null;
    
    /** The minimum console font size. */
    protected static final int minFontSize = 10;
    
    /** The maximum console font size. */
    protected static final int maxFontSize = 72;
    
    /** The current font size as set by the setFontSize method. */
    private int fontSize = minFontSize;
    
    
    //////////////////
    // Constructors //
    //////////////////
    
    /**
     * Constructs a console text pane ready for modification
     * through the deserialization process.
     */
    private ConsoleTextPane() {
        endl = SystemUtilities.getLineSeparator();
    }
    
    
    /**
     * Constructs a console text pane 
     * contained by the given console window.
     *
     * @param w the console window to contain the new pane
     */
    public ConsoleTextPane(ConsoleWindow w) {
        this();
        
        // store the container for the pane
        window = w;
        
        // build the data model for the text pane
        context = new StyleContext();
        doc = new DefaultStyledDocument(context);
        setStyledDocument(doc);
        
        // decide the monospaced font family
        String familyName = ConsoleGateway.getMonospacedFontFamilyName();
        
        // build the base style for the document
        base = context.getStyle(StyleContext.DEFAULT_STYLE);
        StyleConstants.setFontFamily(base, familyName);
        
        // find a good font size to use as the default
        int size = new JLabel("0").getFont().getSize();
        
        // force the size initially to be even
        // although it might be set to be odd later
        if ((size % 2) != 0)
            size += 1;
        
        // add 4 for good measure to make the size
        // larger than the size of a label
        size += 4;
        
        StyleConstants.setFontSize(base, size);

        // set base style for the document
        doc.setCharacterAttributes(0, doc.getLength(), base, false);
        
        // begin with text pane in output mode
        setStream(OUT);
    }
    
    
    ////////////////
    // Public API //
    ////////////////
    
    /**
     * Handles a key event trapped by the text pane.
     *
     * @param evt the trapped key event
     */
    public void processComponentKeyEvent(KeyEvent evt) {
        
        // handle the various types of key events
        switch (evt.getID()) {
            
            // disable enter and backspace keys at key press 
            // and release time
            case KeyEvent.KEY_PRESSED:
            case KeyEvent.KEY_RELEASED:
                if ((evt.getKeyCode() == KeyEvent.VK_ENTER) ||
                    (evt.getKeyCode() == KeyEvent.VK_BACK_SPACE)) {
                        evt.consume();
                        return;
                }
                break;

            // handle keys at key typed time
            // (in between press and release)
            default:

                // beep if caret is in an improper position for input
                // or if the selection goes into 
                // the immutable portion of the text
                if ((getCaretPosition() < start) ||
                    (getSelectionStart() < start)) {
                        beep();
                        evt.consume();
                        return;
                }
                
                // handle typed returns
                if ((evt.getKeyChar() == (char)10) ||
                    (evt.getKeyChar() == (char)13))
                {
                    try {
                        doc.insertString(
                        	doc.getLength(), endl, getStyleFor(IN));

                        fireConsoleInputPerformed(
                        	doc.getText(
                        		start, doc.getLength() - start));

                        evt.consume();
                    } catch (BadLocationException ex) {}
                    finally  {
                        return;
                    }
                }
                
                // handle typed backspaces
                if (evt.getKeyChar() == (char)8) {
                    if (getCaretPosition() == start) {
                        beep();
                        evt.consume();
                        return;
                    }
                }
        }
        
        // send events up the hierarchy if they haven't already been handled
        super.processComponentKeyEvent(evt);
        setCharacterAttributes(getStyleFor(IN), true);
    }
    
    
    /**
     * Appends the given text produced by the given output stream
     * to the end of the text pane content.
     *
     * @param text the text to append to the pane
     * @param stream the identifier of the stream 
     *		that produced the text
     * @see #replaceSelection(String)
     */
    public void append(String text, int stream) {
        setStream(stream);
        
        // bug fix: 20 July 2000
        start = doc.getLength();
        setCaretPosition(start);

        try {
            doc.insertString(doc.getLength(), text, getStyleFor(stream));
        } catch (BadLocationException ex) {
            // never thrown, as we're using doc.getLength()
            // for the position
        }
        
        start = doc.getLength();
        setCaretPosition(start);
    }
    
    
    /**
     * Replaces the currently selected text with the given text, 
     * but only if the selected text is completely enclosed 
     * within the bounds of the current input text.
     *
     * @param text the text with which to replace the selected text
     * @see #append(String, int)
     */
    public void replaceSelection(String text) {
        if (getSelectionStart() >= start)
            super.replaceSelection(text);
        else 
            beep();
    }
    
    
    /**
     * Cuts the currently selected text out of the pane 
     * and stores it on the system clipboard, 
     * but only if the selected text is completely enclosed 
     * within the bounds of the current input text.
     *
     * @see #paste()
     */
    public void cut() {
        if (getSelectionStart() >= start)
            super.cut();
        else 
            beep();
    }
    
    
    /**
     * Pastes the clipboard contents into the pane 
     * at the current caret position, 
     * but only if the caret position (or selection)
     * is completely enclosed 
     * within the bounds of the current input text.
     *
     * @see #cut()
     */
    public void paste() {
        if (getSelectionStart() >= start)
            super.paste();
        else
            beep();
    }
    
    /** Returns the font family name of the console font. */
    public final String getFontFamilyName() {
        return StyleConstants.getFontFamily(base);
    }
    
    
    /**
     * <p>Returns the actual font size last set by the method
     * <code>setFontSize</code> after any adjustments.</p>
     */
    public final int getFontSize() {
        return fontSize;
    }
    
    
    /**
     * 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 entire pane 
     * to the given point size modulo adjustments.
     * The font size is forced between a minimum
     * of 10 and a maximum of 72.</p>
     *
     * @param size the desired font size
     */
    public final void setFontSize(int size) {
        // force size within bounds
        if (size < minFontSize)
            size = minFontSize;
        else
        if (size > maxFontSize)
            size = maxFontSize;
        
        // create the style with the new font size and modify
        // the base style to use the new size
        Style font = context.addStyle(
            null, context.getStyle(StyleContext.DEFAULT_STYLE));
        StyleConstants.setFontSize(font, size);
        StyleConstants.setFontSize(base, size);

        // size the current text accordingly
        doc.setCharacterAttributes(0, doc.getLength(), font, false);
        
        // save the font size
        fontSize = size;
    }
    
    
    /**
     * Sets the currently active stream for the console.  
     * Used to differentiate the state when input is being accepted
     * from the state when output is being appended.
     *
     * @param stream the active stream for the console
     */
    public void setStream(int stream) {
        
        // prepare for input if appropriate
        if (stream == IN) {
            start = doc.getLength();
            setCaretPosition(start);
            setCharacterAttributes(getStyleFor(stream), true);
            
            setEditable(true);
            requestFocus();
        }
        
        // otherwise allow only model changes
        else {
            setEditable(false);
            transferFocus();
        }
    }
    
    
    /**
     * Sets the current input color for this pane
     * to the given color.
     *
     * @param c the desired input color
     * @see #getInputColor()
     */
    public void setInputColor(Color c) {
        color[IN] = c;
    }
    
    
    /**
     * Returns the current input color for this pane.
     *
     * @see #setInputColor(Color)
     */
    public Color getInputColor() {
        return color[IN];
    }
    
    
    /**
     * Sets the current output color for this pane
     * to the given color.
     *
     * @param c the desired output color
     * @see #getOutputColor()
     */
    public void setOutputColor(Color c) {
        color[OUT] = c;
    }
    
    
    /**
     * Returns the current output color for this pane.
     *
     * @see #setOutputColor(Color)
     */
    public Color getOutputColor() {
        return color[OUT];
    }
    
    
    /**
     * Sets the current error color for this pane
     * to the given color.
     *
     * @param c the desired error color
     * @see #getErrorColor()
     */
    public void setErrorColor(Color c) {
        color[ERR] = c;
    }
    
    
    /**
     * Returns the current error color for this pane.
     *
     * @see #setErrorColor(Color)
     */
    public Color getErrorColor() {
        return color[ERR];
    }
    
    
    /////////////////////
    // 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) {
        window.consoleInputPerformed(text);
    }
    
    
    /**
     * Returns the appropriate rendering style 
     * for the given stream.
     *
     * @param stream the stream to be rendered
     */
    private Style getStyleFor(int stream) {
        Style s = context.addStyle(null, base);
        StyleConstants.setForeground(s, color[stream]);
        return s;
    }
    
    
    /**
     * Sounds the default system beep.
     *
     * @see #replaceSelection(String)
     * @see #cut()
     * @see #paste()
     */
    private void beep() {
        (new DefaultEditorKit.BeepAction()).actionPerformed(
            new ActionEvent(
                this,
                ActionEvent.ACTION_PERFORMED,
                DefaultEditorKit.beepAction));    
    }
    
}
