/**************************************************************************
*
*  @@@BUILDINFO@@@ 35omvData-2.jsx 2.0.1.63  27-Mar-2007
*  Copyright 2006-2007 Adobe Systems Incorporated
*  All Rights Reserved.
*
* NOTICE:  All information contained herein is, and remains the property of
* Adobe Systems Incorporated  and its suppliers,  if any.  The intellectual 
* and technical concepts contained herein are proprietary to  Adobe Systems 
* Incorporated  and its suppliers  and may be  covered by U.S.  and Foreign 
* Patents,patents in process,and are protected by trade secret or copyright 
* law.  Dissemination of this  information or reproduction of this material
* is strictly  forbidden  unless prior written permission is  obtained from 
* Adobe Systems Incorporated.
**************************************************************************/

// Object Model Viewer Data.
// For dynamic targets, the ESTK stores the mined XML into app.prefsFolder.
// The file name is omv$btid$dictname.xml, e.g. omv$indesign-5.0-en_us$main.xml.

// Create an OMV data provider. Takes the BridgeTalk ID of the target.
// Properties:
// btID			the full BridgeTalk ID if installed, the bare name if not
// xml			the original XML
// map			a copy of the XML map, single-level
// dict			the dictionary name for dynamic mining, null otherwise
// file			a File instance pointing to the OMV file to load
// title		the title of the TOC
// dynamic		true if the target is dynamic and can be mined
// loaded		true if the xml is complete
// menu			the menu command item (if we need to disable)
// ui			null if no UI yet, the OMV instance otherwise
// asked		true if the user has been asked if he wants to load the OMV data
// loadOK		true if loading is OK because user agreed
// dlg			a progress dialog if present; if aborted, this becomes null
// showProgress	true if the OMV UI should show a progress dialog

function OMVData (btID, dictOrFile)
{
	this.btID		= btID;
	this.dict		= (typeof dictOrFile == "string") ? dictOrFile : null;
	this.file		= (dictOrFile instanceof File) ? dictOrFile : null;
	this.menu		= null;
	this.ui			=
	this.xml		=
	this.map		= null;
	this.loaded		= false;
	this.dynamic	= (this.dict != null);
	this.asked		=
	this.loadOK		= (btID == "estoolkit-2.0");
	this.dlg		= null;
	this.showProgress = !this.loadOK;

	OMVData.setData (this);
}

// The OMVData.instances object contains all loaded OMV data stored under
// their BT name, either with the simple BT name, or with a '$' and a
// dictionary name attached. Each entry is an array, filled with all main
// OMVs (OMVs containing a title attribute in their map element).

OMVData.instances = {}

// The OMVData.files object contains all OMV instances stored under the
// files that the OMV has loaded. Each entry is the OMV instance.

OMVData.files = {}

// This is the location of where to create the next menu item.

OMVData.menuItemLocation = "at the beginning of help";

// This is a running count for the creation of menu IDs.

OMVData.menuItemCount = 0;

// An object containing the last target, engine, text, and result for getCodeHints().

OMVData.lastFindInfo = { target:"", engine:"", text:"", found:null };

// Set up all OMV entries in the Help menu. When a menu item is selected,
// the OMV data is loaded, and the UI is created or made visible.

