/*
	Include in output:

	This file is part of Natural Docs, which is Copyright © 2003-2022 Code Clear LLC.
	Natural Docs is licensed under version 3 of the GNU Affero General Public
	License (AGPL).  Refer to License.txt or www.naturaldocs.org for the
	complete details.

	This file may be distributed with documentation files generated by Natural Docs.
	Such documentation is not covered by Natural Docs' copyright and licensing,
	and may have its own copyright and distribution terms as decided by its author.
*/


// Tab Members, from data file

	$Tab_Type = 0;
	$Tab_HTMLTitle = 1;
	$Tab_HashPath = 2;
	$Tab_DataFile = 3;

// Tab Members, added by JavaScript

	$Tab_WideWidth = 4;
	$Tab_NarrowWidth = 5;
	$Tab_LastOffsets = 6;

// Entries

	$Entry_Type = 0;
	$Entry_HTMLTitle = 1;
	$Entry_HashPath = 2;
	$Entry_Members = 3;

	$EntryType_Target = 1;
	$EntryType_Container = 2;

// Constants

	$MaxMenuSections = 10;


"use strict";


/* Class: NDMenu
	___________________________________________________________________________

	Loading Process:

		- When <OnLocationChange()> or <GoToOffsets()> need the menu to be updated, they call <Build()> with the path to
		  the current selection.

		- If all the data needed exists in <menuSections> a new menu is generated.

		- If some of the data is missing <Build()> displays what it can, stores the selection path in <pathBeingBuilt>, requests to
		  load additional menu sections, and returns.  When the data comes back via <OnSectionLoaded()> that function
		  will call <Build()> again.  <Build()> will either finish the update, request more parts of the menu, or just wait for previously
		  requested data to come in.

		- This system allows the user to click a different file before everything finishes loading.  <Build()> will be called again and
		  <pathBeingBuilt> will be replaced or set back to undefined.  If the previous click's data comes in after the new navigation
		  has been completed, <Build()> won't do anything because it cleared <pathBeingBuilt> already.  If the previous click's data
		  comes back but isn't relevant to the new click which is still loading, <Build()> will just rebuild the partial menu with the new
		  click's path to no detrimental effect.

	Caching:

		The class stores loaded sections in <menuSections>, hanging on to them until it grows to $MaxMenuSections, at which
		point unused entries may be pruned.  The array may grow larger than $MaxMenuSections if required to display everything.

		Which entries are in use is tracked by the <firstUnusedMenuSection> index.  This is reset when <Build()> starts, and every
		call to <MenuSection()> rearranges the array and advances it so that it serves as a dividing line.  If the path being built
		is walked start to finish, everything in use will be in indexes below <firstUnusedMenuSection>.

		The array is rearranged so that it gets put in MRU order.  When the array grows too large unused entries can be plucked off the
		back end and the least recently used ones will go first.

		There's another trick to it though.  When <MenuSection()> returns an entry that was past <firstUnusedMenuSection>,
		it doesn't move it to the head of the array, it moves it to the back of the used list.  Why?  Because the paths are walked from
		root to leaf, meaning if you had path A > B > C > D and you just moved entries to the head, they would end up in the cache in
		this order:

		> [ D, C, B, A ]

		So then let's say you navigated from that to A > B > C2 > D2.  The cache now looks like this, with | representing the
		used/unused divider:

		> [ D2, C2, B, A | D, C ]

		C would get ejected from the cache before D, but since D is below C in the hierarchy its presence in the cache is not especially
		useful.  So instead we move entries to the end of the used list, which keeps them in their proper order.  Going to A > B > C > D
		results in this cache:

		> [ A, B, C, D ]

		and then navigating to A > B > C2 > D2 results in this cache:

		> [ A, B, C2, D2 | C, D ]

		D would get ejected before C.  We now have a MRU ordering that also implicitly favors higher hierarchy entries instead of lower
		ones which aren't valuable without their parents.

*/
var NDMenu = new function ()
	{

	// Group: Functions
	// ________________________________________________________________________


	/* Function: Start
	*/
	this.Start = function ()
		{
		// this.pathBeingBuilt = undefined;

		this.menuSections = [ ];
		this.firstUnusedMenuSection = 0;

		// this.tabs = undefined;
		// this.selectedTabType = undefined;


		// Create the sub-containers since they have to be in the right order regardless of the order their data is loaded in.

		var menuContainer = document.getElementById("NDMenu");

		var menuTabBar = document.createElement("div");
		menuTabBar.id = "MTabBar";
		menuContainer.appendChild(menuTabBar);

		var menuContent = document.createElement("div");
		menuContent.id = "MContent";
		menuContainer.appendChild(menuContent);

		// NDFramePage will call OnLocationChange() which will handle building the menu content the first time.


		// Replace with this line to simulate latency:
		// setTimeout("NDCore.LoadJavaScript(\"menu/tabs.js\", \"NDMenuTabsLoader\");", 1500);
		NDCore.LoadJavaScript("menu/tabs.js", "NDMenuTabsLoader");
		};


	/* Function: OnLocationChange

		Called by <NDFramePage> whenever the location hash changes.

		Parameters:

			oldLocation - The <NDLocation> we were previously at, or undefined if there is none because this is the
							   first call after the page is loaded.
			newLocation - The <NDLocation> we are navigating to.
	*/
	this.OnLocationChange = function (oldLocation, newLocation)
		{
		// If we're on the Home tab and there's only one, automatically select it.
		if (newLocation.type == "Home" && this.tabs != undefined && this.tabs.length == 1)
			{
			this.GoToOffsets([0]);
			return;
			}

		if (oldLocation == undefined || oldLocation.type != newLocation.type)
			{
			this.UpdateTabs(newLocation.type);
			}

		// Make sure we didn't just move to a different member of the same file before rebuilding.
		if (oldLocation == undefined || oldLocation.type != newLocation.type || oldLocation.path != newLocation.path)
			{
			this.Build( new NDMenuHashPath(newLocation.type, newLocation.path) );
			}
		};


	/* Function: GoToOffsets
		Changes the current page in the file menu to the passed array of offsets, which should be in the format used by
		<NDMenuOffsetPath>.
	*/
	this.GoToOffsets = function (offsets)
		{
		if (this.tabs != undefined)
			{
			var newSelectedTab;

			if (offsets.length >= 1)
				{
				newSelectedTab = this.tabs[ offsets[0] ][$Tab_Type];

				if (newSelectedTab != this.selectedTabType)
					{  this.UpdateTabs(newSelectedTab);  }
				}
			}

		this.Build( new NDMenuOffsetPath(offsets) );
		};


	/* Function: Build

		Generates the HTML for the menu.

		Parameters:

			path - The path to the selected file or folder.  Can be set to a <NDMenuOffsetPath> or <NDMenuHashPath>.
					  If there's no selection, supply an empty path object.

					  If the previous attempt to build the menu only partially completed because there was not enough data
					  loaded, calling this again with path undefined will make another attempt at building it.
	*/
	this.Build = function (path)
		{
		if (path != undefined)
			{  this.pathBeingBuilt = path;  }

		// This needs to be checked because the user may navigate somewhere that requires another menu section to be
		// loaded, navigate somewhere else before it finishes, and then the menu data comes in.  Build() would be called
		// even though the menu is complete.
		else if (this.pathBeingBuilt == undefined)
			{  return;  }

		// Reset.  Calls to MenuSection() will recalculate this.
		this.firstUnusedMenuSection = 0;

		var newMenuContent = document.createElement("div");
		newMenuContent.id = "MContent";

		var result;

		if (this.tabs != undefined)
			{  result = this.BuildEntries(newMenuContent, this.pathBeingBuilt);  }
		else
			{
			// Wait for the tabs to come in.  Loading tabs.js is handled by Start().  We still want to put up the loading notice
			// though.
			result = { completed: false };
			}

		if (!result.completed)
			{
			var htmlEntry = document.createElement("div");
			htmlEntry.className = "MLoadingNotice";
			newMenuContent.appendChild(htmlEntry);
			}

		var oldMenuContent = document.getElementById("MContent");
		var menuContainer = oldMenuContent.parentNode;

		menuContainer.replaceChild(newMenuContent, oldMenuContent);

		if (result.completed)
			{
			if (result.selectedFile)
				{
				// Scroll the selected file into view.  Check if it's necessary first because scrollIntoView() may change the scroll
				// position even if the element is already visible and we don't want it to be jumpy.

				// If the element's bottom is lower than the extent of the visible menu...
				if (result.selectedFile.offsetTop + result.selectedFile.offsetHeight > menuContainer.scrollTop + menuContainer.clientHeight)
					{  result.selectedFile.scrollIntoView(false);  }

				// If the element's top is higher than the extent of the visible menu...
				else if (result.selectedFile.offsetTop < menuContainer.scrollTop)
					{  result.selectedFile.scrollIntoView(true);  }
				}


			this.pathBeingBuilt = undefined;
			this.CleanUpMenuSections();
			}
		else if (result.needToLoad != undefined)
			{  this.LoadMenuSection(result.needToLoad);  }
		};


	/* Function: BuildEntries

		Generates the HTML menu entries and appends them to the passed element.

		Returns:

			Returns an object with the following members:

			completed - Bool.  Whether it was able to build the complete menu as opposed to just part.
			needToLoad - The ID of the menu section that needs to be loaded, or undefined if none.
			selectedFile - The DOM element of the selected file, or undefined if none.

	*/
	this.BuildEntries = function (htmlMenu, path)
		{
		var result =
			{
			completed: false
			// needToLoad: undefined
			// selectedFile: undefined
			};

		var pathSoFar = [ ];
		var iterator = path.GetIterator();
		var navigationType;
		var selectedTab;
		var hasParentFolders = false;


		// Generate the list of folders up to and including the selected one.

		for (;;)
			{
			navigationType = iterator.NavigationType();

			if (navigationType == $Nav_NeedToLoad)
				{
				result.needToLoad = iterator.needToLoad;
				return result;
				}

			else if (navigationType == $Nav_SelectedFile || navigationType == $Nav_EndOfPath)
				{  break;  }

			else if (iterator.InTabs())
				{
				pathSoFar.push(iterator.CurrentContainerIndex());
				selectedTab = iterator.CurrentEntry();


				// If there's multiple titles, create parent folders.

				if (typeof(selectedTab[$Tab_HTMLTitle]) == "object")
					{
					// We start at 1 instead of 0 because the first string is the tab title.
					// We end at length - 1 since we want to build the last one as selected.
					for (var i = 1; i < selectedTab[$Tab_HTMLTitle].length - 1; i++)
						{
						var htmlEntry = document.createElement("div");
						htmlEntry.className = "MEntry MFolder Parent Empty";
						htmlEntry.innerHTML = selectedTab[$Tab_HTMLTitle][i];

						htmlMenu.appendChild(htmlEntry);
						hasParentFolders = true;
						}

					// Build the final one as selected.
					var htmlEntry = document.createElement("div");
					htmlEntry.className = "MEntry MFolder Selected";
					htmlEntry.innerHTML = selectedTab[$Tab_HTMLTitle][ selectedTab[$Tab_HTMLTitle].length - 1 ];

					htmlMenu.appendChild(htmlEntry);
					hasParentFolders = true;
					}

				iterator.Next();
				}

			else if (navigationType == $Nav_ParentFolder || navigationType == $Nav_SelectedParentFolder)
				{
				pathSoFar.push(iterator.CurrentContainerIndex());
				var currentEntry = iterator.CurrentEntry();

				var title;

				// If there's multiple titles, create all but the last as empty parent folders

				if (typeof(currentEntry[$Entry_HTMLTitle]) == "object")
					{
					for (var i = 0; i < currentEntry[$Entry_HTMLTitle].length - 1; i++)
						{
						var htmlEntry = document.createElement("div");
						htmlEntry.className = "MEntry MFolder Parent Empty";
						htmlEntry.innerHTML = currentEntry[$Entry_HTMLTitle][i];

						htmlMenu.appendChild(htmlEntry);
						hasParentFolders = true;
						}

					title = currentEntry[$Entry_HTMLTitle][ currentEntry[$Entry_HTMLTitle].length - 1 ];
					}
				else
					{  title = currentEntry[$Entry_HTMLTitle];  }

				if (navigationType == $Nav_SelectedParentFolder)
					{
					var htmlEntry = document.createElement("div");
					htmlEntry.className = "MEntry MFolder Selected";
					htmlEntry.innerHTML = title;

					htmlMenu.appendChild(htmlEntry);
					hasParentFolders = true;
					}
				else
					{
					var htmlEntry = document.createElement("a");
					htmlEntry.className = "MEntry MFolder Parent";
					htmlEntry.setAttribute("href", "javascript:NDMenu.GoToOffsets([" + pathSoFar.join(",") + "])");
					htmlEntry.innerHTML = title;

					htmlMenu.appendChild(htmlEntry);
					hasParentFolders = true;
					}

				iterator.Next();
				}

			else
				{  throw "Unexpected navigation type " + navigationType;  }
			}


		// Generate the list of files in the selected folder

		var selectedFolder = iterator.CurrentContainer();
		var selectedFolderHashPath = iterator.CurrentContainerHashPath();
		var selectedFileIndex = iterator.CurrentContainerIndex();  // will be undefined if there's no selection

		// Build the tabs as folders if none are selected.  We don't have to worry about building it as a regular folder when
		// there is only one tab because we will have navigated to it automatically.
		if (iterator.InTabs())
			{
			for (var i = 0; i < this.tabs.length; i++)
				{
				var htmlEntry = document.createElement("a");
				htmlEntry.className = "MEntry MTabAsFolder";
				htmlEntry.id = "M" + this.tabs[i][$Tab_Type] + "Tab";
				htmlEntry.setAttribute("href", "javascript:NDMenu.GoToTab(\"" + this.tabs[i][$Tab_Type] + "\")");

				var tabTitle = this.tabs[i][$Tab_HTMLTitle];
				if (typeof(tabTitle) == "object")
					{  tabTitle = tabTitle[0];  }

				htmlEntry.innerHTML =
					"<span class=\"MFolderIcon\"></span>" +
					"<span class=\"MTabIcon\"></span>" +
					"<span class=\"MTabTitle\">" + tabTitle + "</span>";

				htmlMenu.appendChild(htmlEntry);
				}
			}
		else
			{
			for (var i = 0; i < selectedFolder.length; i++)
				{
				var member = selectedFolder[i];

				if (member[$Entry_Type] == $EntryType_Target)
					{
					if (i == selectedFileIndex)
						{
						var htmlEntry = document.createElement("div");
						htmlEntry.className = "MEntry MFile Selected" + (hasParentFolders ? "" : " TopLevel");
						htmlEntry.innerHTML = member[$Entry_HTMLTitle];

						htmlMenu.appendChild(htmlEntry);
						result.selectedFile = htmlEntry;
						}
					else
						{
						var hashPath = selectedFolderHashPath;

						if (member[$Entry_HashPath] == undefined)
							{  hashPath += member[$Entry_HTMLTitle];  }
						else
							{  hashPath += member[$Entry_HashPath];  }

						var htmlEntry = document.createElement("a");
						htmlEntry.className = "MEntry MFile" + (hasParentFolders ? "" : " TopLevel");
						htmlEntry.setAttribute("href", "#" + hashPath);
						htmlEntry.innerHTML = member[$Entry_HTMLTitle];

						htmlMenu.appendChild(htmlEntry);
						}
					}
				else  // $EntryType_Container
					{
					var title = "<span class=\"MFolderIcon\"></span>";

					if (typeof(member[$Entry_HTMLTitle]) == "object")
						{  title += member[$Entry_HTMLTitle][0];  }
					else
						{  title += member[$Entry_HTMLTitle];  }

					var targetPath = (pathSoFar.length == 0 ? i : pathSoFar.join(",") + "," + i);

					var htmlEntry = document.createElement("a");
					htmlEntry.className = "MEntry MFolder Child" + (hasParentFolders ? "" : " TopLevel");
					htmlEntry.setAttribute("href", "javascript:NDMenu.GoToOffsets([" + targetPath + "])");
					htmlEntry.innerHTML = title;

					htmlMenu.appendChild(htmlEntry);
					}
				}
			}

		result.completed = true;

		// If we managed to completely build the path, meaning we didn't do a partial one because sections needed to be loaded,
		// save the offsets for the tab.  This lets us go back and forth between tabs without losing our place.  The offsets do NOT
		// include the selected file if there is any.  This is by design.  If a file was selected and we switch to another tab and back
		// without navigating, we'd use NDFramePage.currentLocation instead.  If a file was selected, we switched to another tab,
		// navigated, and then switched back, we would want the version that doesn't have the file selection because it's not
		// relevant anymore and would render the menu entry unclickable.
		if (selectedTab != undefined)
			{  selectedTab[$Tab_LastOffsets] = pathSoFar;  }

		return result;
		};



	// Group: Support Functions
	// ________________________________________________________________________


	/* Function: MenuSection
		Returns the menu entries associated with the passed file as an array, or undefined if it isn't loaded yet.
	*/
	this.MenuSection = function (file)
		{
		for (var i = 0; i < this.menuSections.length; i++)
			{
			if (this.menuSections[i].file == file)
				{
				var section = this.menuSections[i];

				// Move to the end of the used sections list.  It doesn't matter if it's ready.
				if (i >= this.firstUnusedMenuSection)
					{
					if (i > this.firstUnusedMenuSection)
						{
						this.menuSections.splice(i, 1);
						this.menuSections.splice(this.firstUnusedMenuSection, 0, section);
						}

					this.firstUnusedMenuSection++;
					}

				if (section.ready == true)
					{  return section.contents;  }
				else
					{  return undefined;  }
				}
			}

		return undefined;
		};


	/* Function: LoadMenuSection
		Starts loading the menu section with the passed file name if it isn't already loaded or in the process of loading.
	*/
	this.LoadMenuSection = function (file)
		{
		for (var i = 0; i < this.menuSections.length; i++)
			{
			if (this.menuSections[i].file == file)
				{
				// If it has an entry, it's either already loaded or in the process of loading.
				return;
				}
			}

		// If we're here, there was no entry for it.
		var entry = {
			file: file,
			contents: undefined,
			ready: false,
			domLoaderID: "NDMenuLoader_" + file.replace(/[^a-z0-9]/gi, "_")
			};

		this.menuSections.push(entry);

		NDCore.LoadJavaScript("menu/" + file, entry.domLoaderID);
		};


	/* Function: OnSectionLoaded
		Called by the menu data file when it has finished loading.
	*/
	this.OnSectionLoaded = function (file, contents)
		{
		for (var i = 0; i < this.menuSections.length; i++)
			{
			if (this.menuSections[i].file == file)
				{
				this.menuSections[i].contents = contents;
				this.menuSections[i].ready = true;

				// We don't need the loader anymore.
				NDCore.RemoveScriptElement(this.menuSections[i].domLoaderID);

				break;
				}
			}

		//	Replace with this line to simulate latency:
		// setTimeout("NDMenu.Build()", 1500);
		this.Build();
		};


	/* Function: CleanUpMenuSections
		Goes through <menuSections> and if there's more than $MaxMenuSections, removes the least recently accessed entries
		that aren't being used.
	*/
	this.CleanUpMenuSections = function ()
		{
		if (this.menuSections.length > $MaxMenuSections)
			{
			for (var i = this.menuSections.length - 1;
				  i >= this.firstUnusedMenuSection && this.menuSections.length > $MaxMenuSections;
				  i--)
				{
				// We don't want to remove an entry if data's being loaded for it.  The event handler could reasonably expect it
				// to exist.
				if (this.menuSections[i].ready == false)
					{  break;  }

				this.menuSections.pop();
				}
			}
		};


	/* Function: OnTabsLoaded
	*/
	this.OnTabsLoaded = function (tabs)
		{
		this.tabs = tabs;

		NDCore.RemoveScriptElement("NDMenuTabsLoader");

		var tabBar = document.getElementById("MTabBar");

		for (var i = 0; i < tabs.length; i++)
			{
			var tab = document.createElement("a");
			tab.id = "M" + tabs[i][$Tab_Type] + "Tab";
			tab.className = "MTab Wide";
			tab.setAttribute("href", "javascript:NDMenu.GoToTab(\"" + tabs[i][$Tab_Type] + "\");");

			var tabTitle = tabs[i][$Tab_HTMLTitle];
			if (typeof(tabTitle) == "object")
				{  tabTitle = tabTitle[0];  }

			tab.innerHTML = "<span class=\"MTabIcon\"></span><span class=\"MTabTitle\">" + tabTitle + "</span>";

			// We can't get the tab's width until it's added to the tab bar.  However, we don't want the tab bar to grow to multiple lines
			// when detecting all the widths, so we temporarily set them all to absolute positioning.
			tab.style.position = "absolute";
			tab.style.visibility = "hidden";

			tabBar.appendChild(tab);

			tabs[i][$Tab_WideWidth] = tab.offsetWidth;
			tab.className = "MTab Narrow";
			tabs[i][$Tab_NarrowWidth] = tab.offsetWidth;

			if (tabs[i][$Tab_Type] == this.selectedTabType)
				{  tab.className += " Selected";  }
			}

		// Resize them while they're still hidden.  Being absolutely positioned won't affect it.
		this.ResizeTabs();

		if (this.ShouldTabsShow() == false)
			{  tabBar.style.display = "none";  }

		// Undo the temporary properties.
		for (var i = 0; i < tabs.length; i++)
			{
			var tab = this.GetTabElement(tabs[i][$Tab_Type]);
			tab.style.position = "static";
			tab.style.visibility = "visible";
			}

		// If we're on the Home tab and there's only one, automatically select it.
		if ((this.selectedTabType == undefined || this.selectedTabType == "Home") && this.tabs.length == 1)
			{  this.GoToOffsets([0]);  }
		else
			{  this.Build();  }
		};


	/* Function: UpdateTabs
		Changes which tab is displayed in the tab bar, but does not do anything else like rebuild the menu underneath.  Will
		replace <selectedTabType> with the parameter.
	*/
	this.UpdateTabs = function (newTabType)
		{
		if (newTabType == this.selectedTabType)
			{  return;  }

		if (this.tabs != undefined)
			{
			if (this.selectedTabType != undefined)
				{
				var tab = this.GetTabElement(this.selectedTabType);

				// Have to check if tab is undefined because it could be migrating off a path that doesn't have a corresponding tab,
				// like "Home".
				if (tab != undefined)
					{  NDCore.RemoveClass(tab, "Selected");  }
				}

			if (newTabType != undefined)
				{
				var tab = this.GetTabElement(newTabType);

				if (tab != undefined)
					{  NDCore.AddClass(tab, "Selected");  }
				}
			}

		// Changing tabs may change the tab bar's visibility, such as moving from "Home" to "File".
		var wasShowing = this.ShouldTabsShow();
		this.selectedTabType = newTabType;
		var shouldShow = this.ShouldTabsShow();

		if (wasShowing != shouldShow)
			{
			var tabBar = document.getElementById("MTabBar");

			if (shouldShow)
				{  tabBar.style.display = "block";  }
			else
				{  tabBar.style.display = "none";  }
			}

		// If only the selected tab has its text visible we need to call this to change which ones are wide and narrow.
		this.ResizeTabs();
		};


	/* Function: ResizeTabs
		Changes whether the tabs are viewed in their wide or narrow forms based on available space.
	*/
	this.ResizeTabs = function ()
		{
		if (this.ShouldTabsShow() == false)
			{  return;  }

		var menu = document.getElementById("NDMenu");

		// Subtract 1 to account for rounding errors.  Otherwise the tabs will sometimes wrap before shrinking.
		var menuWidth = menu.clientWidth - 1;

		var allWideWidth = 0;
		var selectedWideWidth = 0;

		for (var i = 0; i < this.tabs.length; i++)
			{
			allWideWidth += this.tabs[i][$Tab_WideWidth];

			if (this.tabs[i][$Tab_Type] == this.selectedTabType)
				{  selectedWideWidth += this.tabs[i][$Tab_WideWidth];  }
			else
				{  selectedWideWidth += this.tabs[i][$Tab_NarrowWidth];  }
			}

		for (var i = 0; i < this.tabs.length; i++)
			{
			var makeWide;

			if (allWideWidth < menuWidth)
				{  makeWide = true;  }
			else if (selectedWideWidth < menuWidth)
				{  makeWide = (this.tabs[i][$Tab_Type] == this.selectedTabType);  }
			else // go all narrow
				{  makeWide = false;  }

			var tab = this.GetTabElement(this.tabs[i][$Tab_Type]);

			if (makeWide)
				{
				if (NDCore.HasClass(tab, "Narrow"))
					{
					NDCore.RemoveClass(tab, "Narrow");
					NDCore.AddClass(tab, "Wide");
					}
				}
			else
				{
				if (NDCore.HasClass(tab, "Wide"))
					{
					NDCore.RemoveClass(tab, "Wide");
					NDCore.AddClass(tab, "Narrow");
					}
				}
			}
		};


	/* Function: ShouldTabsShow
		Returns whether the tab bar should be visible.  This does not return whether it actually is visible, but encapsulates the
		logic tests for whether it should be.
	*/
	this.ShouldTabsShow = function ()
		{
		return (this.tabs !== undefined && this.selectedTabType != undefined && this.selectedTabType != "Home");
		};


	/* Function: OnUpdateLayout
	*/
	this.OnUpdateLayout = function ()
		{
		this.ResizeTabs();
		};


	/* Function: GoToTab
	*/
	this.GoToTab = function (newTabType)
		{
		var tabIndex;

		for (var i = 0; i < this.tabs.length; i++)
			{
			if (this.tabs[i][$Tab_Type] == newTabType)
				{
				tabIndex = i;
				break;
				}
			}

		// If they clicked the tab heading while it was already active, return to the root.
		if (this.selectedTabType == newTabType)
			{  this.GoToOffsets( [ tabIndex ] );  }

		// If the tab wasn't active but they clicked on the one representing the current location, such as if they opened a File, switched
		// to the Classes tab and then switched back, reload the current location.  This will include the file selection highlight which
		// $Tab_LastOffset wouldn't have.
		else if (newTabType == NDFramePage.currentLocation.type)
			{
			this.UpdateTabs(newTabType);
			this.Build( new NDMenuHashPath(NDFramePage.currentLocation.type, NDFramePage.currentLocation.path) );
			}

		// If the tab wasn't active and doesn't represent the current location, try to display the last saved offsets.  These don't include
		// the file selection.  This lets people switch back and forth between tabs without losing their place.
		else if (this.tabs[tabIndex][$Tab_LastOffsets] != undefined)
			{  this.GoToOffsets(this.tabs[tabIndex][$Tab_LastOffsets]);  }

		// If there's no saved offsets, just start at the root.
		else
			{  this.GoToOffsets( [ tabIndex ] );  }
		};


	/* Function: GetTabElement
		Returns the DOM element for the tab of the passed type.
	*/
	this.GetTabElement = function (type)
		{
		return document.getElementById("M" + type + "Tab");
		};



	// Group: Variables
	// ________________________________________________________________________


	/* var: pathBeingBuilt
		If we're attempting to update the menu, this will be an object that represents the navigation path from the root folder
		to the selected folder or file.  It can be either a <NDMenuOffsetPath> or <NDMenuHashPath>.  Once the menu
		has been completely built this will return to undefined.
	*/

	/* var: menuSections
		An array of <NDMenuSections> that have been loaded for the file menu or are in the process of being loaded.
		The array is ordered from the most recently accessed to the least.
	*/

	/* var: firstUnusedMenuSection
		An index into <menuSections> of the first entry that was not accessed via <MenuSection()> in the
		last call to <Build()>.
	*/

	/* var: tabs
		An array of all the tab information.
	*/

	/* var: selectedTabType
		The type string of the currently selected tab.
	*/

	};




