/*
 * jNPad v0.3 - jNPad's an Simple Text Editor written in Java
 *
 * Copyright (C) 2014-2017  rgs
 *
 * Require JDK 1.6 (or later)
 *
 * This program is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by the Free
 * Software Foundation; either version 2 of the License, or (at your option)
 * any later version.
 *
 * This program 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 General Public License for
 * more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 *
 *
 * Info, Questions, Suggestions & Bugs Report to rgsevero@gmail.com
 */

package jnpad.text;

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Insets;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.event.MouseEvent;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.HashMap;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.swing.JPanel;
import javax.swing.SwingUtilities;
import javax.swing.border.Border;
import javax.swing.border.CompoundBorder;
import javax.swing.border.EmptyBorder;
import javax.swing.event.CaretEvent;
import javax.swing.event.CaretListener;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.event.MouseInputAdapter;
import javax.swing.text.AttributeSet;
import javax.swing.text.BadLocationException;
import javax.swing.text.Element;
import javax.swing.text.StyleConstants;

import jnpad.GUIUtilities;
import jnpad.config.Config;
import jnpad.config.Configurable;
import jnpad.ui.SideBorder;
import jnpad.util.Utilities;

/**
 * The Class SimpleGutter.
 *
 * @version 0.3
 * @since   jNPad v0.3
 */
public class SimpleGutter extends JPanel implements Configurable {
  /** The Constant LEFT. */
  public final static float    LEFT                 = 0.0f;

  /** The Constant CENTER. */
  public final static float    CENTER               = 0.5f;

  /** The Constant RIGHT. */
  public final static float    RIGHT                = 1.0f;

  /** The Constant PREFERRED_HEIGHT. */
  private final static int     PREFERRED_HEIGHT     = Integer.MAX_VALUE - 1000000;

  // Properties that can be changed
  boolean                      updateFont;
  int                          borderGap;
  int                          selectionWidth;                                                              // [0-5]
  Color                        selectionColor       = Color.YELLOW;
  Color                        selectionBorderColor = Color.YELLOW.darker();
  boolean                      hasSelectionBorder   = true;
  Color                        bookmarkBackground   = Color.CYAN;
  int                          bookmarkWidth        = 7;                                                    // [7-15] impar
  int                          bookmarkGap          = 2;                                                    // [0 - 10]
  Color                        bookmarkColor        = Color.RED;
  Color                        bookmarkBorderColor  = Color.RED.darker();
  Color                        currentLineForeground;
  Color                        currentLineBackground;
  boolean                      currentLineBackgroundEnabled;
  float                        digitAlignment;
  int                          minimumDisplayDigits;
  boolean                      lineBorderVisible    = true;
  Border                       outerBorder          = new SideBorder(null, null, null, Color.LIGHT_GRAY, 1);

  // Keep history information to reduce the number of times the component
  // needs to be repainted
  int                          lastDigits;
  int                          lastHeight;
  int                          lastLine;
  boolean                      lasHasSelection;

  HashMap<String, FontMetrics> fonts;

  int                          selectionBegin;
  int                          selectionEnd;

  EditPane                     editPane;
  AbstractTextArea             textArea;

  /** Logger */
  private static final Logger  LOGGER               = Logger.getLogger(SimpleGutter.class.getName());

  /** UID */
  private static final long    serialVersionUID     = -5244882856595180262L;

  /**
   * Instantiates a new gutter_.
   *
   * @param textArea the text area
   */
  public SimpleGutter(AbstractTextArea textArea) {
    this(textArea, 3);
  }

  /**
   * Instantiates a new gutter_.
   *
   * @param textArea the text area
   * @param minimumDisplayDigits the minimum display digits
   */
  public SimpleGutter(AbstractTextArea textArea, int minimumDisplayDigits) {
    editPane = textArea.editPane;
    this.textArea = textArea;

    doConfigure(CFG_ALL);

    setDigitAlignment(RIGHT);
    setMinimumDisplayDigits(minimumDisplayDigits);

    // Add a listener to get events important to the gutter.
    final Handler handler = new Handler();
    textArea.getDocument().addDocumentListener(handler);
    textArea.addCaretListener(handler);
    textArea.addPropertyChangeListener("font", handler); //$NON-NLS-1$
    textArea.addMouseListener(handler);
    textArea.addMouseMotionListener(handler);
    addMouseListener(handler);
  }

