/******************************************************************
 * JADE - Java Agent DEvelopment Framework is a framework to develop
 * multi-agent systems in compliance with the FIPA specifications.
 * Copyright (C) 2002 TILAB S.p.A.
 *
 * This file is donated by Acklin B.V. to the JADE project.
 *
 *
 * GNU Lesser General Public License
 *
 * 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,
 * version 2.1 of the License.
 *
 * 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 jade.tools.gui;

import java.awt.*;
import javax.swing.JComponent;
import javax.swing.text.*;


/**
 *  The text area repaint manager. It performs double buffering and paints
 *  lines of text.The original file is written by Slava Pestov (www.gjt.org)
 *  and altered to fit ACL/SL.
 *
 * @author     Chris van Aart - Acklin B.V., the Netherlands & Slava Pestov
 * @created    June 14, 2002
 */
public class ACLTextAreaPainter extends JComponent implements TabExpander {
  /**
   *  Creates a new repaint manager. This should be not be called directly.
   *
   * @param  textArea  Description of Parameter
   */
  public ACLTextAreaPainter(ACLTextArea textArea) {
    this.textArea = textArea;

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

    firstInvalid = lastInvalid = -1;

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

    setFont(new Font("Dialog", Font.PLAIN, 11));
    setForeground(Color.black);
    setBackground(Color.white);

    styles = ACLSyntaxUtilities.getDefaultSyntaxStyles(this);
    cols = 80;
    rows = 5;
    caretColor = Color.red;
    selectionColor = new Color(0xccccff);
    lineHighlightColor = new Color(0xe0e0e0);
    ;
    lineHighlight = true;
    bracketHighlightColor = Color.black;
    bracketHighlight = true;
    eolMarkerColor = new Color(0x009999);
    eolMarkers = true;

    copyAreaBroken = true;
  }


  /**
   *  Returns if this component can be traversed by pressing the Tab key. This
   *  returns false.
   *
   * @return    The ManagingFocus value
   */
  public final boolean isManagingFocus() {
    return false;
  }


  /**
   *  Returns the syntax styles used to paint colorized text. Entry <i>n</i>
   *  will be used to paint tokens with id = <i>n</i> .
   *
   * @return    The Styles value
   * @see       org.gjt.sp.jedit.syntax.Token
   */
  public final ACLSytntaxStyle[] getStyles() {
    return styles;
  }


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


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


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


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


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


  /**
   *  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.
   *
   * @return    The BracketHighlightEnabled value
   */
  public final boolean isBracketHighlightEnabled() {
    return bracketHighlight;
  }


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


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


  /**
   *  Returns true if EOL markers are drawn, false otherwise.
   *
   * @return    The EOLMarkerEnabled value
   */
  public final boolean isEOLMarkerEnabled() {
    return eolMarkers;
  }


  /**
   *  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            org.gjt.sp.jedit.syntax.Token
   */
  public final void setStyles(ACLSytntaxStyle[] styles) {
    this.styles = styles;
    invalidateOffscreen();
    repaint();
  }


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


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


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


  /**
   *  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;
    invalidateSelectedLines();
  }


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


  /**
   *  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());
  }


  /**
   *  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();
  }


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


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


  /**
   *  Queues a repaint of the changed lines only.
   */
  public final void fastRepaint() {
    if (firstInvalid == -1 && lastInvalid == -1) {
      repaint();
    }

    else {
      repaint(0, textArea.lineToY(firstInvalid)
         + fm.getLeading() + fm.getMaxDescent(),
        getWidth(), (lastInvalid - firstInvalid + 1)
         * fm.getHeight());
    }

  }


  /**
   *  Repaints the specified line. This is equivalent to calling <code>_invalidateLine()</code>
   *  and <code>repaint()</code>.
   *
   * @param  line  The line
   * @see          #_invalidateLine(int)
   */
  public final void invalidateLine(int line) {
    _invalidateLine(line);
    fastRepaint();
  }


  /**
   *  Repaints the specified line range. This is equivalent to calling <code>_invalidateLineRange()</code>
   *  then <code>repaint()</code>.
   *
   * @param  firstLine  The first line to repaint
   * @param  lastLine   The last line to repaint
   */
  public final void invalidateLineRange(int firstLine, int lastLine) {
    _invalidateLineRange(firstLine, lastLine);
    fastRepaint();
  }


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


