//----------------------------------------------------------------------------
// COMPONENT NAME: LPEX Editor
//
// © Copyright IBM Corporation 2006, 2007
// All Rights Reserved.
//
// DESCRIPTION:
// NestingCommand - sample user-defined command (nesting)
//----------------------------------------------------------------------------

package com.ibm.lpex.samples;

import java.util.ArrayList;
import java.util.HashMap;

import com.ibm.lpex.core.LpexCommand;
import com.ibm.lpex.core.LpexDocumentLocation;
import com.ibm.lpex.core.LpexMatch;
import com.ibm.lpex.core.LpexMarkListener;
import com.ibm.lpex.core.LpexView;
import com.ibm.lpex.core.LpexViewAdapter;
import com.ibm.lpex.core.LpexWindow;

import org.eclipse.swt.SWT;
import org.eclipse.swt.events.DisposeEvent;
import org.eclipse.swt.events.DisposeListener;
import org.eclipse.swt.events.PaintEvent;
import org.eclipse.swt.events.PaintListener;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.widgets.Composite;

/**
 * Sample command <b>nesting</b> - display block limits in the source.
 * This is an example of custom drawing in the prefix area margin.  Run this
 * command with the cursor positioned on a block-delimiter bracket (for example,
 * '{' or '}' in a C/C++ source document) to display the limits of the included
 * blocks (up to a given level) as defined by the selected bracket.
 *
 * <p>Here is the NestingCommand <a href="doc-files/NestingCommand.java.html">source
 * code</a>.</p>
 *
 * <p>To run this sample:
 * <ul>
 *  <li>Define the command via an editor preference page, where available, or
 *   from the editor command line:
 *    <pre>set commandClass.nesting com.ibm.lpex.samples.NestingCommand</pre></li>
 *  <li>Run it from the editor command line:
 *    <pre>nesting [on | off]</pre></li>
 * </ul></p>
 *
 * @see com.ibm.lpex.samples All the samples
 */
public class NestingCommand implements LpexCommand
{
 /**
  * Runs this command.
  * Establishes the bounds of the blocks to display, displays the nestings
  * in the prefix-area margin.
  *
  * @param lpexView the document view in which the command was issued
  * @param parameters optional parameter "on" / "off" / "?"
  */
 public boolean doCommand(LpexView lpexView, String parameters)
 {
  if (lpexView != null)
   {
    parameters = parameters.trim();
    if (parameters.length() != 0)
     {
      if ("off".equals(parameters))
       {
        Nester.uninstall(lpexView);
        return true;
       }
      if ("?".equals(parameters)) // command help
       {
        lpexView.doCommand("set messageText Syntax: nesting [on | off]");
        return true;
       }
      if (!"on".equals(parameters))
       {
        lpexView.doCommand("set messageText " + parameters +
                           " is not a valid parameter for the \"nesting\" command.");
        return false;
       }
     }

    // try to display the nestings
    Nester.install(lpexView);
   }

  return true;
 }
}

/*------------------------------*/
/*  nesting-display management  */
/*------------------------------*/

/**
 * This class manages the display of the requested nesting.
 */
