//----------------------------------------------------------------------------
// COMPONENT NAME: LPEX Editor
//
// © Copyright IBM Corporation 1998, 2008
// All Rights Reserved.
//
// DESCRIPTION:
// Lpex - sample stand-alone SWT LPEX widget editor
//----------------------------------------------------------------------------

package com.ibm.lpex.samples;

import java.io.File;
import java.util.ArrayList;

import com.ibm.lpex.core.LpexAction;
import com.ibm.lpex.core.LpexCommand;
import com.ibm.lpex.core.LpexMessageConstants;
import com.ibm.lpex.core.LpexResources;
import com.ibm.lpex.core.LpexStringTokenizer;
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.FocusEvent;
import org.eclipse.swt.events.FocusListener;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Listener;
import org.eclipse.swt.widgets.MessageBox;
import org.eclipse.swt.widgets.Shell;


/**
 * Sample stand-alone editor built on the LPEX edit widget.
 *
 * <p>Here is the Lpex <a href="doc-files/Lpex.java.html">source code</a>.</p>
 *
 * <p>Syntax for running Lpex from the command line:
 * <pre>
 *   java [<i>java options</i>] com.ibm.lpex.samples.Lpex [<i>filename</i>]
 *        [-encoding <i>charEncoding</i>] [-dt {<i>documentType</i> | none}] </pre>
 * For example:
 * <pre>
 *   java com.ibm.lpex.samples.Lpex \sample.props -dt properties </pre>
 * A possible Windows batch program is:
 * <pre>
 *   &#64;start /b javaw com.ibm.lpex.samples.Lpex %1 %2 %3 %4 %5 %6 %7 %8 %9 </pre>
 * (use <code>java</code> instead of <code>javaw</code> to see the stack trace for exceptions).
 * To disable the JIT compiler, run it with this java option:
 * <pre>
 *   -Djava.compiler= </pre>
 * You can run Lpex in a particular locale.  For example, in order to run it in
 * Simplified Chinese (zh_CN), use these two java options:
 * <pre>
 *   -Duser.language=zh -Duser.region=CN </pre></p>
 *
 * Commands and actions defined in here:
 * <ul>
 *  <li><b>Lpex</b>, <b>e</b> commands - open a new Lpex window
 *  <li><b>exit</b> command - close all Lpex views of the document
 *  <li><b>openNewView</b> command - open an additional view on the document
 *  <li><b>quit</b> command - unconditionally quit the Lpex window
 *  <li><b>test</b> command - placeholder for testing new commands
 *  <li><b>view</b> { log | profile } command - view the LPEX editor log / defaults profile
 *  <li><b>close</b> action (F3) - close the Lpex window
 *  <li><b>nextView</b> action (Alt+Shift+-&gt;) - go to the next view of this document
 *  <li><b>openNewView</b> action (Ctrl+O) - open an additional view on the document
 *  <li><b>prevView</b> action (Alt+Shift+&lt;-) - go to the previous view of this document
 *  <li><b>test</b> action - placeholder for testing new actions.
 * </ul>
 *
 * <p>Example user-defined editor action to open an additional document view with Lpex:</p>
 * <table bgcolor="#f0f0f0"><tr><td><pre>&nbsp;
 * lpexView.defineAction(<font color="#800080">"openNewView"</font>, <font color="#0000ff"><b>new</b></font> LpexAction() {
 *  <font color="#0000ff"><b>public void</b></font> doAction(LpexView view)
 *  {
 *   Display display = getDisplay();
 *   display.asyncExec(<font color="#0000ff"><b>new</b></font> Runnable() {
 *    <font color="#0000ff"><b>public void</b></font> run() {
 *     <font color="#0000ff"><b>try</b></font>
 *      {
 *       Class cl = Class.forName(<font color="#800080">"com.ibm.lpex.samples.Lpex"</font>);
 *       Constructor lpexConstructor = cl.getConstructor(<font color="#0000ff"><b>new</b></font> Class[]
 *        { LpexView.class, com.ibm.lpex.samples.Lpex.Delegate.class,
 *          Rectangle.class, Boolean.TYPE });
 *       lpexConstructor.newInstance(<font color="#0000ff"><b>new</b></font> Object[]
 *        { <i>getView</i>(), <i>getDelegate</i>(), <font color="#0000ff"><b>new</b></font> Rectangle(10, 10, 648, 711),&nbsp;
 *          Boolean.valueOf(<font color="#0000ff"><b>false</b></font>) });
 *      }
 *     <font color="#0000ff"><b>catch</b></font>(Exception e) {}
 *     }});
 *  }
 *  <font color="#0000ff"><b>public boolean</b></font> available(LpexView view)
 *  { <font color="#0000ff"><b>return true</b></font>; }
 * }); </pre></td></tr></table>
 *
 * @see com.ibm.lpex.samples All the samples
 */
