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

package com.ibm.lpex.samples;

import com.ibm.lpex.core.LpexCommand;
import com.ibm.lpex.core.LpexNls;
import com.ibm.lpex.core.LpexStringTokenizer;
import com.ibm.lpex.core.LpexView;

/**
 * Sample command <b>detab</b> - expand all tabs to spaces.
 * Expands all the tab characters in the document to blanks.  By default, it
 * uses the value of the <b>tabs</b> editor parameter in the current view.
 * Alternatively, the tab settings may be specified via the command parameters.
 *
 * <p>Here is the DetabCommand
 * <a href="doc-files/DetabCommand.java.html">source code</a>.
 * This class shares several methods with {@link EntabCommand}.</p>
 *
 * <p>To run this sample:
 * <ul>
 *  <li>Define this user command via an editor preference page, where available,
 *   or from the editor command line:
 *   <pre>set commandClass.detab com.ibm.lpex.samples.DetabCommand</pre></li>
 *  <li>Run it from the editor command line:
 *   <pre>detab [<i>stop1 stop2 ..</i> [every <i>increment</i>]]</pre></li>
 * </ul></p>
 *
 * @see com.ibm.lpex.samples.EntabCommand
 * @see com.ibm.lpex.samples All the samples
 */
public class DetabCommand implements LpexCommand
{
 /**
  * Runs this command.
  * Expands all the tabs in the document.
  *
  * @param lpexView the document view in which the command was issued
  * @param parameters optional tab stops
  */
 public boolean doCommand(LpexView lpexView, String parameters)
 {
  parameters = parameters.trim();

  if ("?".equals(parameters)) // command help
   {
    if (lpexView != null)
     {
      lpexView.doCommand("set messageText Syntax: detab [<tab stop> .. [every <increment>]]");
     }
    return true;
   }

  // if no view, just validate the parameters (if any)
  if (lpexView == null)
   {
    return validSettings(parameters);
   }

  // determine and validate the tab settings to use
  Settings currentSettings = new Settings();
  if (!initSettings(lpexView, parameters, currentSettings,
                    TestCommand.commandName(this, lpexView)))
   {
    return false;
   }

  // expand the tabs in the document
  boolean changes = expandTabs(lpexView, currentSettings);

  // indicate successful completion
  message(lpexView, changes? "Tabs in document expanded." :
                             "No tabs found in document.");
  return true;
 }

 /**
  * Expands the tabs in the document, according to the given settings.
  *
  * @return indication whether any tabs were expanded in the document
  */
 boolean expandTabs(LpexView lpexView, Settings currentSettings)
 {
  // remember original cursor location on the screen
  int originalElement = lpexView.currentElement();
  int originalPosition = lpexView.queryInt("displayPosition");

  // process all non-show elements
  boolean changes = false;
  int elements = lpexView.elements();
  for (int element = 1; element <= elements; element++)
   {
    if (!lpexView.show(element))
     {
      if (expandTabs(element, currentSettings))
       {
        changes = true;
       }
     }
   }

  // restore the cursor
  if (changes && (lpexView.currentElement() != originalElement ||
                  lpexView.queryInt("displayPosition") != originalPosition))
   {
    lpexView.jump(originalElement, originalPosition);
   }

  return changes;
 }

 /**
  * Expands the tabs in a non-show element, according to the given settings.
  * Tabs are interpreted in terms of display columns.
  *
  * <p>Currently, we change the entire element text - could be subtler and
  * replace '\t' with a blank and then insert any additional blanks up to the
  * tab stop, in order to maintain marks better.</p>
  *
  * @return indication whether any tabs were expanded in the element
  */
 boolean expandTabs(int element, Settings currentSettings)
 {
  LpexView lpexView = currentSettings._lpexView;
  String text = lpexView.elementText(element);
  if (text.indexOf('\t') < 0)
   {
    return false; // empty line / no tabs.
   }

  StringBuilder buffer = new StringBuilder(text.length() + 32);

  int tc = 0;         // last-used tab stop in currentSettings._tabStops[]
  int lastTabVal = 0; // last-used tab stop position

  int i = 0;          // current index into text
  int pos = 1;        // current display column position

  for (; i < text.length(); i++)
   {
    char c = text.charAt(i);
    if (c == '\t')
     {
      // establish relevant tab stop
      while (tc < currentSettings._tabStops.length && lastTabVal <= pos)
       {
        lastTabVal = currentSettings._tabStops[tc++];
       }
      while (lastTabVal <= pos)
       {
        lastTabVal += currentSettings._tabIncrement; // NB it's non-zero
       }

      int tabLen = lastTabVal - pos;
      for (int j = 0; j < tabLen; j++)
       {
        buffer.append(' ');
       }
      pos = lastTabVal; // we're at lastTabVal display column position now
     }
    else
     {
      buffer.append(c);
      pos += getSourceWidth(lpexView, c);
     }
   }

  lpexView.setElementText(element, buffer.toString());
  return true;
 }