  /**
   *  Invalidates the offscreen graphics context. This should not be called
   *  directly.
   */
  public final void invalidateOffscreen() {
    offImg = null;
    offGfx = null;
  }


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


  /**
   *  Returns if the copyArea() should not be used.
   *
   * @return    The CopyAreaBroken value
   */
  public boolean isCopyAreaBroken() {
    return copyAreaBroken;
  }


  /**
   *  Returns the painter's preferred size.
   *
   * @return    The PreferredSize value
   */
  public Dimension getPreferredSize() {
    return super.getPreferredSize();
    /*
        Dimension dim = new Dimension();
        dim.width = fm.charWidth('w') * cols;
        dim.height = fm.getHeight() * rows;
        return dim;
      */
  }


  /**
   *  Returns the painter's minimum size.
   *
   * @return    The MinimumSize value
   */
  public Dimension getMinimumSize() {
    return super.getMinimumSize();
  }


  /**
   *  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) {
    super.setFont(font);
    fm = textArea.getFontMetrics(font);
    textArea.recalculateVisibleLines();
  }


  /**
   *  Disables the use of the copyArea() function (which is broken in JDK
   *  1.2).
   *
   * @param  copyAreaBroken  The new CopyAreaBroken value
   */
  public void setCopyAreaBroken(boolean copyAreaBroken) {
    this.copyAreaBroken = copyAreaBroken;
  }


  /**
   *  Paints any lines that changed since the last paint to the offscreen
   *  graphics, then repaints the offscreen to the specified graphics context.
   *
   * @param  g  The graphics context
   */
  public void update(Graphics g) {
    tabSize = fm.charWidth('w') * ((Integer)textArea.getDocument().getProperty(
      PlainDocument.tabSizeAttribute)).intValue();

    // returns true if offscreen was created. When it's created,
    // all lines, not just the invalid ones, need to be painted.
    if (ensureOffscreenValid()) {
      firstInvalid = textArea.getFirstLine();
      lastInvalid = firstInvalid + textArea.getVisibleLines();
    }

    if (firstInvalid != -1 && lastInvalid != -1) {
      int lineCount;
      try {
        if (firstInvalid == lastInvalid) {
          lineCount = offscreenRepaintLine(firstInvalid,
            textArea.getHorizontalOffset());
        }

        else {
          lineCount = offscreenRepaintLineRange(
            firstInvalid, lastInvalid);
        }

        if (lastInvalid - firstInvalid + 1 != lineCount) {
          // XXX stupid hack
          Rectangle clip = g.getClipBounds();
          if (!clip.equals(getBounds())) {
            repaint();
          }

        }
      }
      catch (Exception e) {
        System.err.println("Error repainting line"
           + " range {" + firstInvalid + ","
           + lastInvalid + "}:");
        e.printStackTrace();
      }
      firstInvalid = lastInvalid = -1;
    }

    g.drawImage(offImg, 0, 0, null);
  }


  /**
   *  Same as <code>update(g)</code>.
   *
   * @param  g  Description of Parameter
   */
  public void paint(Graphics g) {
    update(g);
  }


  /**
   *  Marks a line as needing a repaint, but doesn't actually repaint it until
   *  <code>repaint()</code> is called manually.
   *
   * @param  line  The line to invalidate
   */
  public void _invalidateLine(int line) {
    int firstVisible = textArea.getFirstLine();
    int lastVisible = firstVisible + textArea.getVisibleLines();

    if (line < firstVisible || line > lastVisible) {
      return;
    }

    if (line >= firstInvalid && line <= lastInvalid) {
      return;
    }

    if (firstInvalid == -1 && lastInvalid == -1) {
      firstInvalid = lastInvalid = line;
    }

    else {
      firstInvalid = Math.min(line, firstInvalid);
      lastInvalid = Math.max(line, lastInvalid);
    }
  }