  /**
   * Configure.
   * 
   * @param cfg int
   * @see jnpad.config.Configurable#configure(int)
   */
  @Override
  public void configure(final int cfg) {
    doConfigure(cfg);
  }
  
  /**
   * Do configure.
   *
   * @param cfg the cfg
   */
  private void doConfigure(final int cfg) {
    if ((cfg & CFG_COLOR) != 0) {
      setBackground(Config.GUTTER_BACKGROUND.getValue());
      setForeground(Config.GUTTER_FOREGROUND.getValue());
      setCurrentLineForeground(Config.GUTTER_CURRENT_FOREGROUND.getValue());
      setCurrentLineBackground(Config.GUTTER_CURRENT_BACKGROUND.getValue());
      setCurrentLineBackgroundEnabled(Config.GUTTER_CURRENT_BACKGROUND_ENABLED.getValue());
      setLineBorderVisible(Config.GUTTER_BORDER_VISIBLE.getValue());
      setLineBorder(Config.GUTTER_BORDER_COLOR.getValue(), Config.GUTTER_BORDER_WIDTH.getValue());
      setBorderGap(Config.GUTTER_BORDER_GAP.getValue());
      setSelectionWidth(Config.GUTTER_SELECTION_WIDTH.getValue());
      setSelectionColor(Config.GUTTER_SELECTION_COLOR.getValue());
      setSelectionBorderColor(Config.GUTTER_SELECTION_BORDER_COLOR.getValue());
      setHasSelectionBorder(Config.GUTTER_SELECTION_BORDER.getValue());
      setBookmarkBackground(Config.GUTTER_BOOKMARK_BACKGROUND.getValue());
      setBookmarkColor(Config.GUTTER_BOOKMARK_COLOR.getValue());
      setBookmarkBorderColor(Config.GUTTER_BOOKMARK_BORDER_COLOR.getValue());
      setBookmarkWidth(Config.GUTTER_BOOKMARK_WIDTH.getValue());
      setBookmarkGap(Config.GUTTER_BOOKMARK_GAP.getValue());
    }
    if ((cfg & CFG_FONT) != 0) {
      setFont(textArea.getFont());
      setUpdateFont(Config.GUTTER_UPDATE_FONT.getValue());
    }
  }

  /**
   * Gets the update font property.
   * 
   * @return the update font property
   */
  public boolean getUpdateFont() {
    return updateFont;
  }

  /**
   * Set the update font property. Indicates whether this Font should be updated
   * automatically when the Font of the related text component is changed.
   * 
   * @param updateFont when true update the Font and repaint the line numbers,
   *          otherwise just repaint the line numbers.
   */
  public void setUpdateFont(boolean updateFont) {
    this.updateFont = updateFont;
  }

  /**
   * Gets the selection width.
   * 
   * @return int
   */
  public int getSelectionWidth() {
    return selectionWidth;
  }

  /**
   * Sets the selection width.
   * 
   * @param width int
   */
  public void setSelectionWidth(int width) {
    selectionWidth = width > 5 ? 5 : width < 0 ? 0 : width;
  }

  /**
   * Gets the selection color.
   *
   * @return the selection color
   */
  public Color getSelectionColor() {
    return selectionColor;
  }

  /**
   * Sets the selection color.
   *
   * @param color the new selection color
   */
  public void setSelectionColor(Color color) {
    if (color != null) {
      selectionColor = color;
    }
  }

  /**
   * Gets the selection border color.
   * 
   * @return Color
   */
  public Color getSelectionBorderColor() {
    return selectionBorderColor;
  }

  /**
   * Sets the selection border color.
   * 
   * @param color Color
   */
  public void setSelectionBorderColor(Color color) {
    if (color != null) {
      selectionBorderColor = color;
    }
  }

  /**
   * Checks for selection border.
   *
   * @return true, if successful
   */
  public boolean hasSelectionBorder() {
    return hasSelectionBorder;
  }

  /**
   * Sets the checks for selection border.
   *
   * @param b the new checks for selection border
   */
  public void setHasSelectionBorder(boolean b) {
    hasSelectionBorder = b;
  }