OMVData.setup = function()
{
	// Collect all XML info into two groups. 
	// Each element contains an object:
	// { dict:dictName, title:theTitle, btid:BridgeTalkID }
	var groups = [
		// Group 0 is for CommonFiles
		[],
		// Group 1 is for the other files. 
		[]
	];
	var apps = BridgeTalk.getTargets();
	for (var i = 0; i < apps.length; i++)
	{
		var app = BridgeTalk.getSpecifier (apps [i]);
		if (!app)
			continue;
		switch (BridgeTalk.getOMVDictionaryType (app))
		{
			case "dynamic":
				var obj = { btid:app, dict:"" };
				var name = this.getDisplayName (app);
				obj.title = localize ("$$$/ESToolkit/OMV/Title=%1 Object Model", name);
				groups[1].push (obj);
				break;
		}
	}
	// Add all directories inside the Dictionaries folder
	var dictFolder = Folder (Folder.commonFiles + "/Adobe/Scripting Dictionaries CS3");
	var folders = dictFolder.getFiles();
	for (var i = 0; i < folders.length; i++)
	{
		var f = folders [i];
		if (!(f instanceof Folder))
			continue;

		var files = f.getFiles ("*.xml");
		for (var j = 0; j < files.length; j++)
		{
			// the title of the map, and the (optional) dictionary name
			var title = null, dictName = null;

			// Attempt to load the beginning of the XML file,
			// to find the official reference name plus any dynamic dict info
			var xmlfile = files [j];
			var resolved = xmlfile.resolve();
			if (resolved)
				xmlfile = resolved;
			// Open this file, and read the first 1024 bytes to find the <map> element
			if (!xmlfile.open())
				// IO error - ignore
				continue;

			var s = xmlfile.read (1024);
			xmlfile.close();
			var re = /<map +title="(.+?)"/;
			title = re.exec (s);
			if (!title)
				// no title; this must be a sub-XML file
				continue;
			
			// got the title
			title = title[1];
			// determine the group; group 1 is for CommonFiles, group 2 for everything else
			var btid = f.name;
			var grp = 1;
			if (btid.toUpperCase() == "COMMONFILES")
				grp = 0, btid = "estoolkit-2.0";
			else
				btid = BridgeTalk.getSpecifier (btid);

			if (!btid)
				// use this for installed OMVs withput the app installed
				btid = f.name;

			// create the object for the group, with a file as dict
			groups [grp].push ( { dict:xmlfile, title:title, btid:btid } );
		}
	}
	
	// Sort the groups by title
	function sortfn (a, b)
	{
		if (a.title < b.title)
			return -1;
		else if (a.title > b.title)
			return 1;
		else
			return 0;
	}
	for (grp = 0; grp < 2; grp++)
		groups [grp].sort (sortfn);
		
	// We got all groups lined up; now, set up the menu
	for (grp = 0; grp < 2; grp++)
	{
		for (var i = 0; i < groups [grp].length; i++)
		{
			var obj = groups [grp][i];
			// At the beginning of each group, insert a separator (except for group #0)
			if (0 != grp && 0 == i)
				OMVData.menuItemLocation = "---" + OMVData.menuItemLocation;
			var data = new OMVData (obj.btid, obj.dict);
			this.createMenuItem (data, obj.title);
		}
	}
}

// Create a menu item. Returns the ID

OMVData.createMenuItem = function (data, title)
{
	OMVData.menuItemCount++;
	var menuID = "help/omv" + OMVData.menuItemCount;
	
	var item = new MenuElement ("command", title, OMVData.menuItemLocation, menuID);
	OMVData.menuItemLocation = "after " + menuID;
	
	// connect the data
	data.menu = item;
	item.data = data;
	item.onSelect = function()
	{
		if (!this.data.show())
			this.enabled = false;
	}
}

// Display the OMV instance for viewing.

OMVData.prototype.show = function()
{
	// If the user wants to see, we assume that loading is OK.
	this.asked		= 
	this.loadOK	= true;
	if (!this.load())
		return false;
	else if (this.ui)
		this.ui.w.show();
	else
		this.ui = new OMV (this);
	return true;
}

// Load the data. If the data is dynamic, mine the target.

OMVData.prototype.load = function()
{
	if (!this.asked)
	{
		this.asked = true;
		var name = this.getDisplayName();
		this.loadOK = (PrefUtils.getValue( 'prefs.omv.confirmLoad', 'Boolean' ))
					? queryBox ("$$$/ESToolkit/OMV/NotLoaded=The Object Model for %1 has not yet been loaded.^nLoad it now?", name)
					: true;
	}
	if (this.loadOK && !this.loaded)
	{
		// Do progress messages for all OMV files except for the CommonFiles
		if (this.showProgress)
		{
			var msg = localize ("$$$/ESToolkit/OMV/LoadingObjectModel=Loading Object Model for %1...", this.getDisplayName());
			this.dlg = new ProgressBox (msg);
			this.dlg.setProgress (0, 100);
			this.loadXML();
			this.dlg.stop();
			this.dlg = null;
		}
		else
			this.loadXML();
	}
	return this.loaded && (this.xml != null);
}

// Create a File instance pointing to an XML file in the Prefs area.
// The name is "omv$btid.xml", or "omv$btid$dict.xml", where "btid"
// is the full BT ID, and "dict" is the dictionary name if given.

OMVData.prototype.getTempXMLFile = function()
{
	var s = app.prefsFolder + "/omv$" + this.btID;
	if (this.dict)
		s += "$" + this.dict;
	return File (s + ".xml");
}

