/* -*- mode: java; c-basic-offset: 2; indent-tabs-mode: nil -*- */

/*
 * TextAreaPainter.java - Paints the text area
 * Copyright (C) 1999 Slava Pestov
 *
 * You may use and modify this package for any purpose. Redistribution is
 * permitted, in both source and binary form, provided that this notice
 * remains intact in all source distributions of this package.
 */
package processing.app.syntax;

import java.awt.Cursor;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.event.MouseEvent;

import javax.swing.ToolTipManager;
import javax.swing.text.*;
import javax.swing.JComponent;

import processing.app.Preferences;
import processing.app.syntax.im.CompositionTextPainter;
import processing.app.ui.Toolkit;


/**
 * The text area repaint manager. It performs double buffering and paints
 * lines of text.
 * @author Slava Pestov
 */
public class TextAreaPainter extends JComponent implements TabExpander {
  /** A specific painter composed by the InputMethod.*/
  protected CompositionTextPainter compositionTextPainter;

  protected JEditTextArea textArea;
  protected TextAreaDefaults defaults;

//  protected boolean blockCaret;
//  protected SyntaxStyle[] styles;
//  protected Color caretColor;
//  protected Color selectionColor;
//  protected Color lineHighlightColor;
//  protected boolean lineHighlight;
//  protected Color bracketHighlightColor;
//  protected boolean bracketHighlight;
//  protected Color eolMarkerColor;
//  protected boolean eolMarkers;

//  protected int cols;
//  protected int rows;

  // moved from TextAreaDefaults
  private Font plainFont;
  private Font boldFont;
  private boolean antialias;
//  private Color fgcolor;
//  private Color bgcolor;

  protected int tabSize;
  protected FontMetrics fm;

  protected Highlight highlights;

  int currentLineIndex;
  Token currentLineTokens;
  Segment currentLine;


  /**
   * Creates a new repaint manager. This should be not be called directly.
   */
  public TextAreaPainter(JEditTextArea textArea, TextAreaDefaults defaults) {
    this.textArea = textArea;
    this.defaults = defaults;

    setAutoscrolls(true);
//    setDoubleBuffered(true);
    setOpaque(true);

    ToolTipManager.sharedInstance().registerComponent(this);

    currentLine = new Segment();
    currentLineIndex = -1;

    setCursor(Cursor.getPredefinedCursor(Cursor.TEXT_CURSOR));

//    // unfortunately probably can't just do setDefaults() since things aren't quite set up
//    setFont(defaults.plainFont);
////    System.out.println("defaults font is " + defaults.font);
//    setForeground(defaults.fgcolor);
//    setBackground(defaults.bgcolor);
    updateAppearance();

//    blockCaret = defaults.blockCaret;
//    styles = defaults.styles;
//    caretColor = defaults.caretColor;
//    selectionColor = defaults.selectionColor;
//    lineHighlightColor = defaults.lineHighlightColor;
//    lineHighlight = defaults.lineHighlight;
//    bracketHighlightColor = defaults.bracketHighlightColor;
//    bracketHighlight = defaults.bracketHighlight;
//    eolMarkerColor = defaults.eolMarkerColor;
//    eolMarkers = defaults.eolMarkers;
//    antialias = defaults.antialias;

//    cols = defaults.cols;
//    rows = defaults.rows;
  }


  public void updateAppearance() {
    setForeground(defaults.fgcolor);
    setBackground(defaults.bgcolor);

    // Ensure that our monospaced font is loaded
    // https://github.com/processing/processing/pull/4639
    Toolkit.getMonoFontName();

    String fontFamily = Preferences.get("editor.font.family");
    final int fontSize = Toolkit.zoom(Preferences.getInteger("editor.font.size"));
    plainFont = new Font(fontFamily, Font.PLAIN, fontSize);
    if (!fontFamily.equals(plainFont.getFamily())) {
      System.err.println(fontFamily + " not available, resetting to monospaced");
      fontFamily = "Monospaced";
      Preferences.set("editor.font.family", fontFamily);
      plainFont = new Font(fontFamily, Font.PLAIN, fontSize);
    }
    boldFont = new Font(fontFamily, Font.BOLD, fontSize);
    antialias = Preferences.getBoolean("editor.smooth");

    // moved from setFont() override (never quite comfortable w/ that override)
    fm = super.getFontMetrics(plainFont);
    tabSize = fm.charWidth(' ') * Preferences.getInteger("editor.tabs.size");
    textArea.recalculateVisibleLines();
  }