  /**
   * Gets the bookmark background.
   *
   * @return the bookmark background
   */
  public Color getBookmarkBackground() {
    return bookmarkBackground;
  }

  /**
   * Sets the bookmark background.
   *
   * @param color the new bookmark background
   */
  public void setBookmarkBackground(Color color) {
    if (color != null) {
      bookmarkBackground = color;
    }
  }

  /**
   * Gets the bookmark area width.
   *
   * @return the bookmark area width
   */
  public int getBookmarkAreaWidth() {
    return getBookmarkWidth() + getBookmarkGap() * 2;
  }

  /**
   * Gets the bookmark width.
   *
   * @return the bookmark width
   */
  public int getBookmarkWidth() {
    return bookmarkWidth;
  }

  /**
   * Sets the bookmark width.
   *
   * @param width the new bookmark width
   */
  public void setBookmarkWidth(int width) {
    bookmarkWidth = width > 15 ? 15 : width < 7 ? 7 : width;
  }

  /**
   * Gets the bookmark gap.
   *
   * @return the bookmark gap
   */
  public int getBookmarkGap() {
    return bookmarkGap;
  }

  /**
   * Sets the bookmark gap.
   *
   * @param gap the new bookmark gap
   */
  public void setBookmarkGap(int gap) {
    bookmarkGap = gap > 10 ? 10 : gap < 0 ? 0 : gap;
  }

  /**
   * Gets the bookmark color.
   *
   * @return the bookmark color
   */
  public Color getBookmarkColor() {
    return bookmarkColor;
  }

  /**
   * Sets the bookmark color.
   *
   * @param color the new bookmark color
   */
  public void setBookmarkColor(Color color) {
    if (color != null) {
      bookmarkColor = color;
    }
  }

  /**
   * Gets the bookmark border color.
   *
   * @return the bookmark border color
   */
  public Color getBookmarkBorderColor() {
    return bookmarkBorderColor;
  }

  /**
   * Sets the bookmark border color.
   *
   * @param color the new bookmark border color
   */
  public void setBookmarkBorderColor(Color color) {
    if (color != null) {
      bookmarkBorderColor = color;
    }
  }

  /**
   * Gets the border gap.
   *
   * @return the border gap
   */
  public int getBorderGap() {
    return borderGap;
  }

  /**
   * Sets the border gap.
   *
   * @param gap the new border gap
   */
  public void setBorderGap(int gap) {
    borderGap = gap;
    Border inner = new EmptyBorder(0, gap + getBookmarkAreaWidth(), 0, gap);
    if (isLineBorderVisible()) {
      setBorder(new CompoundBorder(getLineBorder(), inner));
    }
    else {
      setBorder(inner);
    }
    lastDigits = 0;
    setPreferredWidth();
  }

  /**
   * Checks if is line border visible.
   *
   * @return true, if is line border visible
   */
  public boolean isLineBorderVisible() {
    return lineBorderVisible;
  }

  /**
   * Sets the line border visible.
   *
   * @param b the new line border visible
   */
  public void setLineBorderVisible(boolean b) {
    lineBorderVisible = b;
  }

  /**
   * Gets the line border.
   *
   * @return the line border
   */
  public Border getLineBorder() {
    return outerBorder;
  }

  /**
   * Sets the line border.
   *
   * @param color the color
   * @param width the width
   */
  public void setLineBorder(Color color, int width) {
    if (color != null) {
      outerBorder = new SideBorder(null, null, null, color, width);
    }
  }

  /**
   * Gets the current line foreground.
   *
   * @return the current line foreground
   */
  public Color getCurrentLineForeground() {
    return currentLineForeground == null ? getForeground() : currentLineForeground;
  }

  /**
   * Sets the current line foreground.
   *
   * @param c the new current line foreground
   */
  public void setCurrentLineForeground(Color c) {
    currentLineForeground = c;
  }

  /**
   * Gets the current line background.
   *
   * @return the current line background
   */
  public Color getCurrentLineBackground() {
    return currentLineBackground == null ? getBackground() : currentLineBackground;
  }

