/*
 * 12/21/2004
 *
 * ConfigurableCaret.java - The caret used by RTextArea.
 * Copyright (C) 2004 Robert Futrell
 * robert_futrell at users.sourceforge.net
 * http://fifesoft.com/rsyntaxtextarea
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307 USA.
 */
package org.fife.ui.rtextarea;

import java.awt.*;
import java.awt.event.*;
import java.awt.datatransfer.*;
import java.awt.event.ActionEvent;
import java.io.*;
import javax.swing.*;
import javax.swing.plaf.*;
import javax.swing.text.*;

/**
 * The caret used by {@link RTextArea}. This caret has all of the properties that
 * <code>javax.swing.text.DefaultCaret</code> does, as well as adding the following niceties:
 * 
 * <ul>
 * <li>This caret can paint itself several different ways:
 * <ol>
 * <li>As a vertical line (like <code>DefaultCaret</code>)</li>
 * <li>As a slightly thicker vertical line (like Eclipse)</li>
 * <li>As an underline</li>
 * <li>As a "block caret"</li>
 * <li>As a rectangle around the current character</li>
 * </ol>
 * </li>
 * <li>On Microsoft Windows and other operating systems that do not support system selection (i.e., selecting text, then
 * pasting via the middle mouse button), clicking the middle mouse button will cause a regular paste operation to occur.
 * On systems that support system selection (i.e., all UNIX variants), the middle mouse button will behave normally.</li>
 * </ul>
 * 
 * @author Robert Futrell
 * @version 0.6
 */
public class ConfigurableCaret extends DefaultCaret {

    /**
     * The minimum value of a caret style.
     */
    public static final int MIN_STYLE = 0;

    /**
     * The vertical line style.
     */
    public static final int VERTICAL_LINE_STYLE = 0;

    /**
     * The horizontal line style.
     */
    public static final int UNDERLINE_STYLE = 1;

    /**
     * The block style.
     */
    public static final int BLOCK_STYLE = 2;

    /**
     * The block border style.
     */
    public static final int BLOCK_BORDER_STYLE = 3;

    /**
     * A thicker vertical line (2 pixels instead of 1).
     */
    public static final int THICK_VERTICAL_LINE_STYLE = 4;

    /**
     * The maximum value of a caret style.
     */
    public static final int MAX_STYLE = THICK_VERTICAL_LINE_STYLE;

    /**
     * Action used to select a word on a double click.
     */
    static private transient Action selectWord = null;

    /**
     * Action used to select a line on a triple click.
     */
    static private transient Action selectLine = null;

    /**
     * holds last MouseEvent which caused the word selection
     */
    private transient MouseEvent selectedWordEvent = null;

    /**
     * Used for fastest-possible retrieval of the character at the caret's position in the document.
     */
    private transient Segment seg;

    /**
     * Whether the caret is a vertical line, a horizontal line, or a block.
     */
    private int style;

    /**
     * The selection painter. By default this paints selections with the text area's selection color.
     */
    private ChangeableHighlightPainter selectionPainter;

    /**
     * Whether this is Java 1.4.
     */
    /* TODO: Remove me when 1.4 support is removed. */
    private static final boolean IS_JAVA_1_4 =
            "1.4".equals(System.getProperty("java.specification.version"));

    /**
     * Creates the caret using {@link #VERTICAL_LINE_STYLE}.
     */
    public ConfigurableCaret() {
        this(VERTICAL_LINE_STYLE);
    }

    /**
     * Constructs a new <code>ConfigurableCaret</code>.
     * 
     * @param style
     *            The style to use when painting the caret. If this is invalid, then {@link #VERTICAL_LINE_STYLE} is
     *            used.
     */
    public ConfigurableCaret(int style) {
        seg = new Segment();
        setStyle(style);
        selectionPainter = new ChangeableHighlightPainter();
    }

    /**
     * Adjusts the caret location based on the MouseEvent.
     */
    private void adjustCaret(MouseEvent e) {
        if ((e.getModifiers() & ActionEvent.SHIFT_MASK) != 0 && getDot() != -1)
            moveCaret(e);
        else
            positionCaret(e);
    }