public final class Lpex
{
 private Shell _shell;    // our shell
 private boolean _browse; // opened as read only

 private LpexWindow _lpexWindow;
 private LpexView _lpexView;
 LpexViewNode _lpexViewNode;

 private Shell _papaShell;
 private Listener _papaShellListener;

 /**
  * Entry point.
  */
 public static void main(String[] args)
 {
  new Lpex(args, null /*bounds*/, false /*browse*/, null /*papaShell*/);
 }

 /**
  * Constructor for opening a file.
  * @param parms     file name and parameters
  * @param bounds    size and position for the window
  * @param browse    if <code>true</code>, enforce read-only mode
  * @param papaShell Shell of the external program that started us, or <code>null</code>
  */
 public Lpex(String[] parms, Rectangle bounds, boolean browse, Shell papaShell)
 {
  // extract file name and parameters
  LpexParameters p = new LpexParameters(parms);
  String filename = p.filename();

  // ensure canonical file name
  if (filename != null)
   {
    if (filename.trim().length() == 0)
     {
      filename = null;
     }
    else
     {
      try
       {
        filename = (new File(filename)).getCanonicalPath();
       }
      catch(Exception e) {}
     }
   }

  // create an LpexView for the given file name and character encoding,
  _lpexView = new LpexView(filename, p.encoding(), false); // do "updateProfile" later
  _lpexViewNode = new LpexViewNode(this);

  // determine document parser to use
  if (p.documentType() != null)
   {
    if ("none".equals(p.documentType()))
     {
      _lpexView.doCommand("set updateProfile.noParser on");
     }
    else
     {
      _lpexView.doCommand("set updateProfile.parser " +
       _lpexView.query("current.updateProfile.parserAssociation." + p.documentType()));
     }
   }

  // opened from an external program, asked to listen to its demise
  if (papaShell != null)
   {
    _papaShell = papaShell;
    _papaShellListener = new Listener() {
      public void handleEvent(Event event) {
       if (event.type == SWT.Close)
        {
         if (_lpexView.queryInt("changes") == 0 && !_lpexView.queryOn("dirty"))
          {
           _lpexView.doCommand("exit");     // clean doc - close immediately
          }
         else if (_shell != null)
          {
           _shell.getDisplay().asyncExec(new Runnable() {
            public void run() {
             if (_lpexView != null)
              {
               _lpexView.doCommand("exit"); // changes pending - must prompt user
              }
             }});
          }
        }
       else if (event.type == SWT.Dispose)
        {
         _papaShell = null;
         _papaShellListener = null;
        }
       }};

    _papaShell.addListener(SWT.Close, _papaShellListener);
    _papaShell.addListener(SWT.Dispose, _papaShellListener);
   }

  // initialize the view
  setUpView(bounds, browse, null);
 }

 /**
  * Constructor for an external program to create a new (secondary) view on its
  * existing document.
  * @param lpexView master LpexView of the document
  * @param delegate optional Delegate object for the external program to handle
  *                 actions and commands from this Lpex view
  * @param bounds   size and position for the window
  * @param browse   if <code>true</code>, enforce read-only mode
  */
 public Lpex(LpexView lpexView, Delegate delegate, Rectangle bounds, boolean browse)
        throws LpexView.ViewInstantiationException
 {
  this(LpexViewNode.getLpexViewNode(lpexView, delegate), bounds, browse);
 }