  /*
  public void setDefaults(TextAreaDefaults defaults) {
    setFont(defaults.font);
    setForeground(defaults.fgcolor);
    setBackground(defaults.bgcolor);

    setBlockCaretEnabled(defaults.blockCaret);
    setStyles(defaults.styles);
    setCaretColor(defaults.caretColor);
    setSelectionColor(defaults.selectionColor);
    setLineHighlightColor(defaults.lineHighlightColor);
    setLineHighlightEnabled(defaults.lineHighlight);
    setBracketHighlightColor(defaults.bracketHighlightColor);
    setBracketHighlightEnabled(defaults.bracketHighlight);
    setEOLMarkerColor(defaults.eolMarkerColor);
    setEOLMarkersPainted(defaults.eolMarkers);
    setAntialias(defaults.antialias);

    // only used for getPreferredSize()
    cols = defaults.cols;
    rows = defaults.rows;
  }
  */


  /**
   * Get CompositionTextPainter, creating one if it doesn't exist.
   */
   public CompositionTextPainter getCompositionTextpainter() {
     if (compositionTextPainter == null){
       compositionTextPainter = new CompositionTextPainter(textArea);
     }
     return compositionTextPainter;
   }


  /**
   * Returns the syntax styles used to paint colorized text. Entry <i>n</i>
   * will be used to paint tokens with id = <i>n</i>.
   * @see processing.app.syntax.Token
   */
  public final SyntaxStyle[] getStyles() {
    return defaults.styles;
  }


//  /**
//   * Sets the syntax styles used to paint colorized text. Entry <i>n</i>
//   * will be used to paint tokens with id = <i>n</i>.
//   * @param styles The syntax styles
//   * @see processing.app.syntax.Token
//   */
//  public final void setStyles(SyntaxStyle[] styles) {
//    this.styles = styles;
//    repaint();
//  }


//  /**
//   * Returns the caret color.
//   */
//  public final Color getCaretColor() {
//    return caretColor;
//  }


//  /**
//   * Sets the caret color.
//   * @param caretColor The caret color
//   */
//  public final void setCaretColor(Color caretColor) {
//    this.caretColor = caretColor;
//    invalidateSelectedLines();
//  }


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


//  /**
//   * Sets the selection color.
//   * @param selectionColor The selection color
//   */
//  public final void setSelectionColor(Color selectionColor) {
//    this.selectionColor = selectionColor;
//    invalidateSelectedLines();
//  }


//  /**
//   * Returns the line highlight color.
//   */
//  public final Color getLineHighlightColor() {
//    return lineHighlightColor;
//  }


//  /**
//   * Sets the line highlight color.
//   * @param lineHighlightColor The line highlight color
//   */
//  public final void setLineHighlightColor(Color lineHighlightColor) {
//    this.lineHighlightColor = lineHighlightColor;
//    invalidateSelectedLines();
//  }


//  /**
//   * Returns true if line highlight is enabled, false otherwise.
//   */
//  public final boolean isLineHighlightEnabled() {
//    return lineHighlight;
//  }


  /**
   * Enables or disables current line highlighting.
   * @param lineHighlight True if current line highlight
   * should be enabled, false otherwise
   */
  public final void setLineHighlightEnabled(boolean lineHighlight) {
//    this.lineHighlight = lineHighlight;
    defaults.lineHighlight = lineHighlight;
    invalidateSelectedLines();
  }


//  /**
//   * Returns the bracket highlight color.
//   */
//  public final Color getBracketHighlightColor() {
//    return bracketHighlightColor;
//  }


//  /**
//   * Sets the bracket highlight color.
//   * @param bracketHighlightColor The bracket highlight color
//   */
//  public final void setBracketHighlightColor(Color bracketHighlightColor) {
//    this.bracketHighlightColor = bracketHighlightColor;
//    invalidateLine(textArea.getBracketLine());
//  }


