/* class JTextArea
 *
 * Copyright (C) 2001  R M Pitman
 *
 * 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 charvax.swing;

import charva.awt.*;
import charva.awt.event.KeyEvent;
import charva.awt.event.MouseEvent;
import charva.awt.event.ScrollEvent;
import charva.awt.event.ScrollListener;

import java.util.Enumeration;
import java.util.Vector;

/**
 * JTextArea is an (optionally editable) multi-line area that displays
 * plain text.
 * The JTextArea class implements the Scrollable interface, which enables
 * it to be placed inside a charvax.swing.JScrollPane. In fact, in the
 * CHARVA framework it should ALWAYS be used inside a JScrollPane, otherwise
 * it will be unusable (its size depends on the text it contains).<p>
 * <p/>
 * Note that, unlike the javax.swing.JTextArea, pressing the TAB key while
 * the keyboard focus is in the JTextArea will not cause a tab to be
 * inserted; instead, it will move the keyboard input focus to the next
 * focus-traversable component (if there is one). This is because (in
 * javax.swing) the user can user can use the mouse to move the keyboard
 * input focus away from the JTextArea, whereas CHARVA has no mouse support.
 */

/**
 * Support for
    public void setBounds( Rectangle bounds )
    public void setBounds( int top_, int left_, int bottom_, int right_)
    public void setBounds(Point topleft_, Dimension size_)
 */