 /**
  * Constructor for a new view on an existing document.
  * @param lpexViewNode an existing view on the document
  * @param bounds       size and position for the window
  * @param browse       if <code>true</code>, enforce read-only mode
  */
 private Lpex(LpexViewNode lpexViewNode, Rectangle bounds, boolean browse)
         throws LpexView.ViewInstantiationException
 {
  // originating view
  LpexView lpexView = lpexViewNode.lpexView();

  // create a new LpexView for the given view's document,
  // do an explicit "updateProfile" later
  _lpexView = new LpexView(lpexView, false);
  _lpexViewNode = new LpexViewNode(this, lpexViewNode);

  // initialize the view
  setUpView(bounds, browse, lpexView);
 }

 /**
  * Initializes the new Lpex view.
  * @param bounds   size and position for the window
  * @param browse   if <code>true</code>, enforce read-only mode
  * @param fromView optional document view from which this Lpex was started
  */
 private void setUpView(Rectangle bounds, boolean browse, LpexView fromView)
 {
  _browse = browse;

  // create a new Shell
  _shell = new Shell();

  // use the same parser as the originating view
  if (fromView != null)
   {
    if (fromView.queryOn("updateProfile.noParser"))
     {
      _lpexView.doCommand("set updateProfile.noParser on");
     }
    else
     {
      _lpexView.doCommand("set updateProfile.parser " + fromView.query("updateProfile.parser"));
     }
   }

  // create & set a window for our LpexView
  _lpexWindow = new LpexWindow(_shell);
  _lpexView.setWindow(_lpexWindow);

  // add an LpexViewListener to handle all the LPEX listening needed here
  _lpexView.addLpexViewListener(new LpexViewAdapter() {
   public void renamed(LpexView lpexView)
   { Lpex.this.renamed(lpexView); }
   public void updateProfile(LpexView lpexView)
   { Lpex.this.updateProfile(lpexView); }
   public void disposed(LpexView lpexView)
   { _lpexViewNode.remove(); }
  });

  // run the "updateProfile" command
  _lpexView.doDefaultCommand("updateProfile");

  // for a regular document with an underlying file, if file is currently
  // read only listen to the user changing its attribute to read/write
  if (_lpexView.queryOn("readonly"))
   {
    if (!_browse && hasUnderlyingFile())
     {
      _lpexView.window().textWindow().addFocusListener(new FocusListener() {
       public void focusGained(FocusEvent e) {
        if (_lpexView != null && !isFileReadonly())
         {
          _lpexView.doCommand("set readonly off");
          _lpexView.doCommand("screenShow view");
          _lpexView.window().textWindow().removeFocusListener(this);
         }
        }
       public void focusLost(FocusEvent e) {}
       });
     }
   }
  // start up in originating view's read-only state
  else
   {
    if (fromView != null && fromView.queryOn("readonly"))
     {
      _lpexView.doCommand("set readonly on");
     }
   }

  // listen to a few Shell events
  _shell.addListener(SWT.Close, new Listener() {
    public void handleEvent(Event event) { closeShell(event); }});
  _shell.addListener(SWT.Dispose, new Listener() {
    public void handleEvent(Event event) { disposeShell(event); }});
  _shell.addListener(SWT.Resize, new Listener() {
    public void handleEvent(Event event) { resizeShell(event); }});

  // start the new view in the same state as the originating one
  if (fromView != null)
   {
    _lpexView.doCommand("set includedClasses " + fromView.query("includedClasses"));
    _lpexView.doCommand("set excludedClasses " + fromView.query("excludedClasses"));
    String element = fromView.query("element");
    if (element != null) // there are visible elements
     {
      _lpexView.doCommand("locate element " + element);
      _lpexView.doCommand("set cursorRow " + fromView.query("cursorRow"));
      _lpexView.doCommand("set position " + fromView.query("position"));
     }
   }

  // set Shell's position & size, open it
  if (bounds == null)
   {
    int width = _lpexView.queryInt("userParameter.SwtLpex.width");
    int height = _lpexView.queryInt("userParameter.SwtLpex.height");
    bounds = new Rectangle(10, 10, (width <= 0)? 674 : width, (height <= 0)? 683 : height);
   }
  _shell.setBounds(bounds);
  _shell.open();

  setWindowTitle();

  // display view, set input focus in the edit area
  //_lpexView.doDefaultCommand("screenShow");
  //_lpexWindow.textWindow().setFocus();

  // run the Shell window: read & dispatch events from the OS queue until it's disposed
  Display display = _shell.getDisplay();
  while (!_shell.isDisposed())
   {
    // no events for us in the queue, sleep to give other applications a chance to run
    if (!display.readAndDispatch())
     {
      display.sleep();
     }
   }
  _shell = null;
 }