    /**
     * Adjusts the focus, if necessary.
     * 
     * @param inWindow
     *            if true indicates requestFocusInWindow should be used
     */
    private void adjustFocus(boolean inWindow) {
        RTextArea textArea = getTextArea();
        if ((textArea != null) && textArea.isEnabled() &&
                textArea.isRequestFocusEnabled()) {
            if (inWindow)
                textArea.requestFocusInWindow();
            else
                textArea.requestFocus();
        }
    }

    /**
     * Overridden to damage the correct width of the caret, since this caret can be different sizes.
     * 
     * @param r
     *            The current location of the caret.
     */
    protected synchronized void damage(Rectangle r) {
        if (r != null) {
            validateWidth(r); // Check for "0" or "1" caret width
            x = r.x - 1;
            y = r.y;
            width = r.width + 4;
            height = r.height;
            repaint();
        }
    }

    /**
     * Called when the UI is being removed from the interface of a JTextComponent. This is used to unregister any
     * listeners that were attached.
     * 
     * @param c
     *            The text component. If this is not an <code>RTextArea</code>, an <code>Exception</code> will be
     *            thrown.
     * @see Caret#deinstall
     */
    public void deinstall(JTextComponent c) {
        if (!(c instanceof RTextArea))
            throw new IllegalArgumentException(
                    "c must be instance of RTextArea");
        super.deinstall(c);
    }

    /**
     * Gets the text editor component that this caret is bound to.
     * 
     * @return The <code>RTextArea</code>.
     */
    protected RTextArea getTextArea() {
        return (RTextArea) getComponent();
    }

    /**
     * Returns whether this caret's selection uses rounded edges.
     * 
     * @return Whether this caret's edges are rounded.
     * @see #setRoundedSelectionEdges
     */
    public boolean getRoundedSelectionEdges() {
        return ((ChangeableHighlightPainter) getSelectionPainter()).
                getRoundedEdges();
    }

    /**
     * Gets the painter for the Highlighter. This is overridden to return our custom selection painter.
     * 
     * @return The painter.
     */
    protected Highlighter.HighlightPainter getSelectionPainter() {
        return selectionPainter;
    }

    /**
     * Gets the current style of this caret.
     * 
     * @return The caret's style.
     * @see #setStyle(int)
     */
    public int getStyle() {
        return style;
    }

    /**
     * Installs this caret on a text component.
     * 
     * @param c
     *            The text component. If this is not an {@link RTextArea}, an <code>Exception</code> will be thrown.
     * @see Caret#install
     */
    public void install(JTextComponent c) {
        if (!(c instanceof RTextArea))
            throw new IllegalArgumentException(
                    "c must be instance of RTextArea");
        super.install(c);
    }

    /**
     * Called when the mouse is clicked. If the click was generated from button1, a double click selects a word, and a
     * triple click the current line.
     * 
     * @param e
     *            the mouse event
     * @see MouseListener#mouseClicked
     */
    public void mouseClicked(MouseEvent e) {

        if (!e.isConsumed()) {

            RTextArea textArea = getTextArea();
            int nclicks = e.getClickCount();

            if (SwingUtilities.isLeftMouseButton(e)) {
                if (nclicks <= 2) {
                    // Only handle these clicks for 1.4. In 1.5 the word
                    // selection is (also?) handled in mousePressed, and if we
                    // handle it here, our word selection gets doubled-up.
                    if (IS_JAVA_1_4) {
                        if (nclicks == 1) {
                            selectedWordEvent = null;
                        }
                        else { // 2
                            selectWord(e);
                            selectedWordEvent = null;
                        }
                    }
                }
                else {
                    nclicks %= 2; // Alternate selecting word/line.
                    switch (nclicks) {
                    case 0:
                        selectWord(e);
                        selectedWordEvent = null;
                        break;
                    case 1:
                        Action a = null;
                        ActionMap map = textArea.getActionMap();
                        if (map != null)
                            a = map.get(RTextAreaEditorKit.selectLineAction);
                        if (a == null) {
                            if (selectLine == null) {
                                selectLine = new RTextAreaEditorKit.SelectLineAction();
                            }
                            a = selectLine;
                        }
                        a.actionPerformed(new ActionEvent(textArea,
                                ActionEvent.ACTION_PERFORMED,
                                null, e.getWhen(), e.getModifiers()));
                    }
                }
            }

            else if (SwingUtilities.isMiddleMouseButton(e)) {
                if (nclicks == 1 && textArea.isEditable() && textArea.isEnabled()) {
                    // Paste the system selection, if it exists (e.g., on UNIX
                    // platforms, the user can select text, the middle-mouse click
                    // to paste it; this doesn't work on Windows). If the system
                    // doesn't support system selection, just do a normal paste.
                    JTextComponent c = (JTextComponent) e.getSource();
                    if (c != null) {
                        try {
                            Toolkit tk = c.getToolkit();
                            Clipboard buffer = tk.getSystemSelection();
                            // If the system supports system selections, (e.g. UNIX),
                            // try to do it.
                            if (buffer != null) {
                                adjustCaret(e);
                                TransferHandler th = c.getTransferHandler();
                                if (th != null) {
                                    Transferable trans = buffer.getContents(null);
                                    if (trans != null)
                                        th.importData(c, trans);
                                }
                                adjustFocus(true);
                            }
                            // If the system doesn't support system selections
                            // (e.g. Windows), just do a normal paste.
                            else {
                                textArea.paste();
                            }
                        } catch (HeadlessException he) {
                            // do nothing... there is no system clipboard
                        }
                    } // if (c!=null)
                } // if (nclicks == 1 && component.isEditable() && component.isEnabled())
            } // else if (SwingUtilities.isMiddleMouseButton(e))

        } // if (!c.isConsumed())

    }