public class JTextArea
        extends charvax.swing.text.JTextComponent
        implements charva.awt.Scrollable {

    /**
     * The default constructor creates an empty text area with 10 rows
     * and 10 columns.
     */
    public JTextArea() {
        this("");
    }

    /**
     * Construct a text area with 10 rows and 10 columns, and containing
     * the specified text.
     */
    public JTextArea(String text_) {
        this(text_, 10, 10);
    }

    /**
     * Construct a text area wth the specified number of rows and columns,
     * and containing the specified text.
     */
    public JTextArea(String text_, int rows_, int columns_) {
        setDocument(text_);
        _rows = rows_;
        _preferredRows = rows_;
        _columns = columns_;
        _preferredColumns = columns_;
        setCaretPosition(0);
    }

    public void setBounds( Rectangle bounds ) {
        super.setBounds(bounds);
        setColumns(bounds.getRight() - bounds.getLeft() + 1 );
        setRows(bounds.getBottom() - bounds.getTop() + 1 );
    }

    public void setBounds( int top_, int left_, int bottom_, int right_) {
        super.setBounds(top_,left_,bottom_,right_);
        setColumns( right_ -  left_ + 1);
        setRows( bottom_ - top_ + 1);
  
    }

    public void setBounds(Point topleft_, Dimension size_) {
        super.setBounds(topleft_, size_);
        setColumns( size_.width );
        setRows( size_.height );
    }


    /**
     * Sets the number of columns in this JTextArea.
     */
    public void setColumns(int columns_) {
        _columns = columns_;
        _preferredColumns = columns_;
    }

    /**
     * Returns the number of columns in this JTextArea.
     */
    public int getColumns() {
        return _preferredColumns;
    }

    /**
     * Sets the number of rows in this JTextArea.
     */
    public void setRows(int rows_) {
        _rows = rows_;
        _preferredRows = rows_;
    }

    /**
     * Returns the number of rows in this JTextArea.
     */
    public int getRows() {
        return _preferredRows;
    }

    /**
     * Returns the size of this component.
     */
    public Dimension getSize() {
        return new Dimension(_columns, _rows);
    }

    public int getWidth() {
        return _columns;
    }

    public int getHeight() {
        return _rows;
    }

    /**
     * Appends the specified text to the end of the document.
     */
    public synchronized void append(String text_) {
        super._document.append(text_);
        _caretPosition = super._document.length();

        refresh();
    }

    /**
     * Inserts the specified text at the specified position (ie at the
     * specified offset from the start of the document)..
     */
    public synchronized void insert(String text_, int pos_) {
        super._document.insert(pos_, text_);
        _caretPosition = pos_ + text_.length();

        refresh();
    }

    /**
     * Replaces the text from the specified start position to end position
     * with the specified text.
     */
    public synchronized void replaceRange(String text_, int start_, int end_) {
        super._document.replace(start_, end_, text_);
        _caretPosition = start_ + text_.length();

        refresh();
    }

    /**
     * Sets the position of the text insertion caret for this JTextArea.
     */
    public void setCaretPosition(int caret_) {
        super.setCaretPosition(caret_);

        refresh();
    }

    /**
     * Returns the number of lines of text displayed in the JTextArea.
     */
    public int getLineCount() {
        return offsetCalc(LINE_COUNT, 0);
    }

    /**
     * Returns the offset of the first character in the specified line
     * of text.
     */
    public int getLineStartOffset(int line_) {
        return offsetCalc(LINE_START_OFFSET, line_);
    }

    /**
     * Returns the offset of the last character in the specified line.
     */
    public int getLineEndOffset(int line_) {
        return offsetCalc(LINE_END_OFFSET, line_);
    }

    /**
     * Translates an offset (relative to the start of the document)
     * to a line number.
     */
    public int getLineOfOffset(int offset_) {
        return offsetCalc(LINE_OF_OFFSET, offset_);
    }

    /**
     * Sets the line-wrapping policy of the JTextArea. If set to true,
     * lines will be wrapped if they are too long to fit within the
     * allocated width. If set to false, the lines will always be
     * unwrapped. The default value of this property is false.
     */
    public void setLineWrap(boolean wrap_) {
        _lineWrap = wrap_;
        _rows = _preferredRows;
        _columns = _preferredColumns;
    }

    /**
     * Returns the line-wrapping policy of the JTextArea. If set to true,
     * lines will be wrapped if they are too long to fit within the
     * allocated width. If set to false, the lines will always be
     * unwrapped.
     */
    public boolean getLineWrap() {
        return _lineWrap;
    }

    /**
     * Sets the line-wrapping style to be used if getLineWrap() is true.
     * If true, lines will be wrapped at word boundaries (whitespace) if
     * they are too long to fit in the allocated number of columns. If
     * false, lines will be wrapped at character boundaries. The default
     * value of this property is false.
     */
    public void setWrapStyleWord(boolean wrapWord_) {
        _wrapStyleWord = wrapWord_;
    }

    /**
     * Returns the line-wrapping style to be used if getLineWrap() is true.
     * If true, lines will be wrapped at word boundaries (whitespace) if
     * they are too long to fit in the allocated number of columns. If
     * false, lines will be wrapped at character boundaries.
     */
    public boolean getWrapStyleWord() {
        return _wrapStyleWord;
    }

    /**
     * Called by the LayoutManager.
     */
    public Dimension minimumSize() {
        return getSize();
    }

    /**
     * Process KeyEvents that have been generated by this JTextArea.
     */
    public void processKeyEvent(KeyEvent ke_) {

        /* First call all KeyListener objects that may have been registered
         * for this component.
         */
        super.processKeyEvent(ke_);

        /* Check if any of the KeyListeners consumed the KeyEvent.
         */
        if (ke_.isConsumed())
            return;

        int caret = getCaretPosition();
        int line = getLineOfOffset(caret);
        int key = ke_.getKeyCode();
        if (key == '\t') {
            getParent().nextFocus();
            return;
        } else if (key == KeyEvent.VK_BACK_TAB) {
            getParent().previousFocus();
            return;
        } else if (key == KeyEvent.VK_LEFT && caret > 0) {
            setCaretPosition(caret - 1);
        } else if (key == KeyEvent.VK_RIGHT &&
                caret < getDocument().length()) {
            setCaretPosition(caret + 1);
        } else if (key == KeyEvent.VK_HOME) {
            int lineStart = getLineStartOffset(line);
            setCaretPosition(lineStart);
        } else if (key == KeyEvent.VK_END) {
            int lineEnd = getLineEndOffset(line);
            setCaretPosition(lineEnd);
        } else if ((key == KeyEvent.VK_PAGE_UP ||
                key == KeyEvent.VK_PAGE_DOWN) &&
                (getParent() instanceof JViewport)) {

            JViewport viewport = (JViewport) getParent();
            int vertical_offset = -1 * viewport.getViewPosition().y;
            int viewport_height = viewport.getSize().height;
            if (key == KeyEvent.VK_PAGE_UP) {
                if (line > vertical_offset)
                    line = vertical_offset;
                else
                    line = vertical_offset - viewport_height;

                line = (line < 0) ? 0 : line;
            } else {
                if (line < vertical_offset + viewport_height - 1)
                    line = vertical_offset + viewport_height - 1;
                else
                    line = vertical_offset + (2 * viewport_height) - 1;

                line = (line > getLineCount() - 1) ?
                        (getLineCount() - 1) :
                        line;
            }
            setCaretPosition(getLineStartOffset(line));
        } else if (key == KeyEvent.VK_UP && line > 0) {
            int column = caret - getLineStartOffset(line);
            int prevlineStart = getLineStartOffset(line - 1);
            int prevlineEnd = getLineEndOffset(line - 1);
            if (column > prevlineEnd - prevlineStart) {
                column = prevlineEnd - prevlineStart;
            }
            setCaretPosition(prevlineStart + column);
        } else if (key == KeyEvent.VK_DOWN &&
                line < getLineCount() - 1) {
            int column = caret - getLineStartOffset(line);
            int nextlineStart = getLineStartOffset(line + 1);
            int nextlineEnd = getLineEndOffset(line + 1);
            if (column > nextlineEnd - nextlineStart) {
                column = nextlineEnd - nextlineStart;
            }
            setCaretPosition(nextlineStart + column);
        } else if (super.isEditable() == false) {
            Toolkit.getDefaultToolkit().beep();
        } else if (key >= ' ' && key <= 0177) {
            char[] arry = {(char) key};
            insert(new String(arry), caret);
        } else if (key == KeyEvent.VK_ENTER) {
            char[] arry = {'\n'};
            insert(new String(arry), caret);
        } else if (key == KeyEvent.VK_BACK_SPACE && caret > 0) {
            replaceRange("", caret - 1, caret);
        } else if (key == KeyEvent.VK_DELETE &&
                caret < getDocument().length() - 1) {
            replaceRange("", caret, caret + 1);
        }

        /* If this JTextArea is contained in a JViewport, let the JViewport
         * do the drawing, after setting the clip rectangle.
         */
        if ((getParent() instanceof JViewport) == false) {
            draw();
            requestFocus();
            super.requestSync();
        }
    }

    /**
     * Process a MouseEvent that was generated by clicking the mouse
     * somewhere inside this JTextArea.
     * Clicking the mouse inside the JTextArea moves the caret position
     * to where the mouse was clicked.
     */
    public void processMouseEvent(MouseEvent e_) {
        super.processMouseEvent(e_);

        if (e_.getButton() == MouseEvent.BUTTON1 &&
                e_.getModifiers() == MouseEvent.MOUSE_CLICKED &&
                this.isFocusTraversable()) {

            /* Get the absolute origin of this component.
             */
            Point origin = getLocationOnScreen();
            Insets insets = super.getInsets();
            origin.translate(insets.left, insets.top);

            int line = e_.getY() - origin.y;
            if (line > getLineCount() - 1)
                return;

            int column = e_.getX() - origin.x;
            int lineStart = getLineStartOffset(line);
            int lineEnd = getLineEndOffset(line);
            if (column > lineEnd - lineStart) {
                column = lineEnd - lineStart;
            }
            setCaretPosition(lineStart + column);
            repaint();
        }
    }

    /**
     * Implements the abstract method in charva.awt.Component.
     */
    public void draw() {
        Point tempCaret = null;
        Point caret = _caret;

        /* Get the absolute origin of this component.
         */
        Point origin = getLocationOnScreen();

        Toolkit term = Toolkit.getDefaultToolkit();
        int colorpair = getCursesColor();

        /* Start by blanking out the text area
         */
        term.blankBox(origin, getSize(), colorpair);
        term.setCursor(origin);

        /* Scan through the entire document, drawing each character in it.
         */
        ScrollEvent scrollevent = null;
        int row = 0, col = 0;
        outerloop:
            for (int i = 0; i < super._document.length(); i++) {

                /* At some point during the scan of the document, the
                 * caret position should match the scan index, unless the caret
                 * position is after the last character of the document.
                 */
                if (_caretPosition == i) {
                    tempCaret = new Point(col, row);

                    /* If the caret has changed, generate a ScrollEvent. Note
                     * that this method may be called multiple times during the
                     * scan; however, we must post only the last event generated.
                     */
                    if (tempCaret.equals(caret) == false) {
                        scrollevent = generateScrollEvent(tempCaret,
                                new Point(col, row));
                        caret = tempCaret;
                    }
                }

                char chr = super._document.charAt(i);
                if (col < _columns) {
                    if (chr == '\n') {
                        col = 0;
                        row++;
                        if (row >= _rows)
                            _rows++;
                        term.setCursor(origin.addOffset(col, row));
                    } else {
                        term.addChar(chr, 0, colorpair);
                        col++;
                    }
                } else {	// We have reached the right-hand column.
                    if (_lineWrap == false) {
                        if (chr == '\n') {
                            col = 0;
                            row++;
                            if (row >= _rows)
                                _rows++;
                            term.setCursor(origin.addOffset(col, row));
                        } else {
                            term.addChar(chr, 0, colorpair);
                            col++;
                            _columns++;
                        }
                    } else {	// line-wrap is true
                        if (_wrapStyleWord == false) {
                            col = 0;
                            row++;
                            if (row >= _rows)
                                _rows++;
                            term.setCursor(origin.addOffset(col, row));
                            if (chr != '\n')    // thanks to Chris Rogers for this
                                term.addChar(chr, 0, colorpair);
                        } else {
                            /* We must back-track until we get to whitespace, so
                             * that we can move the word to the next line.
                             */
                            int j;
                            for (j = 0; j < _columns; j++) {
                                char tmpchr = super._document.charAt(i - j);
                                if (tmpchr == ' ' || tmpchr == '\t') {
                                    deleteEOL(term, col - j, row, colorpair);
                                    col = 0;
                                    row++;
                                    if (row >= _rows)
                                        _rows++;
                                    i -= j;
                                    term.setCursor(origin.addOffset(col, row));
                                    break;
                                }
                            }
                            if (j == _columns) {	// the word was too long
                                if (chr == ' ' || chr == '\n' || chr == '\t') {
                                    col = 0;
                                    row++;
                                    if (row >= _rows)
                                        _rows++;
                                    term.setCursor(origin.addOffset(col, row));
                                }
                            }
                        }
                    }	// end if line-wrap is true
                }		// end if we have reached the right-hand column
            }		// end FOR loop.

        /* Check for the case where the caret position is after the last
         * character of the document.
         */
        if (_caretPosition == super._document.length()) {
            tempCaret = new Point(col, row);

            /* If the caret has changed, generate a ScrollEvent
             */
            if (tempCaret.equals(caret) == false) {
                scrollevent = generateScrollEvent(tempCaret,
                        new Point(col, row));
            }
            caret = tempCaret;
        }

        /* Post a ScrollEvent, if one was generated; but only if the
         * caret has really changed.  We have to be careful to avoid an
         * endless loop, where a ScrollEvent triggers a draw(), which
         * triggers an unnecessary ScrollEvent and so on.
         */
        if ((_caret.equals(caret) == false) && scrollevent != null) {
            term.getSystemEventQueue().postEvent(scrollevent);
            _caret = caret;
        }
    }

    public void requestFocus() {
        /* Generate the FOCUS_GAINED event.
         */
        super.requestFocus();

        /* Get the absolute origin of this component.
         */
        Point origin = getLocationOnScreen();
        Toolkit.getDefaultToolkit().setCursor(origin.addOffset(_caret));
    }

    /**
     * Register a ScrollListener object for this JTextArea.
     */
    public void addScrollListener(ScrollListener sl_) {
        if (_scrollListeners == null)
            _scrollListeners = new Vector();
        _scrollListeners.add(sl_);
    }

    /**
     * Remove a ScrollListener object that is registered for this JTextArea.
     */
    public void removeScrollListener(ScrollListener sl_) {
        if (_scrollListeners == null)
            return;
        _scrollListeners.remove(sl_);
    }

    /**
     * Process scroll events generated by this JTextArea.
     */
    public void processScrollEvent(ScrollEvent e_) {
        if (_scrollListeners != null) {
            for (Enumeration e = _scrollListeners.elements();
                 e.hasMoreElements();) {

                ScrollListener sl = (ScrollListener) e.nextElement();
                sl.scroll(e_);
            }
        }
    }

    /**
     * Returns the preferred size of the viewport for this JTextArea
     * when it is in a JScrollPane (this method implements the
     * Scrollable interface). The size is determined by the number of
     * rows and columns set for this JTextArea (either in the constructor
     * or in the setColumns() and setRows() methods).
     */
    public Dimension getPreferredScrollableViewportSize() {
        return new Dimension(_preferredColumns, _preferredRows);
    }

    public void debug(int level_) {
        for (int i = 0; i < level_; i++)
            System.err.print("    ");
        System.err.println("JTextArea origin=" + _origin +
                " size=" + getSize());
    }

    /**
     * A private helper method to delete from a specified position
     * until the end of the line.
     */
    private void deleteEOL(Toolkit term_, int col_, int row_, int colorpair_) {
        Point origin = getLocationOnScreen();
        term_.setCursor(origin.addOffset(col_, row_));
        for (int i = col_; i < _columns; i++)
            term_.addChar(' ', 0, colorpair_);
    }

    /**
     * This helper method converts offset to line number and
     * vice versa.
     */
    private int offsetCalc(int mode_, int value_) {
        int lineOfOffset = 0;
        int row = 0;

        if (mode_ == LINE_START_OFFSET && value_ == 0)
            return 0;

        for (int col = 0, i = 0;
             i < super._document.length();
             i++) {

            if (mode_ == LINE_OF_OFFSET && value_ == i) {
                lineOfOffset = row;
            }

            char chr = super._document.charAt(i);
            if (col < _columns) {
                if (chr == '\n') {
                    col = 0;
                    row++;
                } else {
                    col++;
                }
            } else {	// We have reached the right-hand column.
                if (_lineWrap == false) {
                    if (chr == '\n') {
                        col = 0;
                        row++;
                    }
                } else {	// line-wrap is true
                    if (_wrapStyleWord == false) {
                        col = 0;
                        row++;
                    } else {
                        /* We must back-track until we get to whitespace, so
                         * that we can move the word to the next line.
                         */
                        int j;
                        for (j = 0; j < _columns; j++) {
                            char tmpchr = super._document.charAt(i - j);
                            if (tmpchr == ' ' || tmpchr == '\t') {
                                col = 0;
                                row++;
                                i -= j;
                                break;
                            }
                        }
                        if (j == _columns) {	// the word was too long
                            if (chr == ' ' || chr == '\n' || chr == '\t') {
                                col = 0;
                                row++;
                            }
                        }
                    }
                }	// end if line-wrap is true
            }		// end if we have reached the right-hand column

            if (mode_ == LINE_START_OFFSET && col == 0 && row == value_) {
                return i + 1;
            } else if (mode_ == LINE_END_OFFSET && col == 0 && row == value_ + 1) {
                return i;
            }

        }		// end FOR loop.

        if (mode_ == LINE_OF_OFFSET) {
            if (value_ == super._document.length()) {
                return row;
            } else
                return lineOfOffset;
        } else if (mode_ == LINE_COUNT) {
            return row + 1;
        } else if (mode_ == LINE_END_OFFSET && row == value_) {
            return super._document.length();
        } else {
            throw new IndexOutOfBoundsException("Invalid offset or line number: mode=" + mode_ +
                    " value=" + value_ + " row=" + row + " doc=\"" + _document + "\"");
        }
    }

    /* Private helper method used to redraw the component if its state
     * has changed.
     */
    private void refresh() {
        /* If this JTextArea is contained in a JViewport, the PaintEvent
         * that we post must request a redraw of the JViewport, so that
         * the JViewport can set the clipping rectangle before calling the
         * draw() method of this JTextArea.
         */
        Component todraw;
        if (getParent() instanceof JViewport)
            todraw = getParent();
        else
            todraw = this;

        /* If this component is already displayed, generate a PaintEvent
         * and post it onto the queue.
         */
        todraw.repaint();
    }

    /**
     * Private method, called whenever the caret changes.
     */
    private ScrollEvent generateScrollEvent(Point tempCaret_, Point col_row_) {
        int direction;

        /* Determine the direction of scrolling
         */
        if (tempCaret_.y > _caret.y) {
            if (tempCaret_.x > _caret.x)
                direction = ScrollEvent.UP_LEFT;
            else if (tempCaret_.x < _caret.x)
                direction = ScrollEvent.UP_RIGHT;
            else
                direction = ScrollEvent.UP;
        } else if (tempCaret_.y < _caret.y) {
            if (tempCaret_.x > _caret.x)
                direction = ScrollEvent.DOWN_LEFT;
            else if (tempCaret_.x < _caret.x)
                direction = ScrollEvent.DOWN_RIGHT;
            else
                direction = ScrollEvent.DOWN;
        } else {
            if (tempCaret_.x > _caret.x)
                direction = ScrollEvent.LEFT;
            else
                direction = ScrollEvent.RIGHT;
        }
        return new ScrollEvent(this, direction, col_row_);

    }

    //====================================================================
    // INSTANCE VARIABLES

    private int _rows;
    private int _columns;

    private int _preferredRows;
    private int _preferredColumns;

    /**
     * The caret is updated only when the component is drawn.
     */
    private Point _caret = new Point(0, 0);

    private boolean _lineWrap;
    private boolean _wrapStyleWord = false;

    /**
     * A list of ScrollListeners registered for this JTextArea.
     */
    private Vector _scrollListeners = null;

    private static final int LINE_COUNT = 1;
    private static final int LINE_START_OFFSET = 2;
    private static final int LINE_END_OFFSET = 3;
    private static final int LINE_OF_OFFSET = 4;
}