  /**
   * Returns true if bracket highlighting is enabled, false otherwise.
   * When bracket highlighting is enabled, the bracket matching the
   * one before the caret (if any) is highlighted.
   */
  public final boolean isBracketHighlightEnabled() {
//    return bracketHighlight;
    return defaults.bracketHighlight;
  }


//  /**
//   * Enables or disables bracket highlighting.
//   * When bracket highlighting is enabled, the bracket matching the
//   * one before the caret (if any) is highlighted.
//   * @param bracketHighlight True if bracket highlighting should be
//   * enabled, false otherwise
//   */
//  public final void setBracketHighlightEnabled(boolean bracketHighlight) {
//    this.bracketHighlight = bracketHighlight;
//    invalidateLine(textArea.getBracketLine());
//  }


  /**
   * Returns true if the caret should be drawn as a block, false otherwise.
   */
  public final boolean isBlockCaretEnabled() {
    return defaults.blockCaret;
  }


//  /**
//   * Sets if the caret should be drawn as a block, false otherwise.
//   * @param blockCaret True if the caret should be drawn as a block,
//   * false otherwise.
//   */
//  public final void setBlockCaretEnabled(boolean blockCaret) {
//    this.blockCaret = blockCaret;
//    invalidateSelectedLines();
//  }


//  /**
//   * Returns the EOL marker color.
//   */
//  public final Color getEOLMarkerColor() {
//    return eolMarkerColor;
//  }


//  /**
//   * Sets the EOL marker color.
//   * @param eolMarkerColor The EOL marker color
//   */
//  public final void setEOLMarkerColor(Color eolMarkerColor) {
//    this.eolMarkerColor = eolMarkerColor;
//    repaint();
//  }


//  /**
//   * Returns true if EOL markers are drawn, false otherwise.
//   */
//  public final boolean getEOLMarkersPainted() {
//    return eolMarkers;
//  }


//  /**
//   * Sets if EOL markers are to be drawn.
//   * @param eolMarkers True if EOL markers should be drawn, false otherwise
//   */
//  public final void setEOLMarkersPainted(boolean eolMarkers) {
//    this.eolMarkers = eolMarkers;
//    repaint();
//  }


//  public final void setAntialias(boolean antialias) {
//    this.antialias = antialias;
//  }


//  /**
//   * Adds a custom highlight painter.
//   * @param highlight The highlight
//   */
//  public void addCustomHighlight(Highlight highlight) {
//    highlight.init(textArea,highlights);
//    highlights = highlight;
//  }


  /**
   * Highlight interface.
   */
  public interface Highlight {
    /**
     * Called after the highlight painter has been added.
     * @param textArea The text area
     * @param next The painter this one should delegate to
     */
    void init(JEditTextArea textArea, Highlight next);

    /**
     * This should paint the highlight and delgate to the
     * next highlight painter.
     * @param gfx The graphics context
     * @param line The line number
     * @param y The y co-ordinate of the line
     */
    void paintHighlight(Graphics gfx, int line, int y);

    /**
     * Returns the tool tip to display at the specified
     * location. If this highlighter doesn't know what to
     * display, it should delegate to the next highlight
     * painter.
     * @param evt The mouse event
     */
    String getToolTipText(MouseEvent evt);
  }


//  /**
//   * Returns the tool tip to display at the specified location.
//   * @param evt The mouse event
//   */
//  public String getToolTipText(MouseEvent evt) {
//    return (highlights == null) ? null : highlights.getToolTipText(evt);
//  }


  /** Returns the font metrics used by this component. */
  public FontMetrics getFontMetrics() {
    return fm;
  }


