package util.ui.beanshell;

import java.awt.Color;
import java.util.HashSet;

import javax.swing.text.AttributeSet;
import javax.swing.text.BadLocationException;
import javax.swing.text.DefaultEditorKit;
import javax.swing.text.DefaultStyledDocument;
import javax.swing.text.Element;
import javax.swing.text.MutableAttributeSet;
import javax.swing.text.SimpleAttributeSet;
import javax.swing.text.StyleConstants;

/**
 * Syntax-Highlighting for the BeanShell-Editor
 */
class SyntaxDocument extends DefaultStyledDocument
{
  private DefaultStyledDocument doc;
  private Element rootElement;

  private boolean multiLineComment;
  private MutableAttributeSet normal;
  private MutableAttributeSet keyword;
  private MutableAttributeSet comment;
  private MutableAttributeSet quote;

  private HashSet<String> keywords;

  public SyntaxDocument()
  {
    doc = this;
    rootElement = doc.getDefaultRootElement();
    putProperty( DefaultEditorKit.EndOfLineStringProperty, "\n" );

    normal = new SimpleAttributeSet();
    StyleConstants.setForeground(normal, Color.black);

    comment = new SimpleAttributeSet();
    StyleConstants.setForeground(comment, Color.gray);
    StyleConstants.setItalic(comment, true);

    keyword = new SimpleAttributeSet();
    StyleConstants.setForeground(keyword, Color.blue);

    quote = new SimpleAttributeSet();
    StyleConstants.setForeground(quote, Color.red);

    keywords = new HashSet<String>();
    keywords.add( "abstract" );
    keywords.add( "boolean" );
    keywords.add( "break" );
    keywords.add( "byte" );
    keywords.add( "byvalue" );
    keywords.add( "case" );
    keywords.add( "cast" );
    keywords.add( "catch" );
    keywords.add( "char" );
    keywords.add( "class" );
    keywords.add( "const" );
    keywords.add( "continue" );
    keywords.add( "default" );
    keywords.add( "do" );
    keywords.add( "double" );
    keywords.add( "else" );
    keywords.add( "extends" );
    keywords.add( "false" );
    keywords.add( "final" );
    keywords.add( "finally" );
    keywords.add( "float" );
    keywords.add( "for" );
    keywords.add( "future" );
    keywords.add( "generic" );
    keywords.add( "goto" );
    keywords.add( "if" );
    keywords.add( "implements" );
    keywords.add( "import" );
    keywords.add( "inner" );
    keywords.add( "instanceof" );
    keywords.add( "int" );
    keywords.add( "interface" );
    keywords.add( "long" );
    keywords.add( "native" );
    keywords.add( "new" );
    keywords.add( "null" );
    keywords.add( "operator" );
    keywords.add( "outer" );
    keywords.add( "package" );
    keywords.add( "private" );
    keywords.add( "protected" );
    keywords.add( "public" );
    keywords.add( "rest" );
    keywords.add( "return" );
    keywords.add( "short" );
    keywords.add( "static" );
    keywords.add( "super" );
    keywords.add( "switch" );
    keywords.add( "synchronized" );
    keywords.add( "this" );
    keywords.add( "throw" );
    keywords.add( "throws" );
    keywords.add( "transient" );
    keywords.add( "true" );
    keywords.add( "try" );
    keywords.add( "var" );
    keywords.add( "void" );
    keywords.add( "volatile" );
    keywords.add( "while" );
  }

  /*
   *  Override to apply syntax highlighting after the document has been updated
   */
  public void insertString(int offset, String str, AttributeSet a) throws BadLocationException
  {
    super.insertString(offset, str, a);
    processChangedLines(offset, str.length());
  }

  /*
   *  Override to apply syntax highlighting after the document has been updated
   */
  public void remove(int offset, int length) throws BadLocationException
  {
    super.remove(offset, length);
    processChangedLines(offset, 0);
  }

  /*
   *  Determine how many lines have been changed,
   *  then apply highlighting to each line
   */
  public void processChangedLines(int offset, int length)
    throws BadLocationException
  {
    String content = doc.getText(0, doc.getLength());

    //  The lines affected by the latest document update

    int startLine = rootElement.getElementIndex( offset );
    int endLine = rootElement.getElementIndex( offset + length );

    //  Make sure all comment lines prior to the start line are commented
    //  and determine if the start line is still in a multi line comment

    setMultiLineComment( commentLinesBefore( content, startLine ) );

    //  Do the actual highlighting

    for (int i = startLine; i <= endLine; i++)
    {
      applyHighlighting(content, i);
    }

    //  Resolve highlighting to the next end multi line delimiter

    if (isMultiLineComment()) {
      commentLinesAfter(content, endLine);
    } else {
      highlightLinesAfter(content, endLine);
    }
  }