// Save the XML to the prefs area.

OMVData.prototype.saveTempXML = function()
{
	// save the mined XML
	var f = this.getTempXMLFile();
	if (f.open ("w"))
	{
		if (this.dlg)
			this.dlg.setText ('$$$/ESToolkit/OMV/Saving=Saving Object Model...');
		// stamp the time if not given
		if (this.xml.map.@time.toString() == "")
			this.xml.map.@time = new Date().toString();
		f.encoding = "UTF-8";
		f.write (this.xml.toXMLString());
		f.close();
		if (f.error != "")
			f.remove();
		else
			this.file = f;
	}
}

// Get the display name for this OMV data instance

OMVData.prototype.getDisplayName = function()
{
	return OMVData.getDisplayName (this.btID);
}

// Get the display name for a BT specifier

OMVData.getDisplayName = function (btID)
{
	var name = BridgeTalk.getDisplayName (btID);
	if (!name)
	{
		name = btID.split ('-')[0];
		if (name == "aftereffects")
			name = "After Effects";
		else
			// just make 1st character upper case
			name = name[0].toUpperCase() + name.substr (1);
	}
	return name;
}

// Get the OMVData instance(s) for a specific target and engine.
// In this context, it is assumed that the engine matches the
// name of the dictionary to get.

OMVData.getData = function (target, engine)
{
	target = target.split('-')[0];

	// Pass 1: the full BT target
	var targetID = target + "-" + engine;
	var omv = OMVData.instances [targetID];
	if (!omv)
		omv = OMVData.instances [target];
	return omv;
}

// Get the OMVData instance(s) for a specific XML.

OMVData.getDataForXML = function (xml)
{
	// find the root
	while (xml.parent() != null)
		xml = xml.parent();

	for (var prop in OMVData.instances)
	{
		var arr = OMVData.instances [prop];
		for (var i = 0; i < arr.length; i++)
		{
			var data = arr [i];
			if (data.xml == xml)
				return data;
		}
	}
	return null;
}

// Set the OMVData instance for a specific target and dictionary.
// In this context, it is assumed that the engine matches the
// name of the dictionary to get. Since the OMV is only running
// in one locale and for one version (we loaded from "Scripting
// Dictionaries CS3"), we use the BT base name to store the data

OMVData.setData = function (data)
{
	var btID = data.btID.split ('-')[0];
	var targetID = btID;
	if (data.dict)
		targetID += "-" + data.dict;
	if (!OMVData.instances [targetID])
		OMVData.instances [targetID] = [];
	OMVData.instances [targetID].push (data);
	if (data.dict)
	{
		// for the first (or main) dictionary, set the global entry, too
		// because the debugger does not know yet which engines it will have
		if (data.dict == "main")
		{
			if (!OMVData.instances [btID])
				OMVData.instances [btID] = [];
			OMVData.instances [btID].push (data);
		}
	}
}

// Checks if the OMV for a specific target/engine has been loaded.

OMVData.checkForTarget = function (target, engine)
{
	return (OMVData.getData (target, engine) != null);
}

// Check the time stamp for a target if that target is dynamic and running.
// On errors, the Bad XML dialog pops up, and this.xml is set to null.

OMVData.checkTimeStampForTarget = function (target, engine)
{
	var omv = OMVData.getData (target, engine);
	if (omv)
	{
		for (var i = 0; i < omv.length; i++)
		{
			var xml = omv [i].xml;
			if (xml)
			{
				if (omv [i].checkTimeStamp())
				{
					if (omv [i].xml != xml)
					{
						// Stuff was updated
						if (omv [i].omv)
							omv [i].omv.detach();
					}
				}
			}
		}
	}
}


// Find code completion text for a specific target/engine combo. This
// works only if the name of a dictionary matches the name of an engine.

OMVData.findTextForTarget = function (target, engine, text)
{
	var text = null;
	var omv = OMVData.getData (target, engine);
	if (omv)
	{
		for (var i = 0; i < omv.length; i++)
		{
			text = omv[i].findText (text);
			if (text)
				break;
		}
	}
	return text;
}

// Load the XML. If there is no installed file,
// attempt to connect to the target and ask the target for its TOC.
// In that case, the XML is incomplete, and the app is asked for
// each class.