  public FontMetrics getFontMetrics(SyntaxStyle style) {
//    return getFontMetrics(style.isBold() ?
//                          defaults.boldFont : defaults.plainFont);
    return getFontMetrics(style.isBold() ? boldFont : plainFont);
  }


  // fry [160806 for 3.2]
  public int getLineHeight() {
    return fm.getHeight() + fm.getDescent();
  }


//  /**
//   * Sets the font for this component. This is overridden to update the
//   * cached font metrics and to recalculate which lines are visible.
//   * @param font The font
//   */
//  public void setFont(Font font) {
////    new Exception().printStackTrace(System.out);
//    super.setFont(font);
//    fm = super.getFontMetrics(font);
//    textArea.recalculateVisibleLines();
//  }


  /**
   * Repaints the text.
   * @param gfx The graphics context
   */
  public void paint(Graphics gfx) {
    Graphics2D g2 = (Graphics2D) gfx;
    g2.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING,
                        antialias ?
                        RenderingHints.VALUE_TEXT_ANTIALIAS_ON :
                        RenderingHints.VALUE_TEXT_ANTIALIAS_OFF);

    // no effect, one way or the other
//    g2.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS,
//                        RenderingHints.VALUE_FRACTIONALMETRICS_ON);

    Rectangle clipRect = gfx.getClipBounds();

    gfx.setColor(getBackground());
    gfx.fillRect(clipRect.x, clipRect.y, clipRect.width, clipRect.height);

    // We don't use yToLine() here because that method doesn't
    // return lines past the end of the document
    int height = fm.getHeight();
    int firstLine = textArea.getFirstLine();
    int firstInvalid = firstLine + clipRect.y / height;
    // Because the clipRect's height is usually an even multiple
    // of the font height, we subtract 1 from it, otherwise one
    // too many lines will always be painted.
    int lastInvalid = firstLine + (clipRect.y + clipRect.height - 1) / height;

    try {
      TokenMarkerState tokenMarker = textArea.getDocument().getTokenMarker();
      int x = textArea.getHorizontalOffset();

      for (int line = firstInvalid; line <= lastInvalid; line++) {
        paintLine(gfx, line, x, tokenMarker);
      }

      if (tokenMarker != null && tokenMarker.isNextLineRequested()) {
        int h = clipRect.y + clipRect.height;
        repaint(0, h, getWidth(), getHeight() - h);
      }
    } catch (Exception e) {
      System.err.println("Error repainting line" +
                         " range {" + firstInvalid + "," + lastInvalid + "}:");
      e.printStackTrace();
    }
  }


  /**
   * Marks a line as needing a repaint.
   * @param line The line to invalidate
   */
  final public void invalidateLine(int line) {
    repaint(0, textArea.lineToY(line) + fm.getMaxDescent() + fm.getLeading(),
            getWidth(), fm.getHeight());
  }


  /**
   * Marks a range of lines as needing a repaint.
   * @param firstLine The first line to invalidate
   * @param lastLine The last line to invalidate
   */
  final void invalidateLineRange(int firstLine, int lastLine) {
    repaint(0,textArea.lineToY(firstLine) +
            fm.getMaxDescent() + fm.getLeading(),
            getWidth(),(lastLine - firstLine + 1) * fm.getHeight());
  }


  /** Repaints the lines containing the selection. */
  final void invalidateSelectedLines() {
    invalidateLineRange(textArea.getSelectionStartLine(),
                        textArea.getSelectionStopLine());
  }


  /** Returns next tab stop after a specified point. */
//  TabExpander tabExpander = new TabExpander() {
  @Override
  public float nextTabStop(float x, int tabOffset) {
    int offset = textArea.getHorizontalOffset();
    int ntabs = ((int)x - offset) / tabSize;
    return (ntabs + 1) * tabSize + offset;
  }
//  };


  // do we go here? do will kill tabs?