  /*
   *  Highlight lines when a multi line comment is still 'open'
   *  (ie. matching end delimiter has not yet been encountered)
   */
  private boolean commentLinesBefore(String content, int line)
  {
    int offset = rootElement.getElement( line ).getStartOffset();

    //  Start of comment not found, nothing to do

    int startDelimiter = lastIndexOf( content, getStartDelimiter(), offset - 2 );

    if (startDelimiter < 0) {
      return false;
    }

    //  Matching start/end of comment found, nothing to do

    int endDelimiter = indexOf( content, getEndDelimiter(), startDelimiter );

    if (endDelimiter < offset && endDelimiter != -1) {
      return false;
    }

    //  End of comment not found, highlight the lines

    doc.setCharacterAttributes(startDelimiter, offset - startDelimiter + 1, comment, false);
    return true;
  }

  /*
   *  Highlight comment lines to matching end delimiter
   */
  private void commentLinesAfter(String content, int line)
  {
    int offset = rootElement.getElement( line ).getEndOffset();

    //  End of comment not found, nothing to do

    int endDelimiter = indexOf( content, getEndDelimiter(), offset );

    if (endDelimiter < 0) {
      return;
    }

    //  Matching start/end of comment found, comment the lines

    int startDelimiter = lastIndexOf( content, getStartDelimiter(), endDelimiter );

    if (startDelimiter < 0 || startDelimiter <= offset)
    {
      doc.setCharacterAttributes(offset, endDelimiter - offset + 1, comment, false);
    }
  }

  /*
   *  Highlight lines to start or end delimiter
   */
  private void highlightLinesAfter(String content, int line)
    throws BadLocationException
  {
    int offset = rootElement.getElement( line ).getEndOffset();

    //  Start/End delimiter not found, nothing to do

    int startDelimiter = indexOf( content, getStartDelimiter(), offset );
    int endDelimiter = indexOf( content, getEndDelimiter(), offset );

    if (startDelimiter < 0) {
      startDelimiter = content.length();
    }

    if (endDelimiter < 0) {
      endDelimiter = content.length();
    }

    int delimiter = Math.min(startDelimiter, endDelimiter);

    if (delimiter < offset) {
      return;
    }

    //  Start/End delimiter found, reapply highlighting

    int endLine = rootElement.getElementIndex( delimiter );

    for (int i = line + 1; i < endLine; i++)
    {
      Element branch = rootElement.getElement( i );
      Element leaf = doc.getCharacterElement( branch.getStartOffset() );
      AttributeSet as = leaf.getAttributes();

      if ( as.isEqual(comment) ) {
        applyHighlighting(content, i);
      }
    }
  }

  /*
   *  Parse the line to determine the appropriate highlighting
   */
  private void applyHighlighting(String content, int line)
    throws BadLocationException
  {
    int startOffset = rootElement.getElement( line ).getStartOffset();
    int endOffset = rootElement.getElement( line ).getEndOffset() - 1;

    int lineLength = endOffset - startOffset;
    int contentLength = content.length();

    if (endOffset >= contentLength) {
      endOffset = contentLength - 1;
    }

    //  check for multi line comments
    //  (always set the comment attribute for the entire line)

    if (endingMultiLineComment(content, startOffset, endOffset)
    ||  isMultiLineComment()
    ||  startingMultiLineComment(content, startOffset, endOffset) )
    {
      doc.setCharacterAttributes(startOffset, endOffset - startOffset + 1, comment, false);
      return;
    }

    //  set normal attributes for the line

    doc.setCharacterAttributes(startOffset, lineLength, normal, true);

    //  check for single line comment

    int index = content.indexOf(getSingleLineDelimiter(), startOffset);

    if ( (index > -1) && (index < endOffset) )
    {
      doc.setCharacterAttributes(index, endOffset - index + 1, comment, false);
      endOffset = index - 1;
    }

    //  check for tokens

    checkForTokens(content, startOffset, endOffset);
  }

  /*
   *  Does this line contain the start delimiter
   */
  private boolean startingMultiLineComment(String content, int startOffset, int endOffset)
    throws BadLocationException
  {
    int index = indexOf( content, getStartDelimiter(), startOffset );

    if ( (index < 0) || (index > endOffset) ) {
      return false;
    } else
    {
      setMultiLineComment( true );
      return true;
    }
  }

  /*
   *  Does this line contain the end delimiter
   */
  private boolean endingMultiLineComment(String content, int startOffset, int endOffset)
    throws BadLocationException
  {
    int index = indexOf( content, getEndDelimiter(), startOffset );

    if ( (index < 0) || (index > endOffset) ) {
      return false;
    } else
    {
      setMultiLineComment( false );
      return true;
    }
  }