OMVData.prototype.loadXML = function ()
{
	var result = false;

	this.xml = new XML('<dictionary><map/><package/></dictionary>');

	// Load all XML files from disk, and merge their contents.
	// The returned value is true for OK, false for errors.
	// For dynamic targets, the ESTK loads the mined XML into app.prefsFolder.
	// The file name is omv$btid$dictname, e.g. omv$indesign-5.0-en_us$main.xml.

	// Look after a temp file first
	var tempFile = this.getTempXMLFile();
	if (!tempFile.exists)
		tempFile = null;

	var f = tempFile ? tempFile : this.file;

	if (f)
	{
		// The progress is set up as follows:
		// 20% = loading and parsing all files
		// 80% = preparing XML
		// To have a good resolution, we set the max value to (#files*100), and increment by 2 * 10
		if (this.dlg)
			this.dlg.setProgress (0, 100);

		var text = Document.safeRead (f, true);
		var ok = false;
		if (text)
		{
			ok = true;
			if (this.dlg)
				this.dlg.setProgress (10);
			try
			{
				text = text.replace (/<dictionary.*>/, "<dictionary>");
				this.xml = new XML (text);
			}
			catch (e)
			{
				ok = false;
			}
		}
		if (!ok)
		{
			// XML error: ignore the temp file
			if (tempFile)
			{
				tempFile.remove();
				tempFile = null;
			}
			else
			{
				this.badXML ("XML file format error");
				return false;
			}
		}
		if (this.dlg)
			this.dlg.setProgress (20);

		if (!this.checkTimeStamp())
			return false;

		// Load sub-XMLs
		if (!tempFile && !this.loadSubXMLFiles (f))
		{
			this.badXML ("Bad sub XML files");
			return "error";
		}

		this.loaded = true;

		if (this.prepareXML())
		{
			this.file = f;
			// Register this OMV under the file name
			OMVData.files [this.file.absoluteURI] = this;
			return true;
		}
		else
			return false;
	}
	else
	{
		if( this.dynamic && !this.file )
		{
			if (!BridgeTalk.isRunning (this.btID))
			{
				var launchMsg = "$$$/ESToolkit/OMV/Launch=%1 must be running to retrieve Object Model data.^nDo you want to launch %1?";
				if (!app.launchTarget (this.btID, launchMsg))
					return false;
			}
		}
		
		// no temp file yet
		if (this.dynamic && this.checkTimeStamp())
		{
			if (this.prepareXML())
			{
				// Register this OMV under the file name
				OMVData.files [this.file.absoluteURI] = this;
				return true;
			}
		}

		this.badXML ("Cannot open " + f);
		return false;
	}
}

// Load all sub-XML files. These files contain additional information. They are
// found in a subdirectory carrying the same name as the main XML file. The names
// of these files are irrelevant. Their XML is merged into the main XML.

OMVData.prototype.loadSubXMLFiles = function (f)
{
	// strip the .xml part
	var dir = Folder (f.fsName.substr (0, f.fsName.length - 4));
	var files = dir.getFiles ("*.xml");
	if (0 == files.length)
		return true;
	// The progress is set up as follows:
	// 20% = loading and parsing main files
	// 60% = loading and parsing sub files
	// 20% = preparing XML
	if (this.dlg)
		this.dlg.setProgress (20);
	var progressValue = 20;
	var increment = 60 / (files.length * 2);
	for (var i = 0; i < files.length; i++)
	{
		f = files [i];
		var resolved = f.resolve();
		if (resolved)
			f = resolved;
		if (f.exists)
		{
			if (this.dlg)
				this.dlg.setProgress (progressValue);
			progressValue += increment;
			var text = Document.safeRead (f, true);
			if (text == null)
				return false;
			var xml;
			try
			{
				// Remove all namespaces
				text = text.replace (/<dictionary.*>/, "<dictionary>");
				xml = new XML (text);
			}
			catch (e)
			{
				return false;
			}
			if (this.dlg)
				this.dlg.setProgress (progressValue);
			progressValue += increment;
			var globals = xml.map.topicref.xpath ('topicref[@href="#/global"]');
			if (globals.length() == 1)
			{
				// Update my own <map> element with the globals if not present
				delete globals.parent()[globals.childIndex()];
				var myGlobals = this.xml.map.topicref.xpath ('topicref[@href="#/global"]')
				if (myGlobals.length() == 0)
					this.xmp.map.appendChild (globals);

				// Add the globals instance stuff to my globals
				globals = xml['package'].xpath ('classdef[@name="global"]');
				myGlobals = this.xml['package'].xpath ('classdef[@name="global"]');
				if (globals.length() > 0)
				{
					if (myGlobals.length() == 0)
						this.xml['package'].appendChild (globals);
					else
					{
						var inst = globals.xpath ('elements[@type="instance"]');
						var myInst = myGlobals.xpath ('elements[@type="instance"]');
						if (myInst.length() == 0)
							myGlobals.appendChild (inst);
						else
							myInst.appendChild (inst.children());
					}
					// remove from the original tree
					delete globals.parent() [globals.childIndex()];
				}
			}
			// Add the other classes
			this.xml.map.appendChild (xml.map.children());
			this.xml['package'].appendChild (xml['package'].children());
			// Register this OMV under the file name
			OMVData.files [f.absoluteURI] = this;
		}
		else
			return false;
	}
	return true;
}