/* Class: NDMenuSection
	___________________________________________________________________________

	An object representing part of the menu structure.

		var: file
		The data file name, such as "files2.js".

		var: contents
		The contents of the data file as an array of menu entries, or undefined if <ready> isn't set yet.

		var: ready
		True if the data has been loaded and is ready to use.  False if the data has been requested but is not ready
		yet.  If the data has not been requested it simply would not have a NDMenuSection object for it.

		var: domLoaderID
		The ID of the DOM script object that's loading this file.

*/




/* Class: NDMenuOffsetPath
	___________________________________________________________________________

	A path through <NDMenu's> hierarchy using array offsets, which is more efficient than using folder names.
	This has the same interface as <NDMenuHashPath> so they can be used interchangeably.

	You can pass an array of file offsets to the constructor, or leave it undefined if there's no selection.  The first
	number refers to an offset into the <NDMenu.tabs> array, and every subsequent number is an offset into the
	levels of the menu hierarchy.

*/
function NDMenuOffsetPath (offsets)
	{

	// Group: Functions
	// ________________________________________________________________________

	/* Function: GetIterator
		Creates and returns a new <iterator: NDMenuOffsetPathIterator> positioned at the beginning of the path.
	*/
	this.GetIterator = function ()
		{
		return new NDMenuOffsetPathIterator(this);
		};


	/* Function: IsEmpty
		Returns whether this is an empty path, which means it points to the root.
	*/
	this.IsEmpty = function ()
		{
		return (this.path.length == 0);
		};


	// Group: Properties
	// ________________________________________________________________________

	/* Property: path
		An array of offsets.  An empty array means there is no selection.  A first entry would be the index into <NDMenu.tabs>,
		and further entries would be indexes into menu folders.  If the last entry points to a folder, it means that folder is selected
		but not a file within it.  If it points to a file, it means that file is selected.
	*/
	if (offsets == undefined)
		{  this.path = [ ];  }
	else
		{  this.path = offsets;  }

	};