  /**
   * Sets the current line background.
   * 
   * @param c the new current line background
   */
  public void setCurrentLineBackground(Color c) {
    currentLineBackground = c;
  }

  /**
   * Checks if is current line background enabled.
   *
   * @return true, if is current line background enabled
   */
  public boolean isCurrentLineBackgroundEnabled() {
    return currentLineBackgroundEnabled;
  }

  /**
   * Sets the current line background enabled.
   *
   * @param b the new current line background enabled
   */
  public void setCurrentLineBackgroundEnabled(boolean b) {
    currentLineBackgroundEnabled = b;
  }

  /**
   * Gets the digit alignment.
   * 
   * @return the alignment of the painted digits
   */
  public float getDigitAlignment() {
    return digitAlignment;
  }

  /**
   * Specify the horizontal alignment of the digits within the component. Common
   * values would be:
   * <ul>
   * <li>SimpleGutter.LEFT
   * <li>SimpleGutter.CENTER
   * <li>SimpleGutter.RIGHT (default)
   * </ul>
   * 
   * @param digitAlignment float
   */
  public void setDigitAlignment(float digitAlignment) {
    this.digitAlignment = digitAlignment > RIGHT ? RIGHT : digitAlignment < LEFT ? LEFT : digitAlignment;
  }

  /**
   * Gets the minimum display digits.
   * 
   * @return the minimum display digits
   */
  public int getMinimumDisplayDigits() {
    return minimumDisplayDigits;
  }

  /**
   * Specify the mimimum number of digits used to calculate the preferred width
   * of the component. Default is 3.
   * 
   * @param minimumDisplayDigits the number digits used in the preferred width
   *          calculation
   */
  public void setMinimumDisplayDigits(int minimumDisplayDigits) {
    this.minimumDisplayDigits = minimumDisplayDigits;
    setPreferredWidth();
  }

  /**
   * Calculate the width needed to display the maximum line number
   */
  void setPreferredWidth() {
    Element root = textArea.getDocument().getDefaultRootElement();
    int lines = root.getElementCount();
    int digits = Math.max(String.valueOf(lines).length(), minimumDisplayDigits);

    //  Update sizes when number of digits in the line number changes
    if (lastDigits != digits) {
      lastDigits = digits;
      FontMetrics fontMetrics = getFontMetrics(getFont());
      int width = fontMetrics.charWidth('0') * digits;
      Insets insets = getInsets();
      int preferredWidth = insets.left + insets.right + width;

      Dimension d = getPreferredSize();
      d.setSize(preferredWidth, PREFERRED_HEIGHT);
      setPreferredSize(d);
      setSize(d);
    }
  }
  
  /**
   * Super paint component.
   *
   * @param g the Graphics
   */
  void superPaintComponent(Graphics g) {
    super.paintComponent(g);
  }

  /**
   * Draw the gutter.
   * 
   * @param g Graphics
   * @see javax.swing.JComponent#paintComponent(java.awt.Graphics)
   */
  @Override
  protected void paintComponent(Graphics g) {
    GUIUtilities.setRenderingHints(g);
    super.paintComponent(g);

    // bookmarks area
    g.setColor(getBookmarkBackground());
    if (textArea.isMain()) {
      g.fillRect(0, 0, getBookmarkAreaWidth(), getHeight());
    }
    else {
      g.drawLine(getBookmarkAreaWidth(), 0, getBookmarkAreaWidth(), getHeight());
    }

    // Determine the width of the space available to draw the line number
    FontMetrics fontMetrics = textArea.getFontMetrics(textArea.getFont());
    Insets insets = getInsets();
    int availableWidth = getSize().width - insets.left - insets.right;

    // Determine the rows to draw within the clipped bounds.
    Rectangle clip = g.getClipBounds();
    int rowStartOffset = textArea.viewToModel(new Point(0, clip.y));
    int endOffset = textArea.viewToModel(new Point(0, clip.y + clip.height));

    while (rowStartOffset <= endOffset) {
      try {
        final Rectangle r = textArea.modelToView(rowStartOffset);

        final boolean isCurrentLine = isCurrentLine(rowStartOffset);

        // Paint current line background 
        if (isCurrentLine && isCurrentLineBackgroundEnabled()
            && !editPane.isActiveLineVisible()) { //[added v0.3] 
          g.setColor(getCurrentLineBackground());
          g.fillRect(r.x + getBookmarkAreaWidth(), r.y, getWidth(), r.height);
        }

        // Paint the line number
        g.setColor(isCurrentLine ? getCurrentLineForeground() : getForeground());
        String lineNumber = getTextLineNumber(rowStartOffset);
        int stringWidth = fontMetrics.stringWidth(lineNumber);
        int x = getOffsetX(availableWidth, stringWidth) + insets.left;
        int y = getOffsetY(rowStartOffset, fontMetrics);
        g.drawString(lineNumber, x, y);

        // Move to the next row
        rowStartOffset = javax.swing.text.Utilities.getRowEnd(textArea, rowStartOffset) + 1;
      }
      catch (Exception ex) {
        LOGGER.log(Level.WARNING, ex.getMessage(), ex);
      }
    }

    Graphics2D g2D = (Graphics2D) g;

    final Object oldRendering = g2D.getRenderingHint(RenderingHints.KEY_RENDERING);
    final Object oldAntialiasing = g2D.getRenderingHint(RenderingHints.KEY_ANTIALIASING);

    g2D.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
    g2D.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);