 /**
  * Estimates the width in display columns for one Java Unicode char converted
  * to the document's source character encoding.  Far from fail-safe...
  *
  * @param lpexView view on a document
  * @param c Java Unicode char
  */
 int getSourceWidth(LpexView lpexView, char c)
 {
  // a bidi mark (not visible on the display)?
  if (LpexNls.isBidiMark(c))
   {
    return 0;
   }

  // a surrogate - for tabs calcs, returning 1 for each surrogate if a pair should be OK?
  if (c >= '\uD800' && c <= '\uDBFF' || // high surrogate
      c >= '\uDC00' && c <= '\uDFFF')   // low surrogate
   {
    return 1;
   }

  // to determine Asian double-width characters, the only way right now is to kludge it
  // via checking for encoding into 2 bytes when the doc's source encoding is MBCS...
  int len = 1;
  if (lpexView.nls().isSourceMbcs())
   {
    len = lpexView.nls().sourceLength(c);
    if (len > 2)
     {
      return 2;
     }
   }

  return len;
 }

 /**
  * Validates any tab stops specified.
  *
  * @param tabs optional tabs setting, e.g., "1 4 8 every 8"
  * @return true = tabs correct / none
  */
 static boolean validSettings(String tabs)
 {
  return (tabs != null && tabs.length() != 0)?
          initSettings(null, tabs, null, null) : true;
 }

 /**
  * Validates and sets up the tab stops and view for one run of the command.
  *
  * @param lpexView document view running the command
  * @param tabs     optional tabs setting, e.g., "1 4 8 every 8"
  * @param settings Settings object to store the result
  * @param command  command name (e.g., "detab")
  * @return true = tabs correct, settings initialized
  */
 static boolean initSettings(LpexView lpexView, String tabs, Settings settings,
                             String command)
 {
  int tabIncrement = 1; // if none, tab stop on every character
  int lastTabStop = 0;
  int count = 1;

  // validate, and determine the count of, the tab stops
  if (tabs == null || tabs.length() == 0)
   {
    tabs = lpexView.query("current.tabs");
   }

  LpexStringTokenizer st = new LpexStringTokenizer(tabs);
  String token = null;
  while (st.hasMoreTokens())
   {
    token = st.nextToken();
    try
     {
      int tabStop = Integer.parseInt(token);
      if (tabStop <= lastTabStop)
       {
        return incorrectParameter(lpexView, token, command);
       }

      if (tabStop != 1)
       {
        count++;
       }
      lastTabStop = tabStop;
      token = null;
     }
    catch(NumberFormatException e)
     {
      break;
     }
   }

  // determine the tab increment
  if (token != null)
   {
    if (!token.equals("every"))
     {
      return incorrectParameter(lpexView, token, command);
     }
    if (!st.hasMoreTokens())
     {
      return missingParameter(lpexView, command);
     }

    token = st.nextToken();
    try
     {
      tabIncrement = Integer.parseInt(token);
      if (tabIncrement < 1)
       {
        return incorrectParameter(lpexView, token, command);
       }
     }
    catch(NumberFormatException e)
     {
      return incorrectParameter(lpexView, token, command);
     }

    if (st.hasMoreTokens())
     {
      return incorrectParameter(lpexView, st.nextToken(), command);
     }
   }

  // set up the tab stops
  int[] tabStops = new int[count];
  tabStops[0] = 1; // always a first tab stop at 1
  int i = 1;
  st = new LpexStringTokenizer(tabs);
  try
   {
    while (st.hasMoreTokens() && i < count)
     {
      int tabStop = Integer.parseInt(st.nextToken());
      if (tabStop != 1)
       {
        tabStops[i] = tabStop;
        i++;
       }
     }
   }
  catch(NumberFormatException e) {}

  if (settings != null)
   {
    settings._tabStops = tabStops;
    settings._tabIncrement = tabIncrement;
    settings._lpexView = lpexView;
   }
  return true;
 }

 /**
  * Displays an incorrect-parameter message on the message line.
  */
 static boolean incorrectParameter(LpexView lpexView, String parm, String command)
 {
  message(lpexView, "\""+parm+"\" is not a correct parameter for the \""+command+"\" command.");
  return false;
 }

 /**
  * Displays a missing-parameter message on the message line.
  */
 static boolean missingParameter(LpexView lpexView, String command)
 {
  message(lpexView, "Additional parameters are required for the \""+command+"\" command.");
  return false;
 }

 /**
  * Displays a message on the editor message line.
  */
 static void message(LpexView lpexView, String message)
 {
  if (lpexView != null)
   {
    lpexView.doCommand("set messageText " + message);
   }
 }

 /**
  * Settings used during one run of the command.
  */
 static class Settings
 {
  int[] _tabStops;     // e.g., 1, 4, 8
  int   _tabIncrement; // e.g., 8
  LpexView _lpexView;
 }
}