 // Returns document name / locale-sensitive "Untitled Document n".
 private String documentName()
 {
  String name = _lpexView.query("name");
  return (name != null)? name : LpexResources.message(LpexMessageConstants.MSG_UNTITLED_DOCUMENT,
                                                      _lpexView.query("documentId"));
 }

 // Returns whether this view's document has an underlying file.
 private boolean hasUnderlyingFile()
 {
  boolean hasFile = false;
  String name = _lpexView.query("name");
  if (name != null && name.length() > 0)
   {
    try
     { hasFile = (new File(name)).exists(); }
    catch (SecurityException e) // read access denied to file
     { hasFile = true; }
    catch (Exception e) {}
   }
  return hasFile;
 }

 // Returns the read-only state of the underlying file.
 private boolean isFileReadonly()
 {
  boolean readonly = false;
  String name = _lpexView.query("name");
  if (name != null && name.length() > 0)
   {
    try
     { File file = new File(name);
       readonly = file.exists() && !file.canWrite(); }
    catch (SecurityException e) // not allowed to read/write file
     { readonly = true; }
    catch (Exception e) {}
   }
  return readonly;
 }

 /*======================*/
 /*  LPEX notifications  */
 /*======================*/
 // Called after a "set name" command was run.
 private void renamed(LpexView lpexView)
 {
  String name = lpexView.query("name");
  if (name != null)
   {
    try
     {
      String canonicalPath = (new File(name)).getCanonicalPath();
      if (canonicalPath != null && !canonicalPath.equals(name))
       {
        lpexView.doDefaultCommand("set name " + canonicalPath);
        return;
       }
     }
    catch(Exception e) {}
   }

  setWindowTitle();
 }

 // Called whenever the "updateProfile" command has been processed.
 // Defines editor commands, actions & their associated keys.
 private void updateProfile(LpexView lpexView)
 {
  // asked to keep this view as read only
  if (_browse && !lpexView.queryOn("readonly"))
   {
    lpexView.doCommand("set readonly on");
   }

  // "Lpex" command - open an Lpex window
  lpexView.defineCommand("Lpex", new LpexCommand() {
   public boolean doCommand(LpexView view, String parameters)
   { return newWindow(parameters, false, false); } });

  // "e" as synonym for the "Lpex" command
  lpexView.defineCommand("e", new LpexCommand() {
   public boolean doCommand(LpexView view, String parameters)
   { return view.doCommand("Lpex " + parameters); } });

  // "quit" command - unconditionally quit this Lpex window
  // (also needed by base profile vi's ":q" commands)
  lpexView.defineCommand("quit", new LpexCommand() {
   public boolean doCommand(LpexView view, String parameters)
   { quit(); return true; } });

  // "exit" command - safely close all Lpex views of the document
  lpexView.defineCommand("exit", new LpexCommand() {
   public boolean doCommand(LpexView view, String parameters)
   { _lpexViewNode.disposeAllLpex(); return true; } });

  // "openNewView" command - open a new Lpex window on the same document
  lpexView.defineCommand("openNewView", new LpexCommand() {
   public boolean doCommand(LpexView view, String parameters)
   { if (view != null) view.doAction(view.actionId("openNewView")); return true; } });

  // "close" action (F3) - optionally save & close this Lpex window
  lpexView.defineAction("close", new LpexAction() {
   public void doAction(LpexView view)
   { close(); }
   public boolean available(LpexView view)
   { return true; } });
  assignKey("f3", "close"); // ol' style close with F3

  // "openNewView" action (Ctrl+O) - open a new Lpex window on the same document
  lpexView.defineAction("openNewView", new LpexAction() {
   public void doAction(LpexView view)
   { newWindow(Lpex.this, Lpex.this._browse); }
   public boolean available(LpexView view)
   { return true; } });
  assignKey("c-o", "openNewView");

  // "nextView" action (Ctrl+Alt+->) - go to the next view of this document
  lpexView.defineAction("nextView", new LpexAction() {
   public void doAction(LpexView view)
   { LpexView v = Lpex.this._lpexViewNode.nextLpexView();
     if (v != null && v.window() != null) v.window().setFocus(); }
   public boolean available(LpexView view)
   { return true; } });
  assignKey("a-s-right", "nextView");

  // "prevView" action (Ctrl+Alt+<-) - go to the previous view of this document
  lpexView.defineAction("prevView", new LpexAction() {
   public void doAction(LpexView view)
   { LpexView v = Lpex.this._lpexViewNode.prevLpexView();
     if (v != null && v.window() != null) v.window().setFocus(); }
   public boolean available(LpexView view)
   { return true; } });
  assignKey("a-s-left", "prevView");

  // "view log | profile" command - view the LPEX editor log / defaults profile
  lpexView.defineCommand("view", new LpexCommand() {
   public boolean doCommand(LpexView view, String parameters)
   { if ("log".equals(parameters))
      { return newWindow('\"' + LpexView.globalQuery("editorLog") + '\"', true, true); }
     if ("profile".equals(parameters))
      { LpexView.doGlobalCommand("profile flush");
        return newWindow('\"' + LpexView.globalQuery("defaultProfile") + '\"', true, true); }
     return (view != null)?
      view.doCommand("set messageText Syntax: view { log | profile }") : true;
   }});

  // "test" command - placeholder for testing new commands
  lpexView.defineCommand("test", new LpexCommand() {
   public boolean doCommand(LpexView view, String parameters)
   { return (view != null)? view.doCommand("set messageText test: " + parameters) : true; } });

  // "test" action - placeholder for testing new actions
  lpexView.defineAction("test", new LpexAction() {
   public void doAction(LpexView view)
   { view.doCommand("set messageText test action"); }
   public boolean available(LpexView view)
   { return true; } });

  // if there is a master view in the chain, delegate to it potential
  // application-sensitive actions and commands, such as "save"
  Delegate delegate = _lpexViewNode.findDelegate();
  if (delegate != null)
   {
    delegate.delegate(lpexView);
   }
 }