//  public float nextTabStop(float x, int tabOffset) {
//    return x;
//  }


  public Dimension getPreferredSize() {
    return new Dimension(fm.charWidth('w') * defaults.cols,
                         fm.getHeight() * defaults.rows);
  }


  public Dimension getMinimumSize() {
    return getPreferredSize();
  }


  /**
   * Accessor used by tools that want to hook in and grab the formatting.
   */
  public int getCurrentLineIndex() {
    return currentLineIndex;
  }


  /**
   * Accessor used by tools that want to hook in and grab the formatting.
   */
  public void setCurrentLineIndex(int what) {
    currentLineIndex = what;
  }


  /**
   * Accessor used by tools that want to hook in and grab the formatting.
   */
  public Token getCurrentLineTokens() {
    return currentLineTokens;
  }


  /**
   * Accessor used by tools that want to hook in and grab the formatting.
   */
  public void setCurrentLineTokens(Token tokens) {
    currentLineTokens = tokens;
  }


  /**
   * Accessor used by tools that want to hook in and grab the formatting.
   */
  public Segment getCurrentLine() {
    return currentLine;
  }


//  /** Old paintLine() method with kooky args order, kept around for X Mode. */
//  @Deprecated
//  protected void paintLine(Graphics gfx, TokenMarker tokenMarker,
//                           int line, int x) {
//    Font defaultFont = getFont();
//    Color defaultColor = getForeground();
  protected void paintLine(Graphics gfx, int line, int x,
                           TokenMarkerState tokenMarker) {
    currentLineIndex = line;
    int y = textArea.lineToY(line);

    if (tokenMarker == null) {
      //paintPlainLine(gfx, line, defaultFont, defaultColor, x, y);
      paintPlainLine(gfx, line, x, y);
    } else if (line >= 0 && line < textArea.getLineCount()) {
      //paintSyntaxLine(gfx, tokenMarker, line, defaultFont, defaultColor, x, y);
      paintSyntaxLine(gfx, line, x, y, tokenMarker);
    }
  }


//  protected void paintLine(Graphics gfx, int line, int x,
//                           TokenMarker tokenMarker) {
//    paintLine(gfx, tokenMarker, line, x);
//  }


//  protected void paintPlainLine(Graphics gfx, int line, Font defaultFont,
//                                Color defaultColor, int x, int y) {
  protected void paintPlainLine(Graphics gfx, int line, int x, int y) {
    paintHighlight(gfx,line,y);
    textArea.getLineText(line, currentLine);

//    gfx.setFont(plainFont);
//    gfx.setFont(defaultFont);
//    gfx.setColor(defaultColor);

    int x0 = x - textArea.getHorizontalOffset();

    y += fm.getHeight();
    // doesn't respect fixed width like it should
//    x = Utilities.drawTabbedText(currentLine, x, y, gfx, this, 0);
//    int w = fm.charWidth(' ');
    for (int i = 0; i < currentLine.count; i++) {
      gfx.drawChars(currentLine.array, currentLine.offset+i, 1, x, y);
      x = currentLine.array[currentLine.offset + i] == '\t' ?
          x0 + (int)nextTabStop(x - x0, i) :
          x + fm.charWidth(currentLine.array[currentLine.offset+i]);
      textArea.offsetToX(line, currentLine.offset + i);
    }

    // Draw characters via input method.
    if (compositionTextPainter != null &&
        compositionTextPainter.hasComposedTextLayout()) {
      compositionTextPainter.draw(gfx, defaults.lineHighlightColor);
    }
    if (defaults.eolMarkers) {
      gfx.setColor(defaults.eolMarkerColor);
      gfx.drawString(".", x, y);
    }
  }


