/*
 * Copyright (c) 2006-2010 Chris Smith, Shane Mc Cormack, Gregory Holmes
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

package com.dmdirc.addons.ui_swing.textpane;

import com.dmdirc.ui.interfaces.Window;

import java.awt.Point;
import java.awt.Toolkit;
import java.awt.datatransfer.StringSelection;
import java.awt.event.AdjustmentEvent;
import java.awt.event.AdjustmentListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionAdapter;
import java.awt.event.MouseMotionListener;
import java.awt.event.MouseWheelEvent;
import java.awt.event.MouseWheelListener;

import javax.swing.BoundedRangeModel;
import javax.swing.DefaultBoundedRangeModel;
import javax.swing.JComponent;
import javax.swing.JScrollBar;

import net.miginfocom.swing.MigLayout;

/**
 * Styled, scrollable text pane.
 */
public final class TextPane extends JComponent implements AdjustmentListener,
        MouseWheelListener, IRCDocumentListener {

    /**
     * A version number for this class. It should be changed whenever the class
     * structure is changed (or anything else that would prevent serialized
     * objects being unserialized with the new class).
     */
    private static final long serialVersionUID = 5;
    /** Scrollbar model. */
    private final BoundedRangeModel scrollModel;
    /** Canvas object, used to draw text. */
    private final TextPaneCanvas canvas;
    /** IRCDocument. */
    private final IRCDocument document;
    /** Parent Frame. */
    private final Window frame;

    /** 
     * Creates a new instance of TextPane. 
     *
     * @param frame Parent Frame
     */
    public TextPane(final Window frame) {
        super();
        this.frame = frame;
        
        setUI(new TextPaneUI());
        document = new IRCDocument(frame.getContainer());
        frame.getConfigManager().addChangeListener("ui", "textPaneFontName",
                document);
        frame.getConfigManager().addChangeListener("ui", "textPaneFontSize",
                document);

        setLayout(new MigLayout("fill"));
        canvas = new TextPaneCanvas(this, document);
        add(canvas, "dock center");
        scrollModel = new DefaultBoundedRangeModel();
        scrollModel.setMaximum(document.getNumLines());
        scrollModel.setExtent(0);
        final JScrollBar scrollBar = new JScrollBar(JScrollBar.VERTICAL);
        scrollBar.setModel(scrollModel);
        add(scrollBar, "dock east");
        scrollBar.addAdjustmentListener(this);
        scrollBar.addAdjustmentListener(canvas);

        addMouseWheelListener(this);
        document.addIRCDocumentListener(this);
        setAutoscrolls(true);

        MouseMotionListener doScrollRectToVisible = new MouseMotionAdapter() {

            /** {@inheritDoc} */
            @Override
            public void mouseDragged(MouseEvent e) {
                if (e.getXOnScreen() > getLocationOnScreen().getX() && e.
                        getXOnScreen() < (getLocationOnScreen().
                        getX() + getWidth()) && e.getModifiersEx() ==
                        MouseEvent.BUTTON1_DOWN_MASK) {
                    if (getLocationOnScreen().getY() > e.getYOnScreen()) {
                        scrollModel.setValue(scrollBar.getValue() - 1);
                    } else if (getLocationOnScreen().getY() + getHeight() <
                            e.getYOnScreen()) {
                        scrollModel.setValue(scrollBar.getValue() + 1);
                    }
                    canvas.highlightEvent(MouseEventType.DRAG, e);
                }
            }
        };
        addMouseMotionListener(doScrollRectToVisible);
    }

    /** {@inheritDoc} */
    @Override
    public void updateUI() {
        setUI(new TextPaneUI());
    }

    /**
     * Returns the last visible line in the textpane.
     *
     * @return Last visible line index
     */
    public int getLastVisibleLine() {
        return scrollModel.getValue();
    }

    /**
     * Sets the scrollbar's maximum position. If the current position is
     * within <code>linesAllowed</code> of the end of the document, the
     * scrollbar's current position is set to the end of the document.
     * 
     * @param linesAllowed The number of lines allowed below the current position
     * @since 0.6
     */
    protected void setScrollBarMax(final int linesAllowed) {
        final int lines = document.getNumLines() - 1;
        final int currentLine = scrollModel.getValue();
        final int allowedDeviation = lines - linesAllowed;

        scrollModel.setMaximum(lines);

        boolean setToMax = currentLine == allowedDeviation;
        if (allowedDeviation == -1) {
            setToMax = true;
        }

        if (!scrollModel.getValueIsAdjusting() && setToMax) {
            scrollModel.setValue(lines);
        }
        canvas.recalc();
    }

    /**
     * Sets the new position for the scrollbar and the associated position
     * to render the text from.
     * @param position new position of the scrollbar
     */
    public void setScrollBarPosition(final int position) {
        scrollModel.setValue(position);
    }

    /** 
     * {@inheritDoc}
     * 
     * @param e Mouse wheel event
     */
    @Override
    public void adjustmentValueChanged(final AdjustmentEvent e) {
        scrollModel.setValue(e.getValue());
    }

    /** 
     * {@inheritDoc}
     * 
     * @param e Mouse wheel event
     */
    @Override
    public void mouseWheelMoved(final MouseWheelEvent e) {
        if (e.getWheelRotation() > 0) {
            scrollModel.setValue(scrollModel.getValue() + e.getScrollAmount());
        } else {
            scrollModel.setValue(scrollModel.getValue() - e.getScrollAmount());
        }
    }

    /**
     *
     * Returns the line information from a mouse click inside the textpane.
     *
     * @param point mouse position
     *
     * @return line number, line part, position in whole line
     */
    public LineInfo getClickPosition(final Point point) {
        return canvas.getClickPosition(point, true);
    }

    /**
     *
     * Returns the line information from a mouse click inside the textpane.
     *
     * @param point mouse position
     * @param selection Are we selecting text?
     *
     * @return line number, line part, position in whole line
     *
     * @since 0.6.3
     */
    public LineInfo getClickPosition(final Point point, final boolean selection) {
        return canvas.getClickPosition(point, selection);
    }

    /**
     * Returns the selected text.
     * 
     *    <li>0 = start line</li>
     *    <li>1 = start char</li>
     *    <li>2 = end line</li>
     *    <li>3 = end char</li>
     *
     * @return Selected text
     */
    public String getSelectedText() {
        final StringBuffer selectedText = new StringBuffer();
        final LinePosition selectedRange = canvas.getSelectedRange();

        if (selectedRange.getStartLine() == -1) {
            return null;
        }

        for (int i = selectedRange.getStartLine(); i <=
                selectedRange.getEndLine();
                i++) {
            if (i != selectedRange.getStartLine()) {
                selectedText.append('\n');
            }
            if (document.getNumLines() <= i) {
                return selectedText.toString();
            }
            final String line = document.getLine(i).getText();
            if (!line.isEmpty()) {
                if (selectedRange.getEndLine() == selectedRange.getStartLine()) {
                    //loop through range
                    if (selectedRange.getStartPos() != -1 && selectedRange.
                            getEndPos() != -1) {
                        selectedText.append(line.substring(
                                selectedRange.getStartPos(),
                                selectedRange.getEndPos()));
                    }
                } else if (i == selectedRange.getStartLine()) {
                    //loop from start of range to the end
                    if (selectedRange.getStartPos() != -1) {
                        selectedText.append(line.substring(
                                selectedRange.getStartPos(), line.length()));
                    }
                } else if (i == selectedRange.getEndLine()) {
                    //loop from start to end of range
                    if (selectedRange.getEndPos() != -1) {
                        selectedText.append(line.substring(0, selectedRange.
                                getEndPos()));
                    }
                } else {
                    //loop the whole line
                    selectedText.append(line);
                }
            }
        }

        return selectedText.toString();
    }

    /**
     * Returns the selected range.
     *
     * @return selected range
     */
    public LinePosition getSelectedRange() {
        return canvas.getSelectedRange();
    }

    /**
     * Returns whether there is a selected range.
     * 
     * @return true iif there is a selected range
     */
    public boolean hasSelectedRange() {
        final LinePosition selectedRange = canvas.getSelectedRange();
        return !(selectedRange.getStartLine() == selectedRange.getEndLine() &&
                selectedRange.getStartPos() == selectedRange.getEndPos());
    }

    /**
     * Selects the specified region of text.
     *
     * @param position Line position
     */
    public void setSelectedTexT(final LinePosition position) {
        canvas.setSelectedRange(position);
    }

    /**
     * Returns the type of text this click represents.
     * 
     * @param lineInfo Line info of click.
     * 
     * @return Click type for specified position
     */
    public ClickTypeValue getClickType(final LineInfo lineInfo) {
        return canvas.getClickType(lineInfo);
    }

    /**
     * Returns the surrouding word at the specified position.
     * 
     * @param lineNumber Line number to get word from
     * @param index Position to get surrounding word
     * 
     * @return Surrounding word
     */
    public String getWordAtIndex(final int lineNumber, final int index) {
        if (lineNumber == -1) {
            return "";
        }
        final int[] indexes =
                canvas.getSurroundingWordIndexes(document.getLine(lineNumber).
                getText(),
                index);
        return document.getLine(lineNumber).getText().substring(indexes[0],
                indexes[1]);
    }

    /** Adds the selected text to the clipboard. */
    public void copy() {
        if (getSelectedText() != null && !getSelectedText().isEmpty()) {
            Toolkit.getDefaultToolkit().getSystemClipboard().setContents(
                    new StringSelection(getSelectedText()), null);
        }
    }

    /** Clears the textpane. */
    public void clear() {
        document.clear();
        scrollModel.setValue(0);
        setScrollBarMax(1);
        canvas.recalc();
    }

    /** Clears the selection. */
    public void clearSelection() {
        canvas.clearSelection();
    }

    /**
     * Trims the document to the specified number of lines.
     *
     * @param numLines Number of lines to trim the document to
     */
    public void trim(final int numLines) {
        if (document.getNumLines() < numLines) {
            return;
        }
        final int trimmedLines = document.getNumLines() - numLines;
        final LinePosition selectedRange = getSelectedRange();

        selectedRange.setStartLine(selectedRange.getStartLine() - trimmedLines);
        selectedRange.setEndLine(selectedRange.getEndLine() - trimmedLines);

        if (selectedRange.getStartLine() < 0) {
            selectedRange.setStartLine(0);
        }
        if (selectedRange.getEndLine() < 0) {
            selectedRange.setEndLine(0);
        }

        setSelectedTexT(selectedRange);
        document.trim(numLines);
    }

    /** Scrolls one page up in the textpane. */
    public void pageDown() {
        scrollModel.setValue(scrollModel.getValue() + 10);
    }

    /** Scrolls one page down in the textpane. */
    public void pageUp() {
        scrollModel.setValue(scrollModel.getValue() - 10);
    }

    /** Scrolls to the beginning of the TextPane */
    public void goToHome() {
        scrollModel.setValue(0);
    }

    /** Scrolls to the end of the TextPane */
    public void goToEnd() {
        scrollModel.setValue(document.getNumLines());
    }

    /** {@inheritDoc}. */
    @Override
    public void lineAdded(final int line, final int size) {
        setScrollBarMax(1);
    }

    /** {@inheritDoc}. */
    @Override
    public void trimmed(final int numLines) {
        setScrollBarMax(1);
    }

    /** {@inheritDoc}. */
    @Override
    public void cleared() {
        canvas.recalc();
    }

    /** {@inheritDoc}. */
    @Override
    public void linesAdded(int line, int length, int size) {
        setScrollBarMax(length);
    }

    /** {@inheritDoc}. */
    @Override
    public void repaintNeeded() {
        canvas.recalc();
    }

    /**
     * Retrieves this textpane's IRCDocument.
     * 
     * @return This textpane's IRC document
     */
    public IRCDocument getDocument() {
        return document;
    }

    /**
     * Retrives the parent window for this textpane.
     * 
     * @return Parent window
     */
    public Window getWindow() {
        return frame;
    }
}