 // Assigns an action to the given key (e.g., "c-o") if free in any context.
 private void assignKey(String key, String actionName)
 {
  if (!_lpexView.keyAssigned(key + ".t")) // text area
     _lpexView.doCommand("set keyAction." + key + ".t " + actionName);
  if (!_lpexView.keyAssigned(key + ".p")) // prefix area
     _lpexView.doCommand("set keyAction." + key + ".p " + actionName);
  if (!_lpexView.keyAssigned(key + ".c")) // command line
     _lpexView.doCommand("set keyAction." + key + ".c " + actionName);
 }

 // Unconditionally quits this Lpex window.
 private void quit()
 {
  _lpexView.dispose();
  _shell.dispose();
 }

 // Safely closes this Lpex window.
 private void close()
 {
  _shell.close();
 }

 // Sets the title of an Lpex window.
 void setWindowTitle()
 {
  String title = "SWT Lpex - " + documentName();
  if (_lpexView.queryInt("documentViews") > 1)
   {
    title += ": " + _lpexView.query("viewId");
   }
  _shell.setText(title);
 }

 // Creates an Lpex window for a new file.  If the file is already opened in
 // the editor, just goes to a view handling it, optionally refreshing it first.
 private boolean newWindow(String parameters, final boolean browse, boolean refresh)
 {
  final String[] parms = LpexStringTokenizer.split(parameters);

  // extract file name, see if it is already opened in the editor
  String filename = (new LpexParameters(parms)).filename();
  if (filename != null)
   {
    LpexView lpexView = _lpexView.lpexView(filename);
    if (lpexView != null && lpexView.window() != null)
     {
      if (refresh)
       {
        lpexView.doAction(lpexView.actionId("reload"));
       }
      if (browse)
       {
        lpexView.doCommand("set readonly on"); // re-set "readonly" after reload
       }
      lpexView.window().setFocus();
      lpexView.doAction(lpexView.actionId("textWindow"));
      return true;
     }
   }

  // if new, open a new document for the file
  _shell.getDisplay().asyncExec(new Runnable() { // à la SwingUtilities#invokeLater()
   public void run() {
    new Lpex(parms, newWindowBounds(), browse, null);
   }});

  return true;
 }

 // Creates an additional Lpex window for an already-open document.
 private boolean newWindow(final Lpex lpex, final boolean browse)
 {
  _shell.getDisplay().asyncExec(new Runnable() {
   public void run() {
    try
     {
      new Lpex(lpex._lpexViewNode, newWindowBounds(), browse);
     }
    catch(LpexView.ViewInstantiationException e) {}
   }});

  return true;
 }