  /**
   *  Marks a range of lines as needing a repaint, but doesn't actually
   *  repaint them until <code>repaint()</code> is called.
   *
   * @param  firstLine  The first line to invalidate
   * @param  lastLine   The last line to invalidate
   */
  public void _invalidateLineRange(int firstLine, int lastLine) {
    int firstVisible = textArea.getFirstLine();
    int lastVisible = firstVisible + textArea.getVisibleLines();

    if (firstLine > lastLine) {
      int tmp = firstLine;
      firstLine = lastLine;
      lastLine = tmp;
    }

    if (lastLine < firstVisible || firstLine > lastVisible) {
      return;
    }

    if (firstInvalid == -1 && lastInvalid == -1) {
      firstInvalid = firstLine;
      lastInvalid = lastLine;
    }
    else {
      if (firstLine >= firstInvalid && lastLine <= lastInvalid) {
        return;
      }

      firstInvalid = Math.min(firstInvalid, firstLine);
      lastInvalid = Math.max(lastInvalid, lastLine);
    }

    firstInvalid = Math.max(firstInvalid, firstVisible);
    lastInvalid = Math.min(lastInvalid, lastVisible);
  }


  /**
   *  Simulates scrolling from <code>oldFirstLine</code> to <code>newFirstLine</code>
   *  by shifting the offscreen graphics and repainting any revealed lines.
   *  This should not be called directly; use <code>JEditTextArea.setFirstLine()</code>
   *  instead.
   *
   * @param  oldFirstLine  The old first line
   * @param  newFirstLine  The new first line
   * @see                  org.gjt.sp.jedit.textarea.JEditTextArea#setFirstLine(int)
   */
  public void scrollRepaint(int oldFirstLine, int newFirstLine) {
    if (offGfx == null) {
      return;
    }

    int visibleLines = textArea.getVisibleLines();

    // No point doing this crap if the user scrolled by >= visibleLines
    if (copyAreaBroken || oldFirstLine + visibleLines <= newFirstLine
       || newFirstLine + visibleLines <= oldFirstLine) {
      _invalidateLineRange(newFirstLine, newFirstLine + visibleLines + 1);
    }

    else {
      int y = fm.getHeight() * (oldFirstLine - newFirstLine);
      offGfx.copyArea(0, 0, offImg.getWidth(this), offImg.getHeight(this), 0, y);

      if (oldFirstLine < newFirstLine) {
        _invalidateLineRange(oldFirstLine + visibleLines - 1,
          newFirstLine + visibleLines + 1);
      }

      else {
        _invalidateLineRange(newFirstLine, oldFirstLine);
      }

    }
  }


  /**
   *  Implementation of TabExpander interface. Returns next tab stop after a
   *  specified point.
   *
   * @param  x          The x co-ordinate
   * @param  tabOffset  Ignored
   * @return            The next tab stop after <i>x</i>
   */
  public float nextTabStop(float x, int tabOffset) {
    int offset = textArea.getHorizontalOffset();
    int ntabs = ((int)x - offset) / tabSize;
    return (ntabs + 1) * tabSize + offset;
  }


  protected boolean ensureOffscreenValid() {
    if (offImg == null || offGfx == null) {
      offImg = textArea.createImage(getWidth(), getHeight());
      offGfx = offImg.getGraphics();
      return true;
    }
    else {
      return false;
    }
  }


  protected int offscreenRepaintLineRange(int firstLine, int lastLine) {
    if (offGfx == null) {
      return 0;
    }

    int x = textArea.getHorizontalOffset();

    int line;
    for (line = firstLine; line <= lastLine; ) {
      line += offscreenRepaintLine(line, x);
    }

    return line - firstLine;
  }


  protected int offscreenRepaintLine(int line, int x) {
    ACLSLTokenMarker tokenMarker = textArea.getTokenMarker();
    Font defaultFont = getFont();
    Color defaultColor = getForeground();

    int y = textArea.lineToY(line);

    if (line < 0 || line >= textArea.getLineCount()) {
      paintHighlight(line, y);
      styles[ACLToken.INVALID].setGraphicsFlags(offGfx, defaultFont);
      offGfx.drawString(".", 0, y + fm.getHeight());
      return 1;
    }

    if (tokenMarker == null) {
      currentLineIndex = line;
      paintPlainLine(line, defaultFont, defaultColor, x, y);
      return 1;
    }
    else {
      int count = 0;
      int lastVisibleLine = Math.min(textArea.getLineCount(),
        textArea.getFirstLine() + textArea.getVisibleLines());
      do {
        currentLineIndex = line + count;
        paintSyntaxLine(tokenMarker, currentLineIndex,
          defaultFont, defaultColor, x, y);
        y += fm.getHeight();

        count++;
      } while (tokenMarker.isNextLineRequested()
         && line + count < lastVisibleLine);
      return count;
    }
  }


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