/* Class: NDMenuOffsetPathIterator
	___________________________________________________________________________

	A class that can walk through <NDMenuOffsetPath>.

	Limitations:

		- The iterator can only walk forward.  Walking backwards wasn't needed when it was written so it doesn't exist.
		- The iterator must be recreated when another section of the menu is loaded.  An iterator created before the load is not
		   guaranteed to notice the new data.

*/
function NDMenuOffsetPathIterator (pathObject)
	{

	// Group: Private Functions
	// ________________________________________________________________________


	/* Private Function: Constructor
		You do not need to call this function.  Simply call "new NDMenuOffsetPathIterotar(pathObject)" and this will be called
		automatically.
	 */
	this.Constructor = function (pathObject)
		{
		this.pathObject = pathObject;
		this.pathIndex = 0;
		this.currentContainer = NDMenu.tabs;

		if (NDMenu.tabs == undefined)
			{  this.needToLoad = "tabs.js";  }

		// this.currentContainerHashPath = undefined;
		};



	// Group: Functions
	// ________________________________________________________________________


	/* Function: Next
		Moves the iterator forward one place in the path.
	*/
	this.Next = function ()
		{
		// If we're past the end of the path...
		if (this.pathIndex >= this.pathObject.path.length)
			{  return;  }

		// If we're in the path but past what's loaded...
		else if (this.currentContainer == undefined)
			{
			this.pathIndex++;
			}

		else
			{
			var currentEntry = this.currentContainer[ this.pathObject.path[ this.pathIndex ] ];
			this.currentContainer = undefined;
			this.currentContainerHashPath = undefined;
			this.needToLoad = undefined;

			if (this.pathIndex == 0)
				{
				this.currentContainerHashPath = currentEntry[$Tab_HashPath];
				this.needToLoad = currentEntry[$Tab_DataFile];
				}
			else if (currentEntry[$Entry_Type] == $EntryType_Container)
				{
				this.currentContainerHashPath = currentEntry[$Entry_HashPath];

				if (typeof currentEntry[$Entry_Members] == "string")
					{  this.needToLoad = currentEntry[$Entry_Members];  }
				else
					{  this.currentContainer = currentEntry[$Entry_Members];  }
				}
			// Do nothing for $EntryType_Target

			this.pathIndex++;

			if (this.needToLoad != undefined)
				{
				this.currentContainer = NDMenu.MenuSection(this.needToLoad);

				if (this.currentContainer != undefined)
					{  this.needToLoad = undefined;  }
				}
			}
		};


	/* Function: NavigationType

		Returns what the current position represents, which will be one of these values:

		$Nav_ParentFolder - The iterator is pointing to a parent folder or tab, though not the currently selected one.
		$Nav_SelectedParentFolder - The iterator is pointing to the currently selected parent folder or tab.
		$Nav_SelectedFile - The iterator is pointing to the currently selected file.
		$Nav_NeedToLoad - The iterator is pointing to a part of the menu hasn't been loaded yet.  The file to load will be stored in
									  <NeedToLoad()>.
		$Nav_EndOfPath - The iterator is past the end of the path.

		Behavior:

			- The iterator can start with $Nav_EndOfPath if there is no tab selected, or $Nav_NeedToLoad if the tab information
			  isn't loaded.
			- If there are folders or tabs in the path, all but the last will be $Nav_ParentFolder and the last will be
			  $Nav_SelectedParentFolder.  This is regardless of whether there is also a file selected or not.
			- If there is a file selected it will return $Nav_SelectedFile, but there does not need to be so it may go straight from
			  $Nav_SelectedParentFolder to $Nav_EndOfPath.
			- It can return $Nav_NeedToLoad at any time.
	*/
	this.NavigationType = function ()
		{
		$Nav_ParentFolder = 1;
		$Nav_SelectedParentFolder = 2;
		$Nav_SelectedFile = 3;
		$Nav_NeedToLoad = 9;
		$Nav_EndOfPath = -1;

		// We need to check for NeedToLoad before EndOfPath.  If the last element of a path pointed to a folder whose contents
		// weren't loaded, calling Next() would lead to currentContainer being undefined because it needs to load, and also pathIndex
		// being past the end because there's no selected file after it.  In this case we need to return NeedToLoad because we'll want
		// it to display the final folder's contents.
		if (this.currentContainer == undefined)
			{  return $Nav_NeedToLoad;  }

		if (this.pathIndex >= this.pathObject.path.length)
			{  return $Nav_EndOfPath;  }

		var currentEntry = this.currentContainer[ this.pathObject.path[ this.pathIndex ] ];

		if (this.InTabs() == false && currentEntry[$Entry_Type] == $EntryType_Target)
			{  return $Nav_SelectedFile;  }

		// So we're at a container.  If it's the last part of the path we know it's selected.
		if (this.pathIndex == this.pathObject.path.length - 1)
			{  return $Nav_SelectedParentFolder;  }

		// And if there's more than one level past it, we know it's not selected.
		if (this.pathIndex + 2 <= this.pathObject.path.length - 1)
			{  return $Nav_ParentFolder;  }

		// However, if there's only one past it, we need to know whether it's a file or a folder to know whether this one selected
		// or not.
		var lookahead = this.Duplicate();
		lookahead.Next();

		if (lookahead.NavigationType() == $Nav_NeedToLoad)
			{
			this.needToLoad = lookahead.NeedToLoad();
			return $Nav_NeedToLoad;
			}
		else if (lookahead.CurrentEntry()[$Entry_Type] == $EntryType_Container)
			{  return $Nav_ParentFolder;  }
		else
			{  return $Nav_SelectedParentFolder;  }
		};


	/* Function: Duplicate
		Creates and returns a new iterator at the same position as this one.
	*/
	this.Duplicate = function ()
		{
		var newObject = new NDMenuOffsetPathIterator (this.pathObject);

		newObject.pathIndex = this.pathIndex;
		newObject.currentContainer = this.currentContainer;
		newObject.needToLoad = this.needToLoad;

		return newObject;
		};



	// Group: Properties
	// ________________________________________________________________________


	/* Function: CurrentEntry
		The tab or menu entry at the current iterator position.
	*/
	this.CurrentEntry = function ()
		{
		if (this.currentContainer != undefined && this.pathIndex < this.pathObject.path.length)
			{  return this.currentContainer[ this.pathObject.path[ this.pathIndex ] ];  }
		else
			{  return undefined;  }
		};


	/* Function: CurrentContainer
		The tab array or folder members that <CurrentEntry> is in.  For folders that have their members in a separate file,
		this will either be the members or undefined if they're not loaded yet.  It will not be the parent entry.
	*/
	this.CurrentContainer = function ()
		{
		return this.currentContainer;
		};


	/* Function: CurrentContainerIndex
		The index into <CurrentContainer> that <CurrentEntry> appears at.
	*/
	this.CurrentContainerIndex = function ()
		{
		if (this.pathIndex < this.pathObject.path.length)
			{  return this.pathObject.path[ this.pathIndex ];  }
		else
			{  return undefined;  }
		};


	/* Function: CurrentContainerHashPath
		The hash path associated with <CurrentContainer>.
	*/
	this.CurrentContainerHashPath = function ()
		{
		return this.currentContainerHashPath;
		};


	/* Function: InTabs
		Returns whether is in the tabs instead of a menu folder.
	*/
	this.InTabs = function ()
		{
		return (this.pathIndex == 0);
		};


	/* Function: NeedToLoad
		If the iterator has reached a part of the menu that hasn't been loaded, this will be the file that it needs.
	*/
	this.NeedToLoad = function ()
		{
		if (this.currentContainer == undefined)
			{  return this.needToLoad;  }
		else
			{  return undefined;  }
		};



	// Group: Variables
	// ________________________________________________________________________


	/* var: pathObject
		A reference to the <NDMenuOffsetPath> object this iterator works on.
	*/

	/* var: pathIndex
		An index into <NDMenuOffsetPath.path> of the current position.
	*/

	/* var: currentContainer
		A reference to the tab array or menu folder the iterator is currently in.  This will be undefined if the iterator is out of
		bounds or the data is not loaded yet.
	*/

	/* var: currentContainerHashPath
		The hash path associated with <currentContainer>.
	*/

	/* var: needToLoad
		If <NavigationType> returns $Nav_NeedToLoad, this will hold the name of the data file that needs to be loaded.  The
		value is not relevant otherwise and is not guaranteed to be undefined.

		Remember that you need to create a new iterator after loading a section of the menu.  Existing ones are not
		guaranteed to notice the new addition.
	*/


	// Call the constructor now that all the members are prepared.
	this.Constructor(pathObject);

	};