 // Returns acceptable bounds for a new window.
 private Rectangle newWindowBounds()
 {
  if (_shell.getMaximized())
   {
    return new Rectangle(5, 5,
                         _lpexView.queryInt("userParameter.SwtLpex.width"),
                         _lpexView.queryInt("userParameter.SwtLpex.height"));
   }

  // new window a bit to the right, down by less than title height unless too low
  Rectangle newBounds = _shell.getBounds();
  Rectangle shellTrim = _shell.computeTrim(0, 0, 0, 0);
  newBounds.x -= shellTrim.x - 3;
  newBounds.y -= shellTrim.y + 2;
  Rectangle displayBounds = _shell.getDisplay().getBounds();
  if (newBounds.y + newBounds.height > displayBounds.height)
   {
    newBounds.y = 5;
   }

  return newBounds;
 }

 /*=============================*/
 /*  Shell event notifications  */
 /*=============================*/
 // Shell is being disposed.
 private void disposeShell(Event event)
 {
  // remove our listeners to the external program's Shell
  if (_papaShellListener != null)
   {
    _papaShell.removeListener(SWT.Close, _papaShellListener);
    _papaShell.removeListener(SWT.Dispose, _papaShellListener);
    _papaShellListener = null;
   }

  // dispose of our view & its resources (document parser, etc.)
  if (_lpexView != null)
   {
    _lpexView.dispose();
   }
 }

 // Shell size was set / Shell was resized.
 private void resizeShell(Event event)
 {
  Rectangle rect = _shell.getClientArea();
  _lpexWindow.setBounds(0, 0, rect.width, rect.height);

  rect = _shell.getBounds();
  if (!_shell.getMaximized())
   {
    _lpexView.doCommand("set userParameter.SwtLpex.width " + rect.width);
    _lpexView.doCommand("set userParameter.SwtLpex.height " + rect.height);
   }
 }

 // Shell close request.
 private void closeShell(Event event)
 {
  event.doit = closeWindow();
 }

 // Checks whether this Lpex window can be safely closed (saved / user-abandoned).
 private boolean closeWindow()
 {
  boolean close = true;

  if (_lpexView != null && _lpexView.queryInt("documentViews") == 1 && // last view?
      (_lpexView.queryInt("changes") != 0 || _lpexView.queryOn("dirty")))
   {
    MessageBox box = new MessageBox(_shell, SWT.ICON_QUESTION | SWT.YES | SWT.NO | SWT.CANCEL);
    box.setText(_shell.getText());
    box.setMessage(LpexResources.message(LpexMessageConstants.MSG_FILE_SAVE_CHANGES,
                                         documentName()));
    int rc = box.open();

    if (rc == SWT.YES)
     {
      _lpexView.doCommand("save");
      if (_lpexView.query("status") != null) // save failed / was cancelled
       {
        close = false;
       }
     }
    else if (rc == SWT.CANCEL)
     {
      close = false;
     }
   }

  return close;
 }


 /**
  * This interface allows an external program to have actions and commands of
  * secondary {@link com.ibm.lpex.samples.Lpex Lpex} views that it creates,
  * delegated to its own master view of the document.
  */
 public interface Delegate
 {
  /**
   * Redefine actions and commands of the given Lpex view.
   */
  public void delegate(LpexView lpexView);
 }


 // keep a list of all Lpex view chains
 private static ArrayList<LpexViewNode> _viewChains = new ArrayList<LpexViewNode>();

 /**
  * LpexView node for the management of multiple views of one LPEX document.
  */
 static final class LpexViewNode
 {
  private LpexViewNode _prev;
  private LpexViewNode _next;
  private LpexView _lpexView;

  private Lpex _lpex; // null = a non-Lpex (external program) master view node
  private Delegate _delegate; // action/command delegation to the master LpexView


  // Constructs a top node for an Lpex view.
  LpexViewNode(Lpex lpex)
  {
   _lpex = lpex;
   _lpexView = lpex._lpexView;
   _viewChains.add(this);
  }