class Nester extends LpexViewAdapter
             implements DisposeListener, LpexMarkListener, PaintListener
{
 // maximum nesting and the margin area needed for it
 static final int MAX_LEVEL = 5;
 static final int INDENT = 3;
 static final int MARGIN = INDENT + MAX_LEVEL * 2 + 4;

 // global list of active nesters
 static HashMap<LpexView,Nester> _nesters = new HashMap<LpexView,Nester>();

 // info for one nester instance
 LpexView _lpexView;
 String _initPrefixArea, _initPrefixAreaText, _initPrefixAreaMargin;
 LpexCommand _initParseCommand;

 int  _markId, _markId1;
 char _bracket, _bracketStyle;

 Color _extentColor, _badExtentColor, _backgroundColor;
 Image _image;

 ArrayList<Extent> _extents = new ArrayList<Extent>();
 boolean _validExtents;

 /**
  * Constructs a nester for the specified view.
  */
 private Nester(LpexView lpexView)
 {
  _lpexView = lpexView;
  _nesters.put(_lpexView, this);

  _lpexView.addLpexViewListener(this);
  _lpexView.window().textWindow().addDisposeListener(this);
  _lpexView.window().textWindow().addPaintListener(this);
  _lpexView.setOwnerDrawMargin(true);

  _extentColor = new Color(_lpexView.window().getDisplay(), 0, 0, 128);    // RGB dark blue
  _badExtentColor = new Color(_lpexView.window().getDisplay(), 255, 0, 0); // RGB red

  // set up the view
  if ("off".equals(_lpexView.query("current.prefixArea")))
   {
    _initPrefixArea = _lpexView.query("prefixArea");
    _initPrefixAreaText = _lpexView.query("prefixAreaText");
    _lpexView.doCommand("set prefixAreaText none");
   }
  if (_lpexView.queryInt("current.prefixAreaMargin") < MARGIN)
   {
    _initPrefixAreaMargin = _lpexView.query("prefixAreaMargin");
    _lpexView.doCommand("set prefixAreaMargin " + MARGIN);
   }

  initTransients();

  _markId  = _lpexView.queryInt("markId.@NestingCommand");
  _markId1 = _lpexView.queryInt("markId.@NestingCommand1");
  _lpexView.addLpexMarkListener(_markId, this);
  _lpexView.addLpexMarkListener(_markId1, this);
 }

 /**
  * Nester initializations that must be redone after an "updateProfile".
  */
 final void initTransients()
 {
  if (_backgroundColor != null)
   {
    _backgroundColor.dispose();
   }
  String[] ma = _lpexView.query("styleAttributes.prefixArea").split(" ");
  _backgroundColor = new Color(_lpexView.window().getDisplay(), // prefix area margin's
   Integer.parseInt(ma[3]), Integer.parseInt(ma[4]), Integer.parseInt(ma[5]));

  _lpexView.doCommand("set prefixArea on");

  _initParseCommand = _lpexView.defineCommand("parse", new LpexCommand() {
   public boolean doCommand(LpexView lpexView, String parameters)
   { return doParseCommand(lpexView, parameters); } });
 }

 /**
  * Request to start displaying the nesting in a block in the given document view.
  * Assumes that the specified view will only ever be shown in its current window.
  */
 static void install(LpexView lpexView)
 {
  if (lpexView == null || lpexView.window() == null)
   {
    return;
   }

  LpexDocumentLocation loc = lpexView.documentLocation();
  if (loc.element == 0)
   {
    lpexView.doCommand("set messageText No visible elements.");
    return;
   }

  if (lpexView.show(loc.element))
   {
    lpexView.doCommand("set messageText Current position on a show line.");
    return;
   }

  lpexView.doCommand("parse"); // parse any pending changes
  LpexDocumentLocation match = LpexMatch.match(lpexView, loc);
  if (match == null)
   {
    lpexView.doCommand("set messageText No match for the current position.");
    return;
   }

  // allow the display of only one nesting per view
  if (_nesters.get(lpexView) != null)
   {
    uninstall(lpexView);
   }

  // mark main block (one Nester per view, one mark pair is sufficient)
  int markElement, markPosition, markElement1, markPosition1;
  if (match.element >= loc.element)
   {
    markElement = loc.element;
    markPosition = (match.position > loc.position)? loc.position : match.position;
    markElement1 = match.element;
    markPosition1 = (match.position > loc.position)? match.position : loc.position;
   }
  else
   {
    markElement = match.element;
    markPosition = match.position;
    markElement1 = loc.element;
    markPosition1 = loc.position;
   }
  lpexView.doCommand("set mark.@NestingCommand " + markElement + " " + markPosition);
  lpexView.doCommand("set mark.@NestingCommand1 " + markElement1 + " " + markPosition1);

  Nester n = new Nester(lpexView);
  n._bracket = lpexView.elementText(markElement).charAt(markPosition - 1);
  String style = lpexView.elementStyle(markElement);
  n._bracketStyle = (style.length() >= markPosition)? style.charAt(markPosition - 1) : '!';
 }

 /**
  * Request to stop the nesting display in the given view.
  */
 static void uninstall(LpexView lpexView)
 {
  Nester nester = _nesters.get(lpexView);
  if (nester != null)
   {
    nester.uninstall();
   }
 }

 /**
  * Removes this nester.  Removes its mark and listeners,
  * restores the original view settings, cleans up.
  */
 private void uninstall()
 {
  if (_lpexView != null)
   {
    _lpexView.setOwnerDrawMargin(false);
    _lpexView.defineCommand("parse", _initParseCommand);
    _lpexView.removeLpexViewListener(this);
    _lpexView.removeLpexMarkListener(_markId, this);
    _lpexView.doCommand("set mark.@NestingCommand clear");
    _lpexView.doCommand("set mark.@NestingCommand1 clear");

    LpexWindow lpexWindow = _lpexView.window();
    if (lpexWindow != null)
     {
      Composite textWindow = lpexWindow.textWindow();
      if (!textWindow.isDisposed())
       {
        textWindow.removePaintListener(this);
        textWindow.removeDisposeListener(this);
        textWindow.redraw();
       }
     }

    if (_initPrefixAreaMargin != null)
     _lpexView.doCommand("set prefixAreaMargin " + _initPrefixAreaMargin);
    if (_initPrefixAreaText != null)
     _lpexView.doCommand("set prefixAreaText " + _initPrefixAreaText);
    if (_initPrefixArea != null)
     _lpexView.doCommand("set prefixArea " + _initPrefixArea);

    _extentColor.dispose();
    _badExtentColor.dispose();
    _backgroundColor.dispose();
    if (_image != null)
     {
      _image.dispose();
     }

    _nesters.remove(_lpexView);
    _lpexView = null;
   }
 }

 // Text window dispose listener - view's window is being disposed, uninstall.
 public void widgetDisposed(DisposeEvent e)
 { uninstall(); }

 // Mark listener - main block's start/end bracket has been modified, uninstall.
 public void markChanged(LpexView lpexView, int markId)
 { uninstall(); }

 // Mark listener - main block's start/end bracket has been deleted, uninstall.
 public void markDeleted(LpexView lpexView, int markId)
 { uninstall(); }

 // View listener - view is being disposed, uninstall.
 public void disposed(LpexView lpexView)
 { uninstall(); }

 // View listener - the updateProfile command has completed.
 public void updateProfile(LpexView lpexView)
 {
  initTransients();      // ensure our transient settings stick
  _validExtents = false; // must now update extents (doc may have changed)
 }

 // View listener - view's screen has been refreshed, update our display.
 public void shown(LpexView lpexView)
 {
  // ensure the extents are up-to-date
  getExtents();

  // display them - should optimize: only call it from here if extents have been
  // updated or the viewport / font (row height) / view filtering has changed
  doubleBufferPaint(null);
 }

 // Text window paint listener - paint event notification.
 public void paintControl(PaintEvent e)
 {
  if (e.x > _lpexView.queryInt("prefixAreaWidth"))
   {
    return; // invalidated area is beyond the prefix-area margin.
   }

  doubleBufferPaint(e);
 }

 // Runs our extended "parse" command - do original command, invalidate _extents.
 boolean doParseCommand(LpexView lpexView, String parameters)
 {
  if (_initParseCommand != null)
   {
    _initParseCommand.doCommand(lpexView, parameters);
   }
  else
   {
    _lpexView.doDefaultCommand("parse " + parameters);
   }

  _validExtents = false;
  return true;
 }

 // Draws the nestings.  Uses an Image that covers the prefix-area margin.
 void doubleBufferPaint(PaintEvent e)
 {
  LpexWindow lpexWindow = _lpexView.window();
  if (lpexWindow == null)
   {
    return;
   }

  int prefixAreaWidth = _lpexView.queryInt("prefixAreaWidth");
  if (prefixAreaWidth == 0)
   {
    return;
   }

  // get height, adjust for non-element rows below text area & top expand header
  int rowHeight = _lpexView.queryInt("rowHeight");
  int rows = _lpexView.queryInt("rows");
  int y = 0;
  for (int i = rows; i > 0 && _lpexView.elementOfRow(i) == 0; i--)
   {
    rows--;
   }
  if (rows > 0 && _lpexView.elementOfRow(1) == 0)
   {
    y += rowHeight;
    rows--;
   }
  if (rows == 0)
   {
    return; // no text elements visible at all.
   }
  int height = rows * rowHeight;

  int prefixAreaMargin = _lpexView.queryInt("current.prefixAreaMargin");
  if (_image != null)
   {
    Rectangle r = _image.getBounds();
    if (r.width != prefixAreaMargin || r.height != height)
     {
      _image.dispose();
      _image = null;
     }
   }
  if (_image == null)
   {
    _image = new Image(lpexWindow.getDisplay(), prefixAreaMargin, height);
   }

  /**/ GC imageGC = new GC(_image);
  imageGC.setBackground(_backgroundColor);
  imageGC.fillRectangle(0, 0, prefixAreaMargin, height);
  imageGC.setLineStyle(SWT.LINE_SOLID);
  imageGC.setLineWidth(1);
  doPaint(imageGC, y);
  GC g = (e != null)? e.gc : new GC(lpexWindow.textWindow());
  g.drawImage(_image, _lpexView.queryInt("expandHideAreaWidth") +
                      prefixAreaWidth - prefixAreaMargin /*x*/, y);
  /**/ imageGC.dispose();
  if (e == null)
   {
    g.dispose();
   }
 }

 // Determines all the extents for the initially selected block.
 void getExtents()
 {
  // only done 1.- very first time around, 2.- after an "updateProfile" (the document
  // may have been reloaded), 3.- after a text change was committed (we extend the
  // "parse" command to listen:  this also ensures text is parsed, as we need
  // up-to-date styles) or on every shown() notification if there is no parser
  if (_validExtents && _lpexView.parser() != null)
   {
    return;
   }

  _extents.clear();
  String start = _lpexView.query("mark.@NestingCommand");
  String end   = _lpexView.query("mark.@NestingCommand1");
  if (start != null && end != null)
   {
    String[] loc = start.split(" ");
    LpexDocumentLocation top = new LpexDocumentLocation(Integer.parseInt(loc[0]),
                                                        Integer.parseInt(loc[1]));
    loc = end.split(" ");
    LpexDocumentLocation match = new LpexDocumentLocation(Integer.parseInt(loc[0]),
                                                          Integer.parseInt(loc[1]));
    if (top.element != match.element)
     {
      _extents.add(new Extent(top.element, match.element, 0)); // main block scope
      getExtents(top, match, 1); // add any inner blocks
     }
   }

  _validExtents = true;
 }

 // Adds the extents found inside the given non-inclusive start & end locations.
 void getExtents(LpexDocumentLocation start, LpexDocumentLocation end, int level)
 {
  if (level >= MAX_LEVEL)
   {
    return;
   }

  boolean skippingFoundBlock = false;
  int startOffset = start.position;
  for (int e = start.element; e <= end.element;)
   {
    if (!_lpexView.show(e))
     {
      String text = _lpexView.elementText(e);
      int len = (e != end.element)? text.length() : end.position;
      for (int i = startOffset; i < len; i++)
       {
        if (text.charAt(i) == _bracket)
         {
          String style = _lpexView.elementStyle(e);
          char s = (i < style.length())? style.charAt(i) : '!';
          if (s == _bracketStyle)
           {
            LpexDocumentLocation top = new LpexDocumentLocation(e, i+1);
            LpexDocumentLocation match = LpexMatch.match(_lpexView, top);
            // check for one level of unmatched start bracket
            boolean mismatch = match == null || match.element > end.element ||
                               match.element == end.element && match.position == end.position;
            int endElement = mismatch? _lpexView.elements() + 1 : match.element;

            // new extent found
            if (top.element != endElement)
             {
              _extents.add(new Extent(top.element, endElement, level));
              // check (recursively) for its inner blocks
              getExtents(top, (match != null)? match : end, level + 1);
             }

            // continue at this level after the found block
            if (match == null)
             {
              return;
             }
            e = endElement;
            startOffset = match.position;
            skippingFoundBlock = true;
            break; // out of inner "for"
           }
         }
       }//end "for" positions inside an element
     }

    if (!skippingFoundBlock)
     {
      e++;
      startOffset = 0; // move to a new element
     }
    skippingFoundBlock = false;
   }//end "for" elements
 }

 /**
  * Paints the block extents to the given Image graphic context.
  * @param imageShift Image may be shifted down one row for top expand header
  */
 void doPaint(GC gc, int imageShift)
 {
  if (_extents.size() == 0)
   {
    return;
   }

  /*==========================================================================*/
  /*  establish lowest/highest lines inside the currently-loaded doc section  */
  /*==========================================================================*/
  int rows = _lpexView.queryInt("rows"); // 1..rows visible in the edit area
  int lowestShowing = _lpexView.queryInt("lines") + 1 -
   (_lpexView.queryInt("lines.beforeStart") + _lpexView.queryInt("lines.afterEnd"));
  int highestShowing = 0;
  for (int i = 1; i <= rows; i++)
   {
    int element = _lpexView.elementOfRow(i);
    if (element != 0 && !_lpexView.show(element))
     {
      int line = _lpexView.lineOfElement(element);
      if (line < lowestShowing)
       {
        lowestShowing = line;
       }
      if (line > highestShowing)
       {
        highestShowing = line;
       }
     }
   }

  if (highestShowing == 0)
   {
    return;
   }

  /*=================*/
  /*  draw nestings  */
  /*=================*/
  int rowHeight = _lpexView.queryInt("rowHeight");
  int currentMargin = _lpexView.queryInt("current.prefixAreaMargin");
  int windowHeight = _lpexView.window().textWindow().getSize().y;

  // iterate through the extents...
  for (int e = 0; e < _extents.size(); e++)
   {
    Extent extent = _extents.get(e);
    int startLine = extent.startLine;
    int endLine   = extent.endLine;
    if (startLine > highestShowing || endLine < lowestShowing)
     {
      continue;
     }

    /*------------------------------------*/
    /*  determine its rows on the screen  */
    /*------------------------------------*/
    int startRow = -1, endRow = -1; // ZERO-based rows for paint calculations
    boolean realStart = false, realEnd = false;
    for (int i = 1; i <= rows; i++)
     {
      int element = _lpexView.elementOfRow(i);
      if (element != 0 && !_lpexView.show(element))
       {
        int line = _lpexView.lineOfElement(element);

        if (line == startLine)
         {
          startRow = i - 1;
          realStart = true;
         }
        else if (line > startLine &&
                 startRow == -1 &&  // start has not yet been set
                 line <= endLine)
         {
          startRow = i - 1;
          endRow  = startRow;
          realEnd = line == endLine;
         }

        if (line > endLine)
         {
          break;
         }
        else if (line == endLine)
         {
          endRow = i - 1;
          realEnd = true;
          break;
         }
        else if (line < endLine && startRow != -1)
         {
          endRow = i - 1;
         }
       }
     }//end "for rows"

    /*------------*/
    /*  paint it  */
    /*------------*/
    if (startRow >= 0)      // if visible...
     {
      int y = (startRow * rowHeight) - imageShift;

      if (y < windowHeight) // ...and within the text window height
       {
        int height = (endRow - startRow + 1) * rowHeight;
        if (realStart)
         {
          y += rowHeight / 2;
          height -= rowHeight / 2;
         }
        if (realEnd)
         {
          height -= rowHeight / 2;
         }

        // paint this extent
        gc.setForeground(extent.bad? _badExtentColor : _extentColor);
        int x = (currentMargin - MARGIN) + INDENT + extent.nestingLevel * 2;
        gc.drawLine(x, y, x, y + height);
        int arrowWidth = currentMargin - x - 3;
        if (realStart)
         {
          gc.drawLine(x,              y,   x+arrowWidth,   y);
          gc.drawLine(x+arrowWidth-2, y-2, x+arrowWidth+1, y);
          gc.drawLine(x+arrowWidth-2, y+2, x+arrowWidth+1, y);
         }
        if (realEnd)
         {
          gc.drawLine(x,              y+height,   x+arrowWidth,   y+height);
          gc.drawLine(x+arrowWidth-2, y+height-2, x+arrowWidth+1, y+height);
          gc.drawLine(x+arrowWidth-2, y+height+2, x+arrowWidth+1, y+height);
         }
       }
     }
   }
 }

 // An extent.  Kept in terms of text lines, rather than elements, so it's
 // not affected by added/removed SHOW lines (e.g., parser error messages).
 class Extent
 {
  int startLine, endLine, nestingLevel;
  boolean bad; // no matching end

  Extent(int startElement, int endElement, int nestingLevel)
  {
   startLine = _lpexView.lineOfElement(startElement);
   endLine = _lpexView.lineOfElement(endElement);
   if (endLine == 0)
    {
     endLine = _lpexView.queryInt("lines") + 1;
     bad = true;
    }
   this.nestingLevel = nestingLevel;
  }
 }
}