/*
 * @(#)AutosizingTextArea.java 5/11/2005
 *
 * Copyright 2002 - 2005 JIDE Software Inc. All rights reserved.
 */
package com.jidesoft.swing;

import javax.swing.*;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.text.Document;
import java.awt.*;

/**
 * An extended version of <code>JTextArea</code> that automatically resizes itself vertically. This component works best
 * when used in a layout that obeys preferred height of its components. For example, you can use a
 * <code>BorderLayout</code> and place <code>AutoResizingTextArea</code> to the north or south side. Similarly, you can
 * use a <code>JideBoxLayout</code> and use FLEXIBLE or FIX as the constraint.
 */
public class AutoResizingTextArea extends JTextArea {

    /**
     * Default maximum height of the text area in rows.
     */
    public static final int DEFAULT_MAX_ROWS = 20;

    /**
     * Default minimum height of the text area in rows.
     */
    public static final int DEFAULT_MIN_ROWS = 1;

    private int _maxRows;
    private int _minRows;
    private ResizingDocumentListener _listener;

    /**
     * Creates a textarea with the default minimum and maximum number of rows.
     */
    public AutoResizingTextArea() {
        this(DEFAULT_MIN_ROWS, DEFAULT_MAX_ROWS);
    }

    /**
     * Creates a textarea with the specified minimum number of rows.
     *
     * @param minRows The minimum number of rows that this textarea can have.
     */
    public AutoResizingTextArea(int minRows) {
        this(minRows, DEFAULT_MAX_ROWS);
    }

    /**
     * Creates a textarea with the specified minimum and maximum number of rows.
     *
     * @param minRows The minimum number of rows that this textarea can have.
     * @param maxRows The maximum number of rows that this textarea can have.
     */
    public AutoResizingTextArea(int minRows, int maxRows) {
        super();
        setMinRows(minRows);
        setMaxRows(maxRows);
        setRows(minRows);
    }

    /**
     * Creates a textarea with the default minimum and maximum row count and the provided initial text. The textarea is
     * sized to fit the provided text.
     *
     * @param text The initial text to display.
     */
    public AutoResizingTextArea(String text) {
        this();
        setText(text);
    }

    /**
     * Create a new <code>AutoResizingTextArea</code> with a height bounded by the provided minimum and maximum row
     * counts and with its width dictated by the provided column count.
     *
     * @param minRows The minimum number of rows that this textarea can have
     * @param maxRows The maximum number of rows that this textarea can have.
     * @param columns The number of columns that this textarea has.
     */
    public AutoResizingTextArea(int minRows, int maxRows, int columns) {
        this(minRows, maxRows);
        setMinRows(minRows);
        setMaxRows(maxRows);
        setColumns(columns);
    }

    /**
     * Create a new <code>AutoResizingTextArea</code> with a height bounded by the provided minimum and maximum row
     * counts and with its width dictated by the provided column count. The textarea is sized to fit the provided text.
     *
     * @param text    The initial text to display in the textarea.
     * @param minRows The minimum number of rows that this textarea can have
     * @param maxRows The maximum number of rows that this textarea can have.
     * @param columns The number of columns that this textarea has.
     *
     * @throws IllegalArgumentException if the rows or columns arguments are negative.
     */
    public AutoResizingTextArea(String text, int minRows, int maxRows, int columns) {
        this(minRows, maxRows, columns);
        setText(text);
    }

    /**
     * Create a new <code>AutoResizingTextArea</code> using a <code>Document</code>. The document will be set to the
     * text area using {@link #setDocument(javax.swing.text.Document)}.
     *
     * @param doc the document.
     */
    public AutoResizingTextArea(Document doc) {
        this();
        setDocument(doc);
    }

    /**
     * Constructs a new <code>AutoResizingTextArea</code> with the specified number of rows and columns, and the given
     * model.  All of the constructors feed through this constructor.
     *
     * @param doc     the model to use, or create a default one if null
     * @param text    the text to be displayed, null if none
     * @param minRows the minimum number of rows >= 0
     * @param maxRows the maximum number of rows >= 0
     * @param columns the number of columns >= 0
     *
     * @throws IllegalArgumentException if the rows or columns arguments are negative.
     */

    public AutoResizingTextArea(Document doc, String text, int minRows, int maxRows, int columns) {
        super(doc, text, minRows, columns);
        setMaxRows(maxRows);
        setMinRows(minRows);
    }