// Create a single-level TOC, ignoring the top level structure for now, and sort the TOC.
// Store this TOC under this.map. Returns false on abort.

OMVData.prototype.prepareXML = function()
{
	if (!this.xml)
		return false;

	var pkg = this.xml['package'];
	// Get rid of the level 1 entries for now
	var list = this.xml.map.children().children();
	// Remove duplicates
	this.map = XML ('<map/>');
	// Set up the classes at the OMV object
	this.classNames = {};

	var increment = this.dlg ? this.dlg.getRemainingProgress() / list.length() : 0;
	for (var i = 0; i < list.length(); i++)
	{
		if (this.dlg && !this.dlg.increment (increment))
		{
			this.badXML ("Load Aborted");
			return false;
		}
		var name = list [i].@navtitle.toString();
		if (this.classNames [name])
			continue;
		this.classNames [name] = list [i].@href.toString();
		this.map.appendChild (list [i]);
	}
	return true;
}

// Get XML from a target with a sync call. Returns null on errors,
// the XML otherwise.

OMVData.prototype.getXMLFromTarget = function (commandXML, noErrorMsg)
{
	if (!BridgeTalk.isRunning (this.btID))
	{
		var launchMsg = "$$$/ESToolkit/OMV/Launch=%1 must be running to retrieve Object Model data.^nDo you want to launch %1?";
		if (!app.launchTarget (this.btID, launchMsg))
			return false;
	}
	var bt = new BridgeTalk;
	bt.type = "Debug";
	bt.target = this.btID + "#estk";
	bt.body = commandXML;
	bt.data = this;
	bt.xml = null;
	// Keep a reference here for async IO garbage collection inhibit
	this.bt = bt;

	bt.onResult = function (bt)
	{
		this.data.bt = null;
		try
		{
			this.xml = new XML (bt.body);
		}
		catch (e) 
		{
			this.xml = null;
		}
	}
	bt.onError = function (bt)
	{
		this.data.bt = null;
		this.xml = null;
	}
	bt.send();

	var then = new Date;
	while (this.bt)
	{
		var now = new Date;
		if (now - then > 15000)
			break;
		BridgeTalk.pump();
		$.sleep (20);
	}
	if (!bt.xml && !noErrorMsg)
		this.badXML ("App did not return valid XML");
	return bt.xml;
}

// Error message for bad XML.

OMVData.prototype.badXML = function (reason)
{
	if (this.dlg)
		this.dlg.stop();

	errorBox ('$$$/ESToolkit/Alerts/BadXML=Cannot load XML!\nHelp is not available.');
	this.xml = null;
	this.loaded = true;
	if (this.menu)
		this.menu.enabled = false;
	if (this.ui)
		this.ui.disable();

	if ($.version.indexOf ("(debug)") > 0)
	{
		print ("Cannot load XML! Reason: " + reason);
		print ($.stack);
	}
}

// Check the time stamp in a loaded TOC from temp XML and a running dynamic target.
// The XML is assumed to have been loaded from a temp file already.
// If the XML needs to be re-mined, do so immediately without asking.
// The progress bar is set to 20%.