  // Chains a new view node after another node of the same document.
  LpexViewNode(Lpex newLpex, LpexViewNode oldNode)
  {
   _lpex = newLpex;
   _lpexView = newLpex._lpexView;

   _prev = oldNode;
   _next = oldNode._next;
   if (oldNode._next != null)
    {
     oldNode._next._prev = this;
    }
   oldNode._next = this;
  }

  // Constructs a top node for an external program's master view.
  private LpexViewNode(LpexView lpexView, Delegate delegate)
  {
   _lpexView = lpexView;
   _delegate = delegate;
   _viewChains.add(this);

   _lpexView.addLpexViewListener(new LpexViewAdapter() {
    // called when the master LpexView is disposed
    public void disposed(LpexView view) {
     // clear chain, we may have delegated actions to the now-disposed view
     disposeAllLpex();
     remove();
     }});
  }

  // Finds an existing, or constructs a new, top node
  // for an external program's master view.
  static LpexViewNode getLpexViewNode(LpexView lpexView, Delegate delegate)
  {
   for (int i = 0; i < _viewChains.size(); i++)
    {
     LpexViewNode lpexViewNode = _viewChains.get(i);
     if (lpexViewNode._lpexView == lpexView)
      {
       return lpexViewNode;
      }
    }

   // not found, start new view chain
   return new LpexViewNode(lpexView, delegate);
  }

  // Safely disposes of all document's Lpex views.
  void disposeAllLpex()
  {
   // dispose all around the top master view, if any
   LpexViewNode node = this;
   while (node != null && node._lpex != null && node._prev != null)
    {
     node = node._prev;
    }
   while (node._next != null)
    {
     node._next._lpex.quit();
    }
   while (node._prev != null)
    {
     node._prev._lpex.quit();
    }

   // if not an external-program view, close top view as well
   if (node._lpex != null)
    {
     node._lpex.close();
    }
  }

  // Removes node.
  void remove()
  {
   // unchain
   if (_prev != null)
    {
     _prev._next = _next;
    }
   if (_next != null)
    {
     _next._prev = _prev;
    }

   // cut self off from Lpex
   if (_lpex != null)
    {
     _lpex._lpexView = null;
     _lpex._lpexViewNode = null;
    }

   // if a top node, remove from list of view chains
   int i = _viewChains.indexOf(this);
   if (i >= 0)
    {
     _viewChains.remove(i);
    }
  }

  // Returns this node's view.
  LpexView lpexView()
  {
   return _lpexView;
  }

  // Finds document chain's next view.
  LpexView nextLpexView()
  {
   LpexViewNode node = _next;
   if (node == null)
    {
     for (node = _prev; node != null && node._prev != null; node = node._prev) {}
    }
   return (node != null)? node._lpexView : null;
  }

  // Finds document chain's previous view.
  LpexView prevLpexView()
  {
   LpexViewNode node = _prev;
   if (node == null)
    {
     for (node = _next; node != null && node._next != null; node = node._next) {}
    }
   return (node != null)? node._lpexView : null;
  }

  // Finds the master view's delegate in this Lpex view's chain.
  Delegate findDelegate()
  {
   for (LpexViewNode node = _prev; node != null; node = node._prev)
    {
     if (node._lpex == null)
      {
       return node._delegate;
      }
    }
   return null;
  }
 }
}


/**
 * This class extracts the Lpex parameters.
 */
final class LpexParameters
{
 private String _filename;      // file name
 private String _encoding = ""; // defaults to autodetect file's encoding
 private String _documentType;  // overriding document type (e.g., file-name extension)

 LpexParameters(String[] parms)
 {
  for (int i = 0; i < parms.length; i++)
   {
    // parameter?
    if (i < parms.length-1 && (parms[i].startsWith("-") || parms[i].startsWith("/")))
     {
      String parm = parms[i].substring(1);
      if ("enc".equals(parm) || "encoding".equals(parm))
       {
        _encoding = parms[++i];
        continue;
       }
      else if ("dt".equals(parm) || "extension".equals(parm))
       {
        _documentType = parms[++i];
        continue;
       }
     }

    // file name?
    if (i == 0)
     {
      _filename = LpexStringTokenizer.trimQuotes(parms[i]);
     }
    // unrecognized parameter
    else
     {
      break;
     }
   }
 }

 String filename() { return _filename; }
 String encoding() { return _encoding; }
 String documentType() { return _documentType; }
}