  /*
   *  We have found a start delimiter
   *  and are still searching for the end delimiter
   */
  private boolean isMultiLineComment()
  {
    return multiLineComment;
  }

  private void setMultiLineComment(boolean value)
  {
    multiLineComment = value;
  }

  /*
   *  Parse the line for tokens to highlight
   */
  private void checkForTokens(String content, int startOffset, int endOffset)
  {
    while (startOffset <= endOffset)
    {
      //  skip the delimiters to find the start of a new token

      while ( isDelimiter( content.substring(startOffset, startOffset + 1) ) )
      {
        if (startOffset < endOffset) {
          startOffset++;
        } else {
          return;
        }
      }

      //  Extract and process the entire token

      if ( isQuoteDelimiter( content.substring(startOffset, startOffset + 1) ) ) {
        startOffset = getQuoteToken(content, startOffset, endOffset);
      } else {
        startOffset = getOtherToken(content, startOffset, endOffset);
      }
    }
  }

  /*
   *
   */
  private int getQuoteToken(String content, int startOffset, int endOffset)
  {
    String quoteDelimiter = content.substring(startOffset, startOffset + 1);
    String escapeString = getEscapeString(quoteDelimiter);

    int index;
    int endOfQuote = startOffset;

    //  skip over the escape quotes in this quote

    index = content.indexOf(escapeString, endOfQuote + 1);

    while ( (index > -1) && (index < endOffset) )
    {
      endOfQuote = index + 1;
      index = content.indexOf(escapeString, endOfQuote);
    }

    // now find the matching delimiter

    index = content.indexOf(quoteDelimiter, endOfQuote + 1);

    if ( (index < 0) || (index > endOffset) ) {
      endOfQuote = endOffset;
    } else {
      endOfQuote = index;
    }

    doc.setCharacterAttributes(startOffset, endOfQuote - startOffset + 1, quote, false);

    return endOfQuote + 1;
  }

  /*
   *
   */
  private int getOtherToken(String content, int startOffset, int endOffset)
  {
    int endOfToken = startOffset + 1;

    while ( endOfToken <= endOffset )
    {
      if ( isDelimiter( content.substring(endOfToken, endOfToken + 1) ) ) {
        break;
      }

      endOfToken++;
    }

    String token = content.substring(startOffset, endOfToken);

    if ( isKeyword( token ) )
    {
      doc.setCharacterAttributes(startOffset, endOfToken - startOffset, keyword, false);
    }

    return endOfToken + 1;
  }

  /*
   *  Assume the needle will be found at the start/end of the line
   */
  private int indexOf(String content, String needle, int offset)
  {
    int index;

    while ( (index = content.indexOf(needle, offset)) != -1 )
    {
      String text = getLine( content, index ).trim();

      if (text.startsWith(needle) || text.endsWith(needle)) {
        break;
      } else {
        offset = index + 1;
      }
    }

    return index;
  }

  /*
   *  Assume the needle will be found at the start/end of the line
   */
  private int lastIndexOf(String content, String needle, int offset)
  {
    int index;

    while ( (index = content.lastIndexOf(needle, offset)) != -1 )
    {
      String text = getLine( content, index ).trim();

      if (text.startsWith(needle) || text.endsWith(needle)) {
        break;
      } else {
        offset = index - 1;
      }
    }

    return index;
  }

  private String getLine(String content, int offset)
  {
    int line = rootElement.getElementIndex( offset );
    Element lineElement = rootElement.getElement( line );
    int start = lineElement.getStartOffset();
    int end = lineElement.getEndOffset();
    return content.substring(start, end - 1);
  }

  /*
   *  Override for other languages
   */
  protected boolean isDelimiter(String character)
  {
    String operands = ";:{}()[]+-/%<=>!&|^~*";

    if (Character.isWhitespace( character.charAt(0) ) ||
      operands.indexOf(character) != -1 ) {
      return true;
    } else {
      return false;
    }
  }

  /*
   *  Override for other languages
   */
  protected boolean isQuoteDelimiter(String character)
  {
    String quoteDelimiters = "\"'";

    if (quoteDelimiters.indexOf(character) < 0) {
      return false;
    } else {
      return true;
    }
  }

  /*
   *  Override for other languages
   */
  protected boolean isKeyword(String token)
  {
    return keywords.contains( token );
  }

  /*
   *  Override for other languages
   */
  protected String getStartDelimiter()
  {
    return "/*";
  }

  /*
   *  Override for other languages
   */
  protected String getEndDelimiter()
  {
    return "*/";
  }

  /*
   *  Override for other languages
   */
  protected String getSingleLineDelimiter()
  {
    return "//";
  }

  /*
   *  Override for other languages
   */
  protected String getEscapeString(String quoteDelimiter)
  {
    return "\\" + quoteDelimiter;
  }

}