//  protected void paintSyntaxLine(Graphics gfx, TokenMarker tokenMarker,
//                                 int line, Font defaultFont,
//                                 Color defaultColor, int x, int y) {
  protected void paintSyntaxLine(Graphics gfx, int line, int x, int y,
                                 TokenMarkerState tokenMarker) {
    textArea.getLineText(currentLineIndex, currentLine);
    currentLineTokens = tokenMarker.markTokens(currentLine, currentLineIndex);

//    gfx.setFont(plainFont);
    paintHighlight(gfx, line, y);

//    gfx.setFont(defaultFont);
//    gfx.setColor(defaultColor);
    y += fm.getHeight();
//    x = paintSyntaxLine(currentLine,
//                        currentLineTokens,
//                        defaults.styles, this, gfx, x, y);
    x = paintSyntaxLine(gfx, currentLine, x, y,
                        currentLineTokens,
                        defaults.styles);
    // Draw characters via input method.
    if (compositionTextPainter != null &&
        compositionTextPainter.hasComposedTextLayout()) {
      compositionTextPainter.draw(gfx, defaults.lineHighlightColor);
    }
    if (defaults.eolMarkers) {
      gfx.setColor(defaults.eolMarkerColor);
      gfx.drawString(".", x, y);
    }
  }


  /**
   * Paints the specified line onto the graphics context. Note that this
   * method munges the offset and count values of the segment.
   * @param line The line segment
   * @param tokens The token list for the line
   * @param styles The syntax style list
   * @param expander The tab expander used to determine tab stops. May
   * be null
   * @param gfx The graphics context
   * @param x The x co-ordinate
   * @param y The y co-ordinate
   * @return The x co-ordinate, plus the width of the painted string
   */