OMVData.prototype.checkTimeStamp = function()
{
	if (this.dlg)
		this.dlg.setProgress (20, 100);

	if (this.dynamic && BridgeTalk.isRunning (this.btID))
	{
		var map = this.getXMLFromTarget ('<GetTOC prefix="#/" dictname="' + this.dict + '"/>');
		if (map)
		{
			var oldStamp = this.xml ? this.xml.map.@time.toString() : "";
			var newStamp = map.@time.toString();
			if (newStamp.length)
			{
				// Always re-mine if the dynamic TOC had a stamp, but the loaded XML had not
				var remove = true;
				if (oldStamp.length)
				{
					var oldDate = new Date (oldStamp);
					var newDate = new Date (newStamp);
					remove = (newDate > oldDate);
				}
				if (remove)
				{
					this.getTempXMLFile().remove();

					// Mine the target. 

					this.xml = null;

					// Try to load the entire dictionary in one chunk
					var xml = this.getXMLFromTarget ('<classinfo prefix="#/" dictname="' + this.dict + '"/>', true);
					if (xml)
					{
						// good load
						this.xml = xml;
						if (this.dlg)
							this.dlg.setProgress (40);
					}
					else
					{
						// old code: mine one by one. May be removed once clients pick up ES 3.7.63
						this.xml	 = new XML ('<dictionary/>');
						this.xml.map = map;
						this.xml.appendChild (XML ('<package/>'));

						var topics = map.topicref.children();
						var count = topics.length();
						// We use 60% (we had 20%, and leave 20% for prepareXML())
						var progressValue = 20;

						for (var i = 0; i < count; i++)
						{
							if (this.dlg && !this.dlg.setProgress (progressValue))
							{
								this.badXML ("Load aborted");
								return false;
							}
							progressValue += (60 / count);
							var topic = topics [i];
							var href = this.splitHref (topic.@href.toString());
							// ignore the package for now
							var xml = this.getXMLFromTarget ('<GetClassInfo prefix="#/" dictname="' + this.dict + '">'
															+ '<element>' + href.cls + '</element></GetClassInfo>');
							if (xml)
							{
								// pull out of a "package" container
								if (xml.localName() == "package")
									xml = xml.children();
								this.xml ['package'].appendChild (xml);
							}
							else
							{
								this.badXML ("Target did not return class info for " + href.cls);
								return false;
							}
						}
					}	// end old code
					if (this.xml)
					{
						var title = this.xml.map.@title.toString();
						if (title == "")
						{
							title = localize ("$$$/ESToolkit/OMV/Title=%1 Object Model", this.getDisplayName());
							this.xml.map.@title = title;
						}
						this.dynamic = false;
						this.loaded = true;
						this.saveTempXML();
					}
				}
			}
		}
	}

	return true;
}

// Find XML. The search string could be a simple string (which could be a
// class name or an element name), or a dot-separated string for class and
// element name. Additional patterns are *.element, .element, class. or
// class.*. If the class XML is not found, a getClassInfo request is sent
// to any dynamic target.
// Returns the found XML (class or element XML) or null.

OMVData.prototype.findXML = function (what, wholeWord)
{
	if (!this.xml)
		return null;

	var className =  null;
	var elementName = null;
	var classWholeWord = wholeWord;
	var elementWholeWord = wholeWord;
	var findText = what.split ('.');
	if (findText.length == 2)
	{
		// Looks like it is both class and element
		// but the class name could be empty or a star
		if (findText[0] == "" || findText[0] == "*")
			elementName = findText [1];
		else
		{
			classWholeWord = true;
			className = findText[0];
			// any element given?
			elementName = (findText[1] != "" && findText[1] != "*") ? findText [1] : "";
		}
	}
	else
	{
		className = findText [0];
		// do not search for "" or "*" (too many results)
		if (className == "" || className == "*")
			return null;
		// if the first character if lower case, it must be an element
		if (className.toLowerCase() == className && className [0] != "$" && className [0] != '_')
			elementName = className, className = null;
	}

	// The data has been set up, but remember that an upper-case
	// class name could as well be an upper-case element name
	var xml = null;
	
	if (className != null && elementName == null)
	{
		xml = this.xml['package'].xpath (classWholeWord
			? "* [@name = '" + className + "']"
			: "* [starts-with (@name, '" + className + "')]");
		// Also assume an element name!
		elementName = className;
		className = null;
	}
	if (elementName != null)
	{
		var xml2;
		if (className)
			xml2 = this.xml['package'].xpath ("* [@name = '" + className + "']");
		else
			xml2 = this.xml['package'].children();
		xml2 = xml2.elements.children();
		if (elementName != "")
		{
			var xpathExpr = elementWholeWord
				? "self::node() [@name = '" + elementName + "']"
				: "self::node() [starts-with (@name, '" + elementName + "')]";
			xml2 = xml2.xpath (xpathExpr);
		}
		if (xml2)
		{
			if (xml)
				xml += xml2;
			else
				xml = xml2;
		}
	}
	if (xml.length() == 0)
		return null;
	else
		return xml;
}