    /**
     * Paints the cursor.
     * 
     * @param g
     *            The graphics context in which to paint.
     */
    public void paint(Graphics g) {

        // If the cursor is currently visible...
        if (isVisible()) {

            try {

                RTextArea textArea = getTextArea();
                g.setColor(textArea.getCaretColor());
                TextUI mapper = textArea.getUI();
                Rectangle r = mapper.modelToView(textArea, getDot());

                // "Correct" the value of rect.width (takes into
                // account caret being at EOL (and thus rect.width==1),
                // etc.
                // We do this even for LINE_STYLE because
                // if they change from that caret to block/underline,
                // the first time they do so width==1, so it will take
                // one caret flash to paint correctly (wider). If we
                // do this every time, then it's painted correctly the
                // first blink.
                validateWidth(r);

                // Need to subtract 2 from height, otherwise
                // the caret will expand too far vertically.
                r.height -= 2;

                switch (style) {

                // Draw a big rectangle, and xor the foreground color.
                case BLOCK_STYLE:
                    g.setXORMode(Color.WHITE);
                    // fills x==r.x to x==(r.x+(r.width)-1), inclusive.
                    g.fillRect(r.x, r.y, r.width, r.height);
                    break;

                // Draw a rectangular border.
                case BLOCK_BORDER_STYLE:
                    // fills x==r.x to x==(r.x+(r.width-1)), inclusive.
                    g.drawRect(r.x, r.y, r.width - 1, r.height);
                    break;

                // Draw an "underline" below the current position.
                case UNDERLINE_STYLE:
                    g.setXORMode(Color.WHITE);
                    int y = r.y + r.height;
                    g.drawLine(r.x, y, r.x + r.width - 1, y);
                    break;

                // Draw a vertical line.
                default:
                case VERTICAL_LINE_STYLE:
                    g.drawLine(r.x, r.y, r.x, r.y + r.height);
                    break;

                // A thicker vertical line.
                case THICK_VERTICAL_LINE_STYLE:
                    g.drawLine(r.x, r.y, r.x, r.y + r.height);
                    r.x++;
                    g.drawLine(r.x, r.y, r.x, r.y + r.height);
                    break;

                } // End of switch (style).

            } catch (BadLocationException ble) {
                ble.printStackTrace();
            }

        } // End of if (isVisible()).

    }

    /**
     * Deserializes a caret. This is overridden to read the caret's style.
     * 
     * @param s
     *            The stream to read from.
     * @throws ClassNotFoundException
     * @throws IOException
     */
    private void readObject(ObjectInputStream s)
            throws ClassNotFoundException, IOException {
        s.defaultReadObject();
        setStyle(s.readInt());
        seg = new Segment();
    }