    try {
      // Paint bookmarks
      if (editPane.hasBookmarks()) {
        DocumentRange[] ranges = editPane.getBookmarks();
        for (DocumentRange range : ranges) {
          paintBookmark(g, range.getStartOffset());
        }
      }

      // Paint selection
      if (textArea.hasSelection() && getSelectionWidth() > 1) {
        paintSelection(g);
      }
    }
    finally {
      g2D.setRenderingHint(RenderingHints.KEY_RENDERING, oldRendering);
      g2D.setRenderingHint(RenderingHints.KEY_ANTIALIASING, oldAntialiasing);
    }
  }

  /**
   * Paint bookmark.
   * 
   * @param g Graphics
   * @param rowStartOffset the row start offset
   */
  void paintBookmark(Graphics g, int rowStartOffset) {
    try {
      Rectangle r = textArea.modelToView(rowStartOffset);

      final int x = getBookmarkGap();
      final int w = getBookmarkWidth();

      // 0___4
      // |   |
      // |/2\|
      // 1   3
      int[] xs = new int[5];
      int[] ys = new int[5];
      xs[0] = xs[1] = x;
      xs[2] = x + w / 2;
      xs[3] = xs[4] = x + w - 1;
      ys[0] = ys[4] = r.y + 1;
      ys[1] = ys[3] = r.y + r.height - 2;
      ys[2] = r.y + r.height - 5;

      g.setColor(getBookmarkColor());
      g.fillPolygon(xs, ys, 5);
      g.setColor(getBookmarkBorderColor());
      g.drawPolygon(xs, ys, 5);
    }
    catch (Exception ex) {
      LOGGER.log(Level.WARNING, ex.getMessage(), ex);
    }
  }

  /**
   * Paint selection.
   * 
   * @param g Graphics
   */
  void paintSelection(Graphics g) {
    try {
      int selectionStart = textArea.getSelectionStart();
      int selectionEnd = textArea.getSelectionEnd();
      if (selectionEnd > selectionStart) {
        Rectangle r1 = textArea.modelToView(selectionStart);
        Rectangle r2 = textArea.modelToView(selectionEnd);

        int x = getWidth() - getSelectionWidth() - 3;
        int y = r1.y;
        int w = getSelectionWidth();
        int h = r2.y - r1.y + r2.height;

        g.setColor(getSelectionColor());
        g.fillRect(x, y, w, h);

        if (hasSelectionBorder()) {
          GUIUtilities.drawBorder(g, getSelectionBorderColor(), x, y, w, h);
        }
      }
    }
    catch (Exception ex) {
      LOGGER.log(Level.WARNING, ex.getMessage(), ex);
    }
  }

  /**
   * We need to know if the caret is currently positioned on the line we are
   * about to paint so the line number can be highlighted.
   * 
   * @param rowStartOffset int
   * @return boolean
   */
  boolean isCurrentLine(int rowStartOffset) {
    int caretPosition = textArea.getCaretPosition();
    Element root = textArea.getDocument().getDefaultRootElement();
    return root.getElementIndex(rowStartOffset) == root.getElementIndex(caretPosition);
  }

  /**
   * Get the line number to be drawn. The empty string will be returned when a
   * line of text has wrapped.
   * 
   * @param rowStartOffset int
   * @return String
   */
  String getTextLineNumber(int rowStartOffset) {
    Element root = textArea.getDocument().getDefaultRootElement();
    int index = root.getElementIndex(rowStartOffset);
    Element line = root.getElement(index);

    if (line.getStartOffset() == rowStartOffset) {
      int i = index + 1;
      return String.valueOf(i); // return (i < 10) ? "0".concat(String.valueOf(i)) : String.valueOf(i);
    }
    return Utilities.EMPTY_STRING;
  }

  /**
   * Determine the X offset to properly align the line number when drawn.
   * 
   * @param availableWidth int
   * @param stringWidth int
   * @return int
   */
  int getOffsetX(int availableWidth, int stringWidth) {
    return (int) ((availableWidth - stringWidth) * digitAlignment);
  }

  /**
   * Determine the Y offset for the current row.
   * 
   * @param rowStartOffset int
   * @param fontMetrics FontMetrics
   * @return int
   * @throws BadLocationException the bad location exception
   */
  int getOffsetY(int rowStartOffset, FontMetrics fontMetrics) throws BadLocationException {
    // Get the bounding rectangle of the row
    Rectangle r = textArea.modelToView(rowStartOffset);
    int lineHeight = fontMetrics.getHeight();
    int y = r.y + r.height;
    int descent = 0;

    // The text needs to be positioned above the bottom of the bounding
    // rectangle based on the descent of the font(s) contained on the row.
    if (r.height == lineHeight) { // default font is being used
      descent = fontMetrics.getDescent();
    }
    else { // We need to check all the attributes for font changes
      if (fonts == null) {
        fonts = new HashMap<String, FontMetrics>();
      }
      Element root = textArea.getDocument().getDefaultRootElement();
      int index = root.getElementIndex(rowStartOffset);
      Element line = root.getElement(index);

      for (int i = 0; i < line.getElementCount(); i++) {
        Element child = line.getElement(i);
        AttributeSet as = child.getAttributes();
        String fontFamily = (String) as.getAttribute(StyleConstants.FontFamily);
        Integer fontSize = (Integer) as.getAttribute(StyleConstants.FontSize);
        String key = fontFamily + fontSize;

        FontMetrics fm = fonts.get(key);

        if (fm == null) {
          Font font = new Font(fontFamily, Font.PLAIN, fontSize.intValue());
          fm = textArea.getFontMetrics(font);
          fonts.put(key, fm);
        }

        descent = Math.max(descent, fm.getDescent());
      }
    }

    return y - descent;
  }

  /**
   * Check for repaint.
   */
  void checkForRepaint() {
    // Get the line the caret is positioned on
    int     caretPosition = textArea.getCaretPosition();
    Element root          = textArea.getDocument().getDefaultRootElement();
    int     currentLine   = root.getElementIndex(caretPosition);
    boolean hasSelection  = textArea.hasSelection();

    final boolean changeLine = lastLine != currentLine;
    final boolean changeSelection = lasHasSelection != hasSelection;

    // Need to repaint so the correct line number can be highlighted
    if (changeLine || changeSelection) {
      repaint();
      if (changeLine) {
        lastLine = currentLine;
      }
      if (changeSelection) {
        lasHasSelection = hasSelection;
      }
    }
  }

  /**
   * A document change may affect the number of displayed lines of text.
   * Therefore the lines numbers will also change.
   */
  void documentChanged() {
    // Preferred size of the component has not been updated at the time
    // the DocumentEvent is fired
    SwingUtilities.invokeLater(new Runnable() {
      public void run() {
        int preferredHeight = textArea.getPreferredSize().height;

        // Document change has caused a change in the number of lines.
        // Repaint to reflect the new line numbers
        if (lastHeight != preferredHeight) {
          setPreferredWidth();
          repaint();
          lastHeight = preferredHeight;
        }
      }
    });
  }

  /**
   * Handle mouse released.
   *
   * @param e the MouseEvent
   */
  void handleMouseReleased(final MouseEvent e) {
    // empty
  }

  /**
   * Handle mouse pressed.
   *
   * @param e the MouseEvent
   */
  void handleMousePressed(final MouseEvent e) {
    Object obj = e.getSource();
    if (obj == textArea) {
      checkForRepaint();
    }
    else if (obj == this) {
      if (!e.isControlDown() && e.getClickCount() == 1) {
        if (e.isShiftDown()) {
          int rowPos = textArea.viewToModel(new Point(0, e.getY()));
          Element line = textArea.getJNPadDocument().getParagraphElement(rowPos);
          selectionEnd = line.getEndOffset() - 1; // -1
          if (selectionBegin <= selectionEnd)
            textArea.select(selectionBegin, selectionEnd);
          else
            textArea.select(selectionEnd, selectionBegin);
        }
        else {
          int rowPos = textArea.viewToModel(new Point(0, e.getY()));
          Element line = textArea.getJNPadDocument().getParagraphElement(rowPos);
          selectionBegin = line.getStartOffset();
          selectionEnd = line.getEndOffset() - 1; // -1
          textArea.select(selectionBegin, selectionEnd);
        }
      }
    }
  }

  /**
   * Handle mouse dragged.
   *
   * @param e the MouseEvent
   */
  void handleMouseDragged(final MouseEvent e) {
    Object obj = e.getSource();
    if (obj == textArea) {
      checkForRepaint();
    }
  }

  //////////////////////////////////////////////////////////////////////////////
  private class Handler extends MouseInputAdapter implements CaretListener, DocumentListener, PropertyChangeListener {
    /**
     * Mouse released.
     *
     * @param e the MouseEvent
     * @see java.awt.event.MouseAdapter#mouseReleased(java.awt.event.MouseEvent)
     */
    @Override
    public void mouseReleased(MouseEvent e) {
      handleMouseReleased(e);
    }
    
    /**
     * Mouse pressed.
     *
     * @param e the MouseEvent
     * @see java.awt.event.MouseAdapter#mousePressed(java.awt.event.MouseEvent)
     */
    @Override
    public void mousePressed(final MouseEvent e) {
      handleMousePressed(e);
    }

    /**
     * Mouse dragged.
     *
     * @param e the MouseEvent
     * @see java.awt.event.MouseAdapter#mouseDragged(java.awt.event.MouseEvent)
     */
    @Override
    public void mouseDragged(final MouseEvent e) {
      handleMouseDragged(e);
    }

    /**
     * Insert update.
     * 
     * @param e DocumentEvent
     * @see javax.swing.event.DocumentListener#insertUpdate(javax.swing.event.DocumentEvent)
     */
    @Override
    public void insertUpdate(final DocumentEvent e) {
      documentChanged();
    }

    /**
     * Removes the update.
     * 
     * @param e DocumentEvent
     * @see javax.swing.event.DocumentListener#removeUpdate(javax.swing.event.DocumentEvent)
     */
    @Override
    public void removeUpdate(final DocumentEvent e) {
      documentChanged();
    }

    /**
     * Changed update.
     * 
     * @param e DocumentEvent
     * @see javax.swing.event.DocumentListener#changedUpdate(javax.swing.event.DocumentEvent)
     */
    @Override
    public void changedUpdate(final DocumentEvent e) {
      documentChanged();
    }

    /**
     * Caret update.
     * 
     * @param e CaretEvent
     * @see javax.swing.event.CaretListener#caretUpdate(javax.swing.event.CaretEvent)
     */
    @Override
    public void caretUpdate(final CaretEvent e) {
      checkForRepaint();
    }

    /**
     * Property change.
     * 
     * @param e PropertyChangeEvent
     * @see java.beans.PropertyChangeListener#propertyChange(java.beans.PropertyChangeEvent)
     */
    @Override
    public void propertyChange(final PropertyChangeEvent e) {
      if (e.getNewValue() instanceof Font) {
        if (updateFont) {
          Font newFont = (Font) e.getNewValue();
          setFont(newFont);
          lastDigits = 0;
          setPreferredWidth();
        }
        else {
          repaint();
        }
      }
    }
  }
  //////////////////////////////////////////////////////////////////////////////

}
