/*
 * @(#)ConsoleTextPane.java    2.5.0   24 April 2006
 *
 * Copyright 2006
 * 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.*;
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.</P>
 *
 * <P>This class is used internally by the JPT
 * and should not need to be used
 * outside of the JPT console package.</P>
 *
 * <P>There was a change between Java 1.4 and
 * Java 5.0 that broke the key handler code
 * in processComponentKeyEvent.  After much
 * effort, this method and related code has
 * been rewritten so that it works with both
 * Java 1.4 and Java 5.0 libraries.</P>
 * 
 * @author  Jeff Raab
 * @author  Richard Rasala
 * @version 2.5.0
 * @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
     * plus the transparent renderer.
     */
    private Color[] color = new Color[]
         { Color.black, Color.black, Color.black };
    
    /** Console window containing this text pane. */
    private ConsoleWindow window = null;
    
    /** The current stream. */
    private int currentstream = OUT;
    
    /** 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) {
        int eventID = evt.getID();
        int keycode = evt.getKeyCode();
        char keychar = evt.getKeyChar();
        
        /**
         * Debug code to track key strokes.
        
        String info = "ID " + eventID
            + "    Key Code " + keycode
            + "    Key Char " + (int) keychar;
        
        System.out.println(info);
        
         */
        
        // pass on
        // modifier keys, cursor keys,
        // and VK_PRINTSCREEN
        // using a simple return with no consume
        switch(keycode) {
            case KeyEvent.VK_ALT:
            case KeyEvent.VK_ALT_GRAPH:
            case KeyEvent.VK_CAPS_LOCK:
            case KeyEvent.VK_CONTROL:
            case KeyEvent.VK_META:
            case KeyEvent.VK_NUM_LOCK:
            case KeyEvent.VK_SCROLL_LOCK:
            case KeyEvent.VK_SHIFT:
            case KeyEvent.VK_MODECHANGE:
            case KeyEvent.VK_ALPHANUMERIC:
            case KeyEvent.VK_ROMAN_CHARACTERS:
            case KeyEvent.VK_KANA:
            case KeyEvent.VK_KANA_LOCK:
            case KeyEvent.VK_KANJI:
            case KeyEvent.VK_HIRAGANA:
            case KeyEvent.VK_KATAKANA:
            case KeyEvent.VK_JAPANESE_ROMAN:
            case KeyEvent.VK_JAPANESE_HIRAGANA:
            case KeyEvent.VK_JAPANESE_KATAKANA:
            case KeyEvent.VK_LEFT:
            case KeyEvent.VK_RIGHT:
            case KeyEvent.VK_UP:
            case KeyEvent.VK_DOWN:
            case KeyEvent.VK_KP_LEFT:
            case KeyEvent.VK_KP_RIGHT:
            case KeyEvent.VK_KP_UP:
            case KeyEvent.VK_KP_DOWN:
            case KeyEvent.VK_PRINTSCREEN:
            
                return;
            
            default:
                break;
        }
        
        // pass on
        // CTRL-C ( 3) for "copy"
        // CTRL-V (22) for "paste"
        // CTRL-X (24) for "cut"
        // using a simple return with no consume
        if ((keychar == (char)3)
            || (keychar == (char)22)
            || (keychar == (char)24))
        {
            return;
        }
        
        // handle the various types of key events
        // that require special behavior
        switch (eventID) {
            
            case KeyEvent.KEY_PRESSED:
                // consume enter at key press 
                if (keycode == KeyEvent.VK_ENTER)
                {
                    evt.consume();
                    return;
                }
                
                // consume backspace at key press if invalid delete
                // this is present for Java 5.0 compatibility
                if (keycode == KeyEvent.VK_BACK_SPACE)
                {
                    if ((getCaretPosition() < start) ||
                        (getSelectionStart() < start))
                    {
                        evt.consume();
                        return;
                    }
                    
                    if (getCaretPosition() == start)
                        if (getSelectionStart() == getSelectionEnd())
                        {
                            evt.consume();
                            return;
                        }
                }
                
                break;
        
            case KeyEvent.KEY_RELEASED:
                // consume enter and backspace keys at key release
                if ((keycode == KeyEvent.VK_ENTER) ||
                    (keycode == KeyEvent.VK_BACK_SPACE))
                {
                    evt.consume();
                    return;
                }
                
                break;
                
            // handle keys at key typed time
            // in between press and release
            default:
                // consume
                // if the caret or the selection
                // is in the immutable part of the text
                if ((getCaretPosition()  < start) ||
                    (getSelectionStart() < start))
                {
                    evt.consume();
                    return;
                }
                
                // handle typed backspace if invalid delete
                // this is present for Java 1.4 compatibility
                if (keychar == (char)8)
                {
                    if (getCaretPosition() == start)
                        if (getSelectionStart() == getSelectionEnd())
                        {
                            evt.consume();
                            return;
                        }
                }
                
                // handle and consume typed newline (10) or return (13)
                // fireConsoleInputPerformed to signal end of line
                if ((keychar == (char)10) || (keychar == (char)13))
                {
                    try
                    {
                        doc.insertString
                            (doc.getLength(), endl, getStyleFor(IN));

                        fireConsoleInputPerformed
                            (doc.getText(start, doc.getLength() - start));
                    }
                    catch (BadLocationException ex) {
                        // never thrown, as we're using doc.getLength()
                        // for the position
                    }
                    finally
                    {
                        evt.consume();
                        return;
                    }
                }
                
                break;
        }
        
        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 ((getCaretPosition() >= start)
            && (getSelectionStart() >= start))
        {
            setCharacterAttributes(getStyleFor(IN), true);
            super.replaceSelection(text);
        }
    }
    
    
    /**
     * 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 ((getCaretPosition() >= start)
            && (getSelectionStart() >= start))
        {
            super.cut();
        }
    }
    
    
    /**
     * 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 ((getCaretPosition() >= start)
            && (getSelectionStart() >= start))
        {
            setCharacterAttributes(getStyleFor(IN), true);
            super.paste();
        }
    }
    
    
    /** 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 which must
     *        equal OUT, ERR, or IN
     */
    public void setStream(int stream) {
        if ((stream < OUT) || (stream > IN))
            return;
        
        currentstream = stream;
        
        // prepare for input if appropriate
        if (stream == IN) {
            start = doc.getLength();
            setCaretPosition(start);
            setCharacterAttributes(getStyleFor(stream), true);
            setCaretColor(null);
            
            setEditable(true);
            requestFocus();
        }
        
        // otherwise allow only model changes
        else {
            setCaretColor(Colors.transparent);
            
            setEditable(false);
            transferFocus();
        }
    }
    
    
    /**
     * Sets the current input color for this pane
     * to the given color.
     *
     * Sets to black if the given color is
     * <code>null</code>.
     * 
     * @param c the desired input color
     * @see #getInputColor()
     */
    public void setInputColor(Color c) {
        if (c == null)
            c = Color.black;
        
        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.
     *
     * Sets to black if the given color is
     * <code>null</code>.
     * 
     * @param c the desired output color
     * @see #getOutputColor()
     */
    public void setOutputColor(Color c) {
        if (c == null)
            c = Color.black;
        
        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.
     *
     * Sets to black if the given color is
     * <code>null</code>.
     * 
     * @param c the desired error color
     * @see #getErrorColor()
     */
    public void setErrorColor(Color c) {
        if (c == null)
            c = Color.black;
        
        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) {
        if (window != null)
            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.
     *
     * This method is no longer used in this class
     * but is retained for possible future use.
     */
    private void beep() {
        (new DefaultEditorKit.BeepAction()).actionPerformed(
            new ActionEvent(
                this,
                ActionEvent.ACTION_PERFORMED,
                DefaultEditorKit.beepAction));    
    }
    
}