/* Class: NDMenuHashPath
	___________________________________________________________________________

	A path through <NDMenu's> hierarchy using a hash path string such as "File2:folder/folder/source.cs".  All paths are
	assumed to terminate on a file name instead of a folder.  	This has the same interface as <NDMenuOffsetPath> so
	they can be used interchangeably.

*/
function NDMenuHashPath (type, hashPath)
	{

	// Group: Functions
	// ________________________________________________________________________

	/* Function: GetIterator
		Creates and returns a new <iterator: NDMenuOffsetPathIterator> positioned at the beginning of the path.
	*/
	this.GetIterator = function ()
		{
		// We generate a new offset path for every iterator created because a path can persist between menu section
		// loads, but an iterator should not.
		return new NDMenuOffsetPathIterator(this.MakeOffsetPath());
		};


	/* Function: IsEmpty
		Returns whether this is an empty path, which means it points to the root.
	*/
	this.IsEmpty = function ()
		{
		return (this.type == undefined || this.type == "Home");
		};


	/* Function: MakeOffsetPath
		Generates and returns a <NDMenuOffsetPath> from the hash path string.

		If there are not enough menu sections loaded to fully resolve it, it will generate what it can and put an extra -1
		offset on the end to indicate  that there's more.  The extra entry prevents things from rendering as selected
		when they may not be.  <NDMenuOffsetPathIterator> shouldn't have to worry about handling the -1
		because it would stop with $Nav_NeedToLoad before using it, and after the section is loaded new iterators will
		have to be created which will cause this function to generate an updated offset path.

		If there are invalid sections of the hash path, such as a folder name that doesn't exist, this will generate as much
		as it can from the valid section and ignore the rest.
	*/
	this.MakeOffsetPath = function ()
		{
		var offsets = [ ];

		// If there's no type because we had an empty location, return the empty offsets.
		if (this.type === undefined)
			{  return new NDMenuOffsetPath(offsets);  }

		// If the tabs aren't loaded yet, return a partial path.
		if (NDMenu.tabs === undefined)
			{
			offsets.push(-1);
			return new NDMenuOffsetPath(offsets);
			}

		// Find the tab that corresponds to the location type and add its offset to the path.
		var tab;
		for (var i = 0; i < NDMenu.tabs.length; i++)
			{
			if (NDMenu.tabs[i][$Tab_Type] == this.type)
				{
				tab = NDMenu.tabs[i];
				offsets.push(i);
				break;
				}
			}

		// If we couldn't find the tab or there's no hash path, we're done.
		if (tab === undefined || this.hashPathString == "" || this.hashPathString === undefined)
			{  return new NDMenuOffsetPath(offsets);  }

		// If the tab has a hash path associated with it, it must match or we're done.
		if (tab[$Tab_HashPath] != undefined && this.hashPathString.StartsWith(tab[$Tab_HashPath]) == false)
			{  return new NDMenuOffsetPath(offsets);  }

		// Get the root container associated with the tab.
		var container = NDMenu.MenuSection(tab[$Tab_DataFile]);
		var containerHashPath = tab[$Tab_HashPath];

		// Quit early if it isn't loaded yet.
		if (container === undefined)
			{
			offsets.push(-1);
			return new NDMenuOffsetPath(offsets);
			}

		// Now we iterate through the levels of the menu for as long as we find members that match part of the hash path.
		do
			{
			var continueSearch = false;

			for (var i = 0; i < container.length; i++)
				{
				var member = container[i];

				if (member[$Entry_Type] == $EntryType_Target)
					{
					var memberHashPath = containerHashPath;

					if (member[$Entry_HashPath] !== undefined)
						{  memberHashPath += member[$Entry_HashPath];  }
					else
						{  memberHashPath += member[$Entry_HTMLTitle];  }

					if (memberHashPath == this.hashPathString)
						{
						offsets.push(i);
						return new NDMenuOffsetPath(offsets);
						}
					}
				else // $EntryType_Container
					{
					if (this.hashPathString == member[$Entry_HashPath])
						{
						offsets.push(i);
						return new NDMenuOffsetPath(offsets);
						}
					else if (this.hashPathString.StartsWith(member[$Entry_HashPath]))
						{
						offsets.push(i);

						containerHashPath = member[$Entry_HashPath];
						continueSearch = true;

						if (typeof member[$Entry_Members] == "string")
							{
							container = NDMenu.MenuSection(member[$Entry_Members]);

							if (container === undefined)
								{
								offsets.push(-1);
								return new NDMenuOffsetPath(offsets);
								}
							}
						else
							{  container = member[$Entry_Members];  }

						break;
						}
					}
				}
			}
		while (continueSearch == true);

		return new NDMenuOffsetPath(offsets);
		};



	// Group: Properties
	// ________________________________________________________________________

	/* Property: type
		The type string of the hash path, such as "File" or "Class".
	*/
	this.type = type;

	/* Property: hashPathString
		The hash path string such as "File2:folder/folder/source.cs".
	*/
	this.hashPathString = hashPath;

	};