//  public int paintSyntaxLine(Segment line, Token tokens, SyntaxStyle[] styles,
//                             TabExpander expander, Graphics gfx,
//                             int x, int y) {
  protected int paintSyntaxLine(Graphics gfx, Segment line, int x, int y,
                                Token tokens, SyntaxStyle[] styles) {
//    Font defaultFont = gfx.getFont();
//    Color defaultColor = gfx.getColor();

    int x0 = x - textArea.getHorizontalOffset();

//    for (byte id = tokens.id; id != Token.END; tokens = tokens.next) {
    for (;;) {
      byte id = tokens.id;
      if (id == Token.END)
        break;

      int length = tokens.length;
      if (id == Token.NULL) {
//        if(!defaultColor.equals(gfx.getColor()))
//          gfx.setColor(defaultColor);
//        if(!defaultFont.equals(gfx.getFont()))
//          gfx.setFont(defaultFont);
        gfx.setColor(defaults.fgcolor);
        gfx.setFont(plainFont);
      } else {
        //styles[id].setGraphicsFlags(gfx,defaultFont);
        SyntaxStyle ss = styles[id];
        gfx.setColor(ss.getColor());
        gfx.setFont(ss.isBold() ? boldFont : plainFont);
      }
      line.count = length;  // huh? suspicious
      // doesn't respect mono metrics, insists on spacing w/ fractional or something
//      x = Utilities.drawTabbedText(line, x, y, gfx, this, 0);
//      gfx.drawChars(line.array, line.offset, line.count, x, y);
//      int w = fm.charWidth(' ');
      for (int i = 0; i < line.count; i++) {
        gfx.drawChars(line.array, line.offset+i, 1, x, y);
        x = line.array[line.offset + i] == '\t' ?
            x0 + (int)nextTabStop(x - x0, i) :
            x + fm.charWidth(line.array[line.offset+i]);
      }
      //x += fm.charsWidth(line.array, line.offset, line.count);
      //x += fm.charWidth(' ') * line.count;
      line.offset += length;

      tokens = tokens.next;
    }

    return x;
  }


  protected void paintHighlight(Graphics gfx, int line, int y) {
    if (line >= textArea.getSelectionStartLine() &&
        line <= textArea.getSelectionStopLine()) {
      paintLineHighlight(gfx, line, y);
    }

    if (highlights != null) {
      highlights.paintHighlight(gfx, line, y);
    }

    if (defaults.bracketHighlight && line == textArea.getBracketLine()) {
      paintBracketHighlight(gfx, line, y);
    }

    if (line == textArea.getCaretLine()) {
      paintCaret(gfx, line, y);
    }
  }


  protected void paintLineHighlight(Graphics gfx, int line, int y) {
    int height = fm.getHeight();
    y += fm.getLeading() + fm.getMaxDescent();

    int selectionStart = textArea.getSelectionStart();
    int selectionEnd = textArea.getSelectionStop();

    if (selectionStart == selectionEnd) {
      if (defaults.lineHighlight) {
        gfx.setColor(defaults.lineHighlightColor);
        gfx.fillRect(0, y, getWidth(), height);
      }
    } else {
      gfx.setColor(defaults.selectionColor);

      int selectionStartLine = textArea.getSelectionStartLine();
      int selectionEndLine = textArea.getSelectionStopLine();
      int lineStart = textArea.getLineStartOffset(line);

      int x1, x2;
      if (selectionStartLine == selectionEndLine) {
        x1 = textArea._offsetToX(line, selectionStart - lineStart);
        x2 = textArea._offsetToX(line, selectionEnd - lineStart);
      } else if(line == selectionStartLine) {
        x1 = textArea._offsetToX(line, selectionStart - lineStart);
        x2 = getWidth();
      } else if(line == selectionEndLine) {
        //x1 = 0;
        // hack from stendahl to avoid doing weird side selection thing
        x1 = textArea._offsetToX(line, 0);
        // attempt at getting the gutter too, but doesn't seem to work
        //x1 = textArea._offsetToX(line, -textArea.getHorizontalOffset());
        x2 = textArea._offsetToX(line, selectionEnd - lineStart);
      } else {
        //x1 = 0;
        // hack from stendahl to avoid doing weird side selection thing
        x1 = textArea._offsetToX(line, 0);
        // attempt at getting the gutter too, but doesn't seem to work
        //x1 = textArea._offsetToX(line, -textArea.getHorizontalOffset());
        x2 = getWidth();
      }

      // "inlined" min/max()
      gfx.fillRect(x1 > x2 ? x2 : x1,y,x1 > x2 ?
                   (x1 - x2) : (x2 - x1),height);
    }
  }


  protected void paintBracketHighlight(Graphics gfx, int line, int y) {
    int position = textArea.getBracketPosition();
    if (position != -1) {
      y += fm.getLeading() + fm.getMaxDescent();
      int x = textArea._offsetToX(line, position);
      gfx.setColor(defaults.bracketHighlightColor);
      // Hack!!! Since there is no fast way to get the character
      // from the bracket matching routine, we use ( since all
      // brackets probably have the same width anyway
      gfx.drawRect(x,y,fm.charWidth('(') - 1, fm.getHeight() - 1);
    }
  }


  protected void paintCaret(Graphics gfx, int line, int y) {
    //System.out.println("painting caret " + line + " " + y);
    if (textArea.isCaretVisible()) {
      //System.out.println("caret is visible");
      int offset =
        textArea.getCaretPosition() - textArea.getLineStartOffset(line);
      int caretX = textArea._offsetToX(line, offset);
      int caretWidth = ((defaults.blockCaret ||
                         textArea.isOverwriteEnabled()) ?
                        fm.charWidth('w') : 1);
      y += fm.getLeading() + fm.getMaxDescent();
      int height = fm.getHeight();

      //System.out.println("caretX, width = " + caretX + " " + caretWidth);

      gfx.setColor(defaults.caretColor);

      if (textArea.isOverwriteEnabled()) {
        gfx.fillRect(caretX, y + height - 1, caretWidth,1);

      } else {
        // some machines don't like the drawRect for the single
        // pixel caret.. this caused a lot of hell because on that
        // minority of machines, the caret wouldn't show up past
        // the first column. the fix is to use drawLine() in
        // those cases, as a workaround.
        if (caretWidth == 1) {
          gfx.drawLine(caretX, y, caretX, y + height - 1);
        } else {
          gfx.drawRect(caretX, y, caretWidth - 1, height - 1);
        }
        //gfx.drawRect(caretX, y, caretWidth, height - 1);
      }
    }
  }


  public int getScrollWidth() {
    // https://github.com/processing/processing/issues/3591
    return super.getWidth();
  }
}