    /**
     * Selects word based on the MouseEvent
     */
    private void selectWord(MouseEvent e) {
        if (selectedWordEvent != null
                && selectedWordEvent.getX() == e.getX()
                && selectedWordEvent.getY() == e.getY()) {
            // We've already the done selection for this.
            return;
        }
        Action a = null;
        RTextArea textArea = getTextArea();
        ActionMap map = textArea.getActionMap();
        if (map != null) {
            a = map.get(RTextAreaEditorKit.selectWordAction);
        }
        if (a == null) {
            if (selectWord == null) {
                selectWord = new RTextAreaEditorKit.SelectWordAction();
            }
            a = selectWord;
        }
        a.actionPerformed(new ActionEvent(textArea,
                ActionEvent.ACTION_PERFORMED,
                null, e.getWhen(), e.getModifiers()));
        selectedWordEvent = e;
    }

    /**
     * Sets whether this caret's selection should have rounded edges.
     * 
     * @param rounded
     *            Whether it should have rounded edges.
     * @see #getRoundedSelectionEdges()
     */
    public void setRoundedSelectionEdges(boolean rounded) {
        ((ChangeableHighlightPainter) getSelectionPainter()).
                setRoundedEdges(rounded);
    }

    /**
     * Overridden to always render the selection, even when the text component loses focus.
     * 
     * @param visible
     *            Whether the selection should be visible. This parameter is ignored.
     */
    public void setSelectionVisible(boolean visible) {
        super.setSelectionVisible(true);
    }

    /**
     * Sets the style used when painting the caret.
     * 
     * @param style
     *            The style to use. If this isn't one of <code>VERTICAL_LINE_STYLE</code>, <code>UNDERLINE_STYLE</code>,
     *            or <code>BLOCK_STYLE</code>, then <code>VERTICAL_LINE_STYLE</code> is used.
     * @see #getStyle()
     */
    public void setStyle(int style) {
        if (style < MIN_STYLE || style > MAX_STYLE)
            style = VERTICAL_LINE_STYLE;
        this.style = style;
        repaint();
    }

    /**
     * Helper function used by the block and underline carets to ensure the width of the painted caret is valid. This is
     * done for the following reasons:
     * 
     * <ul>
     * <li>The <code>View</code> classes in the javax.swing.text package always return a width of "1" when
     * <code>modelToView</code> is called. We'll be needing the actual width.</li>
     * <li>Even in smart views, such as <code>RSyntaxTextArea</code>'s <code>SyntaxView</code> and
     * <code>WrappedSyntaxView</code> that return the width of the current character, if the caret is at the end of a
     * line for example, the width returned from <code>modelToView</code> will be 0 (as the width of unprintable
     * characters such as '\n' is calculated as 0). In this case, we'll use a default width value.</li>
     * </ul>
     * 
     * @param rect
     *            The rectangle returned by the current <code>View</code>'s <code>modelToView</code> method for the
     *            caret position.
     */
    private void validateWidth(Rectangle rect) {

        // If the width value > 1, we assume the View is
        // a "smart" view that returned the proper width.
        // So only worry about this stuff if width <= 1.
        if (rect != null && rect.width <= 1) {

            // The width is either 1 (most likely, we're using a "dumb" view
            // like those in javax.swing.text) or 0 (most likely, we're using
            // a "smart" view like org.fife.ui.rsyntaxtextarea.SyntaxView,
            // we're at the end of a line, and the width of '\n' is being
            // computed as 0).

            try {

                // Try to get a width for the character at the caret
                // position. We use the text area's font instead of g's
                // because g's may vary in an RSyntaxTextArea.
                RTextArea textArea = getTextArea();
                textArea.getDocument().getText(getDot(), 1, seg);
                Font font = textArea.getFont();
                FontMetrics fm = textArea.getFontMetrics(font);
                rect.width = fm.charWidth(seg.array[seg.offset]);

                // This width being returned 0 likely means that it is an
                // unprintable character (which is almost 100% to be a
                // newline char, i.e., we're at the end of a line). So,
                // just use the width of a space.
                if (rect.width == 0) {
                    rect.width = fm.charWidth(' ');
                }

            } catch (BadLocationException ble) {
                // This shouldn't ever happen.
                ble.printStackTrace();
                rect.width = 8;
            }

        } // End of if (rect!=null && rect.width<=1).

    }

    /**
     * Serializes this caret. This is overridden to write the style of the caret.
     * 
     * @param s
     *            The stream to write to.
     * @throws IOException
     *             If an IO error occurs.
     */
    private void writeObject(ObjectOutputStream s) throws IOException {
        s.defaultWriteObject();
        s.writeInt(getStyle());
    }

}