// Create a HREF string for the given XML.
// This link has the form [file]#/Classname[/Containertype/Elementname].

OMVData.prototype.getHrefForXML = function (xml)
{
	var href = "";
	switch (xml.localName())
	{
		case "method":
		case "property":
			href = "#/" + xml.parent().parent().@name 
				 + "/"  + xml.parent().@type
				 + "/"  + xml.@name;
			break;
		default:
			href = "#/" + xml.@name;
			break;
	}
	return href;
}

// Get the XML for a given HREF link.

OMVData.prototype.getXMLForHref = function (href)
{
	if (!this.xml)
		return null;

	href = this.splitHref (href);

	// ignore package for now
	// Get the class
	var xml = this.xml['package'].classdef.xpath ('self::node()[@name="' + href.cls + '"]');
	if (href.type && href.name)
		xml = xml.xpath ('self::node()[@type="' + href.type + '"]/*[@name="' + href.name + '"]');

	if (xml.length() == 0)
		xml = null;
	return xml;
}

// Split up a HREF. Returns an object with these properties:
// file - the file portion, "" if no file portion
// pkg  - the package name, "" for no package name
// cls  - the class name
// type - the element container type (e.g. class, instance, "" if not present)
// name - the element name ("" if not present)

OMVData.prototype.splitHref = function (href)
{
	var obj = { file:"", cls:"", type:"", name:"" };

	var file = href.split ('#');
	if (file.length == 1)
		file [1] = "";
	if (file.length > 1)
	{
		if (file [0].length)
		{
			// Change the file part to a file that we hope will exist
			var f = file[0].split ('/');
			var folder = Folder.commonFiles + "/Adobe/Scripting Dictionaries CS3/";
			if (f [0] == "$COMMON")
			{
				f.shift();
				obj.file = folder + "CommonFiles/" + f.join ('/');
			}
			else if (file [0].length)
			{
				// This is the root directory
				var f = this.file.parent;
				if (!f)
					f = File ("/");
				f.changePath (file [0]);
				if (!f.exists)
				{
					// OK, it is not in the same folder as the main file. Try
					// the subfolder containing the same name
					f = this.file.fsName;
					// just assume that the extension is .xml (= 4 chars)
					f = new Folder (f.substr (0, f.length - 4));
					f.changePath (file [0]);
				}
				obj.file = f.fsName;
			}
		}
		href = file [1];
	}
	else
		href = file [0];

	// just in case someone forgot to add the leading slash
	if (href.length == 0)
		// assume all classes
		href = "/";
	else if (href[0] != '/')
		href = '/' + href;
	href = href.split ('/');
	obj.pkg = href [0];
	if (href.length > 1)
		obj.cls = href [1];
	if (href.length > 2)
	{
		// if the container type is missing, assume "instance"
		if (href.length == 3)
			obj.type = "instance",
			obj.name = href [2];
		else
			obj.type = href [2],
			obj.name = href [3];
	}
	return obj;
}

// Piece a HREF together from the object returned by splitHref()

OMVData.prototype.joinHref = function (href)
{
	var file = href.file;
	if (file)
	{
		var folder = Folder.commonFiles + "/Adobe/Scripting Dictionaries CS3/";
		if (file.indexOf ("CommonFiles") == folder.length)
			file = "$COMMON" + file.substr (folder.length + "CommonFiles".length);
		else
		{
			var f = File (file);
			file = f.getRelativeURI (folder);
		}
	}
	// Rebuild the HREF
	var newHref = file + "#" + href.pkg + "/" + href.cls;
	if (href.name)
		newHref += "/" + href.type + "/" + href.name;
	return newHref;
}

// Code Completion interface.