    /**
     * Sets the number of visible rows. The row value will be forced to the boundaries of the range [minRows ...
     * maxRows] if it is outside that range.
     *
     * @param rows The number of rows to show
     */
    @Override
    public void setRows(int rows) {
        int oldRow = super.getRows();
        int newRow = clipRowCount(rows);
        super.setRows(newRow);

        numberOfRowsUpdated(oldRow, newRow);
    }

    /**
     * Called when the number of rows is updated. By default, it will get the parent scroll pane and call revalidate.
     * Subclass can override it to customize the behavior when number of rows is updated.
     *
     * @param oldRow the previous row count.
     * @param newRow the new row count.
     */
    protected void numberOfRowsUpdated(int oldRow, int newRow) {
        // look for a parent ScrollPane and revalidate its container
        // otherwise revalidate the text area's container
        JScrollPane scroll = getParentScrollPane();
        if (scroll != null) {
            Container parent = scroll.getParent();
            if (parent != null && parent instanceof JComponent) {
                JComponent component = (JComponent) parent;
                component.revalidate();
            }
        }
    }

    /**
     * Gets the maximum number of rows that will be displayed. You can set it using {@link #setMaxRows(int)} or passed
     * in using constructor such as {@link #AutoResizingTextArea(int, int)}.
     *
     * @return the maximum number of rows that will be displayed.
     */
    public int getMaxRows() {
        return _maxRows;
    }

    /**
     * Sets the maximum number of rows that will be displayed.
     *
     * @param maxRows The maximum number of rows.
     */
    public void setMaxRows(int maxRows) {
        _maxRows = maxRows;
        setRows(clipRowCount(getRows()));
    }

    /**
     * Gets the minimum number of rows that will be displayed. You can set it using {@link #setMinRows(int)} or passed
     * in using constructor such as {@link #AutoResizingTextArea(int, int)}.
     *
     * @return the minimum number of rows that will be displayed.
     */
    public int getMinRows() {
        return _minRows;
    }

    /**
     * Sets the minimum number of rows that will be displayed
     *
     * @param minRows The minimum number of rows.
     */
    public void setMinRows(int minRows) {
        _minRows = minRows;
        setRows(clipRowCount(getRows()));
    }

    @Override
    public void setDocument(Document doc) {
        Document document = getDocument();
        if (document != null && _listener != null) {
            document.removeDocumentListener(_listener);
        }
        super.setDocument(doc);
        if (doc != null) {
            if (_listener == null) {
                _listener = new ResizingDocumentListener();
            }
            doc.addDocumentListener(_listener);
        }

        updateSize();
    }

    /**
     * Clips the given row count to fall within the range [m_minRows .. m_maxRows] (inclusive).
     *
     * @param rows The row count to clip.
     *
     * @return a row count clipped to the above range
     */
    private int clipRowCount(int rows) {
        int r = Math.min(_maxRows, rows); // clip to upper bounds
        r = Math.max(_minRows, r); // clip to lower bounds
        return r;
    }

    /**
     * Listens to document change events and updates the row count appropriately.
     */
    private class ResizingDocumentListener implements DocumentListener {
        public void insertUpdate(DocumentEvent e) {
            updateSize();
        }

        public void removeUpdate(DocumentEvent e) {
            updateSize();
        }

        public void changedUpdate(DocumentEvent e) {
            updateSize();
        }

    }

    private int getHeight(int rows) {
        Insets insets = getInsets();
        return rows * getRowHeight() + insets.top + insets.bottom;
    }

    private void updateSize() {
        // look for a parent ScrollPane and revalidate its container
        // otherwise revalidate the text area's container
        Container parent = getParentScrollPane();
        if (parent != null && parent.getParent() instanceof JComponent) {
            JComponent component = (JComponent) parent.getParent();
            component.revalidate();
        }
    }

    @Override
    public Dimension getPreferredScrollableViewportSize() {
        Dimension size = getPreferredSize();
        if (getMinRows() != 0 && getMaxRows() != 0) {
            size.height = Math.max(getHeight(getMinRows()), Math.min(getHeight(getMaxRows()), size.height));
        }
        return size;
    }

    /**
     * Gets the parent scroll pane if any.
     *
     * @return the parent scroll pane. If not found, null will be returned.
     */
    private JScrollPane getParentScrollPane() {
        Component parent = getParent();
        if (parent != null && parent instanceof JViewport) {
            return (JScrollPane) parent.getParent();
        }
        return null;
    }
}