    offGfx.setFont(defaultFont);
    offGfx.setColor(defaultColor);

    y += fm.getHeight();
    x = Utilities.drawTabbedText(currentLine, x, y, offGfx, this, 0);

    if (eolMarkers) {
      offGfx.setColor(eolMarkerColor);
      offGfx.drawString(".", x, y);
    }
  }


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

    paintHighlight(line, y);

    offGfx.setFont(defaultFont);
    offGfx.setColor(defaultColor);
    styles = ACLSyntaxUtilities.getDefaultSyntaxStyles(this);
    y += fm.getHeight();
    x = ACLSyntaxUtilities.paintSyntaxLine(currentLine,
      currentLineTokens, styles, this, offGfx, x, y);

    if (eolMarkers) {
      offGfx.setColor(eolMarkerColor);
      offGfx.drawString(".", x, y);
    }
  }


  protected void paintHighlight(int line, int y) {
    /*
        Clear the line's bounding rectangle
      */
    int gap = fm.getMaxDescent() + fm.getLeading();
    offGfx.setColor(getBackground());
    offGfx.fillRect(0, y + gap, offImg.getWidth(this), fm.getHeight());

    if (line >= textArea.getSelectionStartLine()
       && line <= textArea.getSelectionEndLine()) {
      paintLineHighlight(line, y);
    }

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

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

  }


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

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

    if (selectionStart == selectionEnd) {
      if (lineHighlight) {
        offGfx.setColor(lineHighlightColor);
        offGfx.fillRect(0, y, offImg.getWidth(this), height);
      }

      else {
        offGfx.setColor(selectionColor);

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

        int x1;

        int 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 = offImg.getWidth(this);
        }
        else if (line == selectionEndLine) {
          x1 = 0;
          x2 = textArea.offsetToX(line,
            selectionEnd - lineStart);
        }
        else {
          x1 = 0;
          x2 = offImg.getWidth(this);
        }

        offGfx.fillRect(x1, y, x2 - x1, height);
      }
    }

  }


  protected void paintBracketHighlight(int line, int y) {
    int position = textArea.getBracketPosition();
    if (position == -1) {
      return;
    }
    y += fm.getLeading() + fm.getMaxDescent();
    int x = textArea.offsetToX(line, position);
    offGfx.setColor(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
    offGfx.drawRect(x, y, fm.charWidth('(') - 1,
      fm.getHeight() - 1);
  }


  protected void paintCaret(int line, int y) {
    if (textArea.isCaretVisible()) {
      int offset = textArea.getCaretPosition()
         - textArea.getLineStartOffset(line);
      int caretX = textArea.offsetToX(line, offset);
      int caretWidth = ((blockCaret ||
        textArea.isOverwriteEnabled()) ?
        fm.charWidth('w') : 1);
      y += fm.getLeading() + fm.getMaxDescent();
      int height = fm.getHeight();

      offGfx.setColor(caretColor);

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

      else {
        offGfx.drawRect(caretX, y, caretWidth - 1, height - 1);
      }

    }
  }


  protected static boolean copyAreaBroken;

  // protected members
  protected ACLTextArea textArea;

  protected ACLSytntaxStyle[] styles;
  protected Color caretColor;
  protected Color selectionColor;
  protected Color lineHighlightColor;
  protected Color bracketHighlightColor;
  protected Color eolMarkerColor;

  protected boolean blockCaret;
  protected boolean lineHighlight;
  protected boolean bracketHighlight;
  protected boolean eolMarkers;
  protected int cols;
  protected int rows;

  protected int tabSize;
  protected FontMetrics fm;
  protected Graphics offGfx;
  protected Image offImg;

  protected int firstInvalid;
  protected int lastInvalid;

  // package-private members
  int currentLineIndex;
  ACLToken currentLineTokens;
  Segment currentLine;
}
//  ***EOF***