OMVData.getCodeHints = function (target, engine, text)
{
	if (OMVData.lastFindInfo.target == target
	 && OMVData.lastFindInfo.engine == engine
	 && OMVData.lastFindInfo.target == text)
		return OMVData.lastFindInfo.found;

	if (!OMVData.load (target, true))
		return null;
	if (!OMVData.load ("estoolkit-2.0", true))
		return null;

	// Create a complete array of all OMVs
	var data = [];
	var appData  = OMVData.getData (target, engine);
	var coreData = OMVData.getData ("estoolkit");
	if (appData)
	{
		for (var i = 0; i < appData.length; i++)
			data.push (appData [i]);
	}
	if (coreData && coreData != appData)
	{
		for (i = 0; i < coreData.length; i++)
			data.push (coreData [i]);
	}

	var xml = XML('<></>');
	var count = 0;

outer:
	for (i = 0; i < data.length; i++)
	{
		var partXML = data [i].findXML (text);
		if (partXML)
		{
// turn off tracking as per Alan's request
//			if (data[i].ui)
//				data[i].ui.displayXML (partXML);
			for (var j = 0; j < partXML.length(); j++)
			{
				xml += partXML[j];
				if (++count > 20)
					break outer;
			}
		}
	}

	// return an array of strings for Code Completion
	var returnArray = [];
	// Add an xml{} object to the found info, where each XML is stored
	// as a property under the generated array element's text, plus
	this.lastFindInfo = { target:target, engine:engine, text:text, found:returnArray, xml: {} };

	if (xml)
	{
		for (var i = 0; i < xml.length(); i++)
		{
			var elem = xml [i];
			var name;
			if (elem.localName() == "classdef")
				name = elem.@name.toString();
			else
			{
				name = elem.parent().parent().@name;
				// Avoid global.name
				if (name == "global")
					name = elem.@name.toString();
				// Avoid Array.Array
				else if (name != elem.@name)
					name += "." + elem.@name;
				if (elem.localName() == "method")
				{
					var args = "(";
					var params = elem.parameters.children();
					for (var j = 0; j < params.length(); j++)
					{
						if (j)
							args += ", ";
						args += params [j].@name;
					}
					args += ")";
					if (args.length > 2)
						name += " ";
					name += args;
				}
			}
			var help = this.flatten (elem.shortdesc);
			if (help.length)
				name += ": " + help;
			returnArray.push (name);
			this.lastFindInfo.xml [name] = elem;
		}
		returnArray.sort();
		if (count > 20)
			returnArray.push ("...");
	}

	return returnArray;
}

// Update any UI with the code hint selected. The given text
// is the original text of the array element that was returned.

OMVData.displaySelectedCodeHint = function (target, engine, text)
{
	if (OMVData.lastFindInfo.target != target
	 || OMVData.lastFindInfo.engine != engine)
		return;

	// get the XML
	var xml = OMVData.lastFindInfo.xml [text];
	if (!xml)
		return;

	// get the OMVData that own this XML
	var data = OMVData.getDataForXML (xml);
	if (data && data.ui)
		data.ui.displayXML (xml);
}

// Flatten an XML text to a string.

OMVData.flatten = function (xml)
{
	var s = "";
	xml = xml.children();
	for (var i = 0; i < xml.length(); i++)
	{
		if (s.length)
			s += " ";
		if (xml[i].nodeKind() == "text")
			s += xml[i];
		else
			s += this.flatten (xml [i]);
	}
	return s;
}

// Load the OMV for the given target. Called when the debugger connects.
// Check if the OMVData is loaded, and, if not, attempt to load this data.
// The debugger calls this method to load data for dynamic targets (if the
// target is dynamic at all).

OMVData.load = function (target, loadAll)
{
	// We skip the own debugger here to save load time
	// That OMV is loaded later on during Code Completion
	if (!loadAll && target == "estoolkit-2.0")
		return true;

	// TODO: implement multiple dictionaries
	var data = OMVData.getData (target, "");
	if (!data)
		return false;

	for (var i = 0; i < data.length; i++)
	{
		if (!data [i].load())
			return false;
	}
	return true;
}

// Get the OMV for a specific file. If not present, attempt to load it.

OMVData.getByFile = function (f)
{
	if (!(f instanceof File))
		f = File (f);

	if (!f.exists)
		return null;

	// Make sure that we have preloaded the default stuff
	if (!OMVData.load ("estoolkit-2.0", true))
		return null;
	
	var omv = OMVData.files [f.absoluteURI];
	if (!omv)
	{
		// The file's parent should be the basic BT name
		var btid = f.parent ? BridgeTalk.getSpecifier (f.parent.name) : NULL;
		if (btid)
			omv = new OMVData (btid, f);
	}
	return omv;
}
