/// <reference path="../../Scripts/jquery.js" />
/// <reference path="../../Scripts/require.js" />
/// <reference path="../../Scripts/MadCapGlobal.js" />
/// <reference path="../../Scripts/MadCapUtilities.js" />
/// <reference path="../../Scripts/MadCapDom.js" />
/// <reference path="../../Scripts/MadCapXhr.js" />

/*!
    * Copyright MadCap Software
    * http://www.madcapsoftware.com/
    * Unlicensed use is strictly prohibited
    *
    * v17.2.8047.30191
    */

(function () {
    MadCap.WebHelp = MadCap.CreateNamespace("WebHelp");

    MadCap.WebHelp.HelpSystem = function (parentSubsystem, parentPath, xmlFile, tocPath, browseSequencePath) {
        // Private member variables

        var mSelf = this;
        var mParentSubsystem = parentSubsystem;
        var mPath = new MadCap.Utilities.Url(xmlFile).ToFolder().ToFolder().FullPath;
        var mAbsolutePath = null;
        var mSubsystems = new Array();
        var mTocPath = tocPath;
        var mBrowseSequencePath = browseSequencePath;
        var mFilterMap = null;
        var mFilterUrls = [];
        var mIndexLinkMap = new MadCap.Utilities.Dictionary();
        var mConceptMap = null;
        var mViewedConceptMap = new MadCap.Utilities.Dictionary();
        var mExists = false;
        var mAliasFile = new MadCap.WebHelp.AliasFile(mPath + "Data/Alias.xml", this);
        var mTocFile = new MadCap.WebHelp.TocFile(this, MadCap.WebHelp.TocFile.TocType.Toc);
        var mTestPromise = null;
        var mIndexXmlDoc = null;
        var mBrowseSequenceFile = new MadCap.WebHelp.TocFile(this, MadCap.WebHelp.TocFile.TocType.BrowseSequence);
        var mSkinMap = new MadCap.Utilities.Dictionary();

        // Public properties

        this.TargetType = null;
        this.DefaultStartTopic = null;
        this.InPreviewMode = null;
        this.LiveHelpOutputId = null;
        this.LiveHelpServer = null;
        this.LiveHelpEnabled = false;
        this.IsWebHelpPlus = false;
        this.ContentFolder = null;
        this.UseCustomTopicFileExtension = false;
        this.CustomTopicFileExtension = null;
        this.IsMultilingual = false;
        this.GlossaryUrl = null;
        this.SearchFilterSetUrl = null;
        this.SyncTOC = null;
        this.IndexPartialWordSearch = true;
        this.GlossaryPartialWordSearch = true;
        this.DefaultSkin = null;
        this.IsAutoMerged = false;
        this.LanguageUrl = null;
        this.BreakpointsUrl = null;
        this.PreventExternalUrls = false;
        this.IsResponsive = false;
        this.SearchUrl = null;
        this.PulsePage = null;
        this.ScriptsFolderPath = null;
        this.LanguageCode = null;
        this.LanguageName = null;
	    this.IncludeCSHRuntime = null;
        this.ShowMadCapBacklink = false;
        this.LiveHelpServerType = null;
        this.OutputAnalyticsServer = null;
        this.OutputAnalyticsId = null;
        this.IsCentralHosted = false;
        this.CentralLogoutLink = null;
        this.CentralSettingsLink = null;

        // Constructor

        (function () {
        })();

        // Public functions

        this.Load = function (OnCompleteFunc) {
            MadCap.Utilities.Xhr.Load(xmlFile, true, function (xmlDoc) {
                var loaded = 0;

                function OnLoadSubHelpSystemComplete() {
                    loaded++;

                    if (loaded == mSubsystems.length)
                        OnCompleteFunc();
                }

                function LoadSubsystems(OnLoadSubHelpSystemComplete, OnCompleteFunc) {
                    if (mSubsystems.length > 0) {
                        for (var i = 0; i < mSubsystems.length; i++) {
                            mSubsystems[i].Load(OnLoadSubHelpSystemComplete);
                        }
                    }
                    else {
                        OnCompleteFunc();
                    }
                }

                mExists = xmlDoc != null;

                if (!mExists) {
                    OnCompleteFunc();
                    return;
                }
   
                this.LanguageCode = xmlDoc.documentElement.getAttribute("xml:lang");
                this.LanguageName = xmlDoc.documentElement.getAttribute("LanguageName");
                this.TargetType = xmlDoc.documentElement.getAttribute("TargetType");
                this.DefaultStartTopic = xmlDoc.documentElement.getAttribute("DefaultUrl");
                this.InPreviewMode = MadCap.Dom.GetAttributeBool(xmlDoc.documentElement, "InPreviewMode", false);
                this.LiveHelpOutputId = xmlDoc.documentElement.getAttribute("LiveHelpOutputId");
                this.LiveHelpServer = xmlDoc.documentElement.getAttribute("LiveHelpServer");
                this.LiveHelpServerType = xmlDoc.documentElement.getAttribute("LiveHelpServerType");
                this.OutputAnalyticsServer = xmlDoc.documentElement.getAttribute("OutputAnalyticsServer");
                this.OutputAnalyticsId = xmlDoc.documentElement.getAttribute("OutputAnalyticsId");
                this.IsCentralHosted = MadCap.Dom.GetAttributeBool(xmlDoc.documentElement, "IsCentralHosted", false);
                this.CentralLogoutLink = xmlDoc.documentElement.getAttribute("CentralLogoutLink");
                this.CentralSettingsLink = xmlDoc.documentElement.getAttribute("CentralSettingsLink");

                if(this.LiveHelpServerType == "Central")
                    this.LiveHelpEnabled = this.OutputAnalyticsServer != null;
                else
                    this.LiveHelpEnabled = this.LiveHelpOutputId != null;

                this.MoveContentToRoot = MadCap.Dom.GetAttributeBool(xmlDoc.documentElement, "MoveOutputContentToRoot", false);
                this.ReplaceReservedCharacters = MadCap.Dom.GetAttributeBool(xmlDoc.documentElement, "ReplaceReservedCharacters", false);
                this.MakeFileLowerCase = MadCap.Dom.GetAttributeBool(xmlDoc.documentElement, "MakeFileLowerCase", false);
                this.PreventExternalUrls = MadCap.Dom.GetAttributeBool(xmlDoc.documentElement, "PreventExternalUrls", false);
                this.IsResponsive = MadCap.Dom.GetAttributeBool(xmlDoc.documentElement, "EnableResponsiveOutput", false);
                this.IncludeGlossarySearchResults = MadCap.Dom.GetAttributeBool(xmlDoc.documentElement, "IncludeGlossarySearchResults", true);
                this.ResultsPerPage = MadCap.Dom.GetAttributeInt(xmlDoc.documentElement, "ResultsPerPage", 20);
                this.SearchEngine = MadCap.Dom.GetAttribute(xmlDoc.documentElement, "SearchEngine");
                this.IncludeCSHRuntime = MadCap.Dom.GetAttributeBool(xmlDoc.documentElement, "IncludeCSHRuntime", true);
                this.DebugMode = MadCap.Dom.GetAttributeBool(xmlDoc.documentElement, "DebugMode", false);
                this.HasELearning = MadCap.Dom.GetAttributeBool(xmlDoc.documentElement, "HasELearning", false);

                if (this.SearchEngine == "Elasticsearch") {
                    var elements = xmlDoc.documentElement.getElementsByTagName("Elasticsearch");
                    this.Elasticsearch = elements.item(0);
                }

                var serverEnabled = MadCap.Dom.GetAttributeBool(xmlDoc.documentElement, "ServerEnabled", false);
                this.IsWebHelpPlus = (this.TargetType == "WebHelpPlus" || serverEnabled) && MadCap.String.StartsWith(document.location.protocol, "http", false);

                var contentFolder = "";

                if (!this.MoveContentToRoot)
                    contentFolder = "Content/";

                if (this.MakeFileLowerCase)
                    contentFolder = contentFolder.toLowerCase();

                var microContentFolder = "MicroContent/";
                if (this.MakeFileLowerCase)
                    microContentFolder = microContentFolder.toLowerCase();

                this.ContentFolder = contentFolder;
                this.MicroContentFolder = microContentFolder;
			    this.UseCustomTopicFileExtension = MadCap.Dom.GetAttributeBool(xmlDoc.documentElement, "UseCustomTopicFileExtension", false);
                this.CustomTopicFileExtension = MadCap.Dom.GetAttribute(xmlDoc.documentElement, "CustomTopicFileExtension");
			    this.IsMultilingual = MadCap.Dom.GetAttributeBool(xmlDoc.documentElement, "Multilingual", false);
                this.ShowMadCapBacklink = MadCap.Dom.GetAttributeBool(xmlDoc.documentElement, "ShowMadCapBacklink", false);

                this.GlossaryUrl = GetDataFileUrl(xmlDoc, "Glossary");
                this.TocUrl = GetDataFileUrl(xmlDoc, "Toc");
                this.SearchUrl = GetDataFileUrl(xmlDoc, "SearchUrl");
                this.PulsePage = GetDataFileUrl(xmlDoc, "PulsePage");
                this.BrowseSequencesUrl = GetDataFileUrl(xmlDoc, "BrowseSequence");
                this.IndexUrl = GetDataFileUrl(xmlDoc, "Index");
                this.SearchFilterSetUrl = GetDataFileUrl(xmlDoc, "SearchFilterSet");
                this.LanguageUrl = mPath + "Data/Language.js";
                this.BreakpointsUrl = mPath + "Data/Breakpoints.js";
                this.ScriptsFolderPath = xmlDoc.documentElement.getAttribute("PathToScriptsFolder");
                this.HasBrowseSequences = xmlDoc.documentElement.getAttribute("BrowseSequence") != null;
                this.HasToc = xmlDoc.documentElement.getAttribute("Toc") != null;
                this.TopNavTocPath = xmlDoc.documentElement.getAttribute("TopNavTocPath") == 'true';
                this.LiveHelpServerType = xmlDoc.documentElement.getAttribute("LiveHelpServerType");
                this.OutputAnalyticsServer = xmlDoc.documentElement.getAttribute("OutputAnalyticsServer");

                //

                CacheSkins.call(this, xmlDoc);
                this.DefaultSkin = this.GetSkin($(xmlDoc.documentElement).attr("SkinID"));

                this.SyncTOC = this.DefaultSkin != null && MadCap.String.ToBool(this.DefaultSkin.AutoSyncTOC, false);
                this.IndexPartialWordSearch = this.DefaultSkin == null || MadCap.String.ToBool(this.DefaultSkin.IndexPartialWordSearch, true);
                this.GlossaryPartialWordSearch = this.DefaultSkin == null || MadCap.String.ToBool(this.DefaultSkin.GlossaryPartialWordSearch, true);
                this.DisplayCommunitySearchResults = this.DefaultSkin == null || MadCap.String.ToBool(this.DefaultSkin.DisplayCommunitySearchResults, true);

                this.CommunitySearchResultsCount = 3;
                if (this.DefaultSkin != null) {
                    this.CommunitySearchResultsCount = MadCap.String.ToInt(this.DefaultSkin.CommunitySearchResultsCount, 3);
                }
                //

                var subsystemsNodes = xmlDoc.getElementsByTagName("Subsystems");

                if (subsystemsNodes.length > 0 && subsystemsNodes[0].getElementsByTagName("Url").length > 0) {
                    var urlNodes = xmlDoc.getElementsByTagName("Subsystems")[0].getElementsByTagName("Url");

                    for (var i = 0; i < urlNodes.length; i++) {
                        var urlNode = urlNodes[i];
                        if (!urlNode)
                            continue;

                        var url = urlNode.getAttribute("Source");
                        var subPath = url.substring(0, url.lastIndexOf("/") + 1);
                        var tocPath = urlNode.getAttribute("TocPath");
                        var browseSequencePath = urlNode.getAttribute("BrowseSequencePath");

                        var subHelpSystem = new MadCap.WebHelp.HelpSystem(this, mPath + subPath, mPath + subPath + "Data/HelpSystem.xml", tocPath, browseSequencePath);
                        mSubsystems.push(subHelpSystem);
                    }
                }

            
                this.LoadBreakpoints(function () {
                    mSelf.LoadLanguage(function () {
                        if (!mSelf.IsAutoMerged && mSelf.IsWebHelpPlus) {
                            MadCap.Utilities.Xhr.CallWebService(mSelf.GetPath() + "Service/Service.asmx/GetSubsystems", true, function (xmlDoc, args) {
                                if (xmlDoc) {
                                    $.each(xmlDoc.documentElement.childNodes, function (i, node) {
                                        if (node.nodeName == "Subsystems") {
                                            $.each(node.childNodes, function (i, node) {
                                                if (node.nodeName == "Url") {
                                                    var url = node.getAttribute("Source");
                                                    var subPath = url.substring(0, url.lastIndexOf("/") + 1);
                                                    if (subPath) {
                                                        var subHelpSystem = new MadCap.WebHelp.HelpSystem(mSelf, mPath + subPath, mPath + subPath + "Data/HelpSystem.xml", null, null);
                                                        subHelpSystem.IsAutoMerged = true;
                                                        mSubsystems.push(subHelpSystem);
                                                    }
                                                }
                                            });
                                        }
                                    });
                                }

                                LoadSubsystems(OnLoadSubHelpSystemComplete, OnCompleteFunc);
                            });
                        }
                        else {
                            LoadSubsystems(OnLoadSubHelpSystemComplete, OnCompleteFunc);
                        }
                    })
                });
            }, null, this);
        };

        this.GetExists = function () {
            return mExists;
        };

        this.GetMasterHelpSystem = function () {
            var master = this;

            for (var curr = master.GetParentSubsystem(); curr != null; curr = curr.GetParentSubsystem()) {
                master = curr;
            }

            return master;
        };

        this.GetParentSubsystem = function () {
            return mParentSubsystem;
        };

        this.GetPath = function () {
            return mPath;
        };

        this.GetCurrentTopicPath = function () {
            return MadCap.Utilities.Url.GetDocumentUrl().ToRelative(MadCap.Utilities.Url.GetAbsolutePath(mPath)).FullPath;
        };

        this.GetAbsolutePath = function () {
            if (mAbsolutePath == null) {
                var url = new MadCap.Utilities.Url(mPath);
                mAbsolutePath = url.IsAbsolute ? url.FullPath : new MadCap.Utilities.Url(document.location.href).Path;
            }

            return mAbsolutePath;
        };

        this.GetContentPath = function () {
            return mPath + this.ContentFolder;
        };

        this.GetSkin = function (skinID) {
            return mSkinMap.GetItem(skinID);
        };

        this.GetSkinByName = function (name) {
            var skins = this.GetSkins();
            for (var i = 0; i < skins.length; i++){
                var skin = skins[i];
                if (skin.Name == name) {
                    return skin;
                }
            };

            return null;
        };

        this.GetCurrentSkin = function () {
            var url = MadCap.Utilities.Url.GetDocumentUrl();
            var skinName = url.QueryMap.GetItem("skinName") || url.HashMap.GetItem("skinName");
            if (skinName) {
                var skin = this.GetSkin(skinName);
                if (!skin)
                    skin = this.GetSkinByName(skinName);
                return skin;
            }

            var cshid = url.QueryMap.GetItem('cshid');
            if (cshid) {
                var idInfo = mAliasFile.LookupID(cshid);
                if (idInfo.Skin) {
                    var skin = this.GetSkin(idInfo.Skin);
                    if (!skin)
                        skin = this.GetSkinByName(idInfo.Skin);
                    return skin;
                }
            }

            return this.DefaultSkin;
        };

        this.GetAppliedSkinPath = function (href) {
            var current = new MadCap.Utilities.Url(document.location.href);
            var currentSkin = this.GetCurrentSkin();
            var hrefUrl = new MadCap.Utilities.Url(href);
            var q = hrefUrl.QueryMap.GetLength() > 0 ? '&' : '?';
            var skinQuery = this.DefaultSkin !== currentSkin ? (q + "skinName=" + currentSkin.SkinID) : "";
            var newUrl = current.ToFolder().CombinePath(href);
            return newUrl.PlainPath + newUrl.Query + skinQuery + newUrl.Fragment;
        }

        this.GetTocPath = function (tocType) {
            return tocType == "toc" ? mTocPath : mBrowseSequencePath;
        };

        this.GetFullTocPath = function (tocType, href) {
            var subsystem = this.GetHelpSystem(href);

            if (subsystem == null)
                return null;

            var fullTocPath = new Object();

            fullTocPath.tocPath = this.GetTocPath(tocType);
            subsystem.ComputeTocPath(tocType, fullTocPath);

            return fullTocPath.tocPath;
        };

        this.GetTopicPath = function (linkUrl, ignoreTriPane) {
            var path = this.GetPath();
            var masterHelpSystem = this.GetMasterHelpSystem();

            var docUrl = (new MadCap.Utilities.Url(document.location.href)).ToPath();
            var absUrl = docUrl.CombinePath(path + "Data/").CombinePath(linkUrl);
            var relUrl = absUrl.ToRelative(docUrl);

            // TriPane expects links relative to Content folder
            if (!ignoreTriPane && MadCap.Utilities.HasRuntimeFileType("TriPane") && !masterHelpSystem.MoveContentToRoot) {
                relUrl = relUrl.ToRelative(masterHelpSystem.ContentFolder);
            }

            return relUrl;
        };

        this.GetTransparentImagePath = function () {
            var transparentGifUrl = this.GetPath() +  "Skins/Default/Stylesheets/Images/transparent.gif";
            return transparentGifUrl;
        }

        this.GetPatchedPath = function (linkUrl) {
            if (this.ReplaceReservedCharacters)
                linkUrl = MadCap.Utilities.Url.ReplaceReservedCharacters(linkUrl, '_');
            if (this.MakeFileLowerCase)
                linkUrl = linkUrl.toLowerCase();
            if (this.UseCustomTopicFileExtension)
                linkUrl = new MadCap.Utilities.Url(linkUrl).ToExtension(this.CustomTopicFileExtension).FullPath;

            return linkUrl;
        };

        this.GetAbsoluteTopicPath = function (linkUrl) {
            var href = new MadCap.Utilities.Url(linkUrl);
            var currentUrl = (new MadCap.Utilities.Url(document.location.href)).ToPlainPath();

            return currentUrl.CombinePath(this.GetTopicPath('../' + href.FullPath).FullPath);
        }

        this.ComputeTocPath = function (tocType, tocPath) {
            if (mParentSubsystem) {
                var hsTocPath = this.GetTocPath(tocType);

                if (!MadCap.String.IsNullOrEmpty(hsTocPath)) {
                    tocPath.tocPath = tocPath.tocPath ? hsTocPath + "|" + tocPath.tocPath : hsTocPath;
                }

                mParentSubsystem.ComputeTocPath(tocType, tocPath);
            }
        };

        this.GetHelpSystem = function (path) {
            var helpSystem = null;

            for (var i = 0; i < mSubsystems.length; i++) {
                helpSystem = mSubsystems[i].GetHelpSystem(path);

                if (helpSystem != null) {
                    return helpSystem;
                }
            }

            if (MadCap.String.StartsWith(path, mPath, false)) {
                return this;
            }

            return null;
        };

        this.GetSubsystem = function (id) {
            return mSubsystems[id];
        };

    // Should not have any page-specific links other than loading the data.
    this.GetTestData = function () {
        var helpSystem = this;

        if (!mTestPromise) {
            mTestPromise = $.Deferred();
            if (!helpSystem.HasELearning) {
                mTestPromise.resolve(GetDefaultData());
                return mTestPromise.promise();
            }

            var path = helpSystem.GetPath() + "Data/test_data.js";
            require([path], function (data) {
                mTestPromise.resolve(ProcessData(data));
            }, function () {
                console.log("Test Data not loaded.")
                mTestPromise.resolve(GetDefaultData());
            });
        }

        return mTestPromise.promise();

        function GetDefaultData() {
            return {
                testData: {
                    numOfTopics: 1,
                    nonTestQuestionAnswer: {},
                    testQuestionAnswer: {},
                    lastTopics: {},
                    unlinkedEntries: []
                },
                testTopics: [],
                resultsConfig: GetResultsConfig()
            };
        }

        function ProcessData(data) {
            const testTopicData = ProcessTopics(data.testTopics);
            const nonTestTopicData = ProcessTopics(data.nonTestTopics);
            const nonTocTopicData = ProcessTopics(data.nonTocTopics);
            const testTopicUrls = Object.keys(testTopicData);
            const nonTestTopicUrls = Object.keys(nonTestTopicData);

            const testData = {
                passedUrl: data.testData.p,
                failedUrl: data.testData.f,
                passScore: data.testData.s / 100,
                percComplete: parseInt(data.testData.pc) / 100,
                title: data.testData.n,
                description: data.testData.d,
                id: data.testData.id,
                max: data.testData.m,
                randomize: data.testData.ra === "true",
                testRetries: parseInt(data.testData.tr),
                testQuestionAnswer: GetQuestionAnswerData(testTopicData),
                nonTestQuestionAnswer: GetQuestionAnswerData(nonTestTopicData),
                nonTocQuestionAnswer: GetQuestionAnswerData(nonTocTopicData),
                lastTopics: data.testData.lt,
                numOfTopics: data.testData.nt,
                unlinkedEntries: data.testData.ut
            }

            const resultsConfig = GetResultsConfig(data);

            return {
                useQuizForSuccess: data.q,
                numTopics: testTopicUrls.length + nonTestTopicUrls.length,
                testTopics: testTopicUrls,
                testData: testData,
                resultsConfig: resultsConfig
            };
        }

        function GetResultsConfig(data) {
            if (data && data.tr)
                return {
                    ShowPassFail: data.tr.pf === "1",
                    ShowScore: data.tr.s === "1",
                    UsePercentage: data.tr.p === "1",
                    ShowQuestions: data.tr.q === "1",
                    ShowUserAnswers: data.tr.ua === "1",
                    ShowCorrectAnswers: data.tr.ca === "1",
                    ShowQuestionFeedback: data.tr.rf === "1"
                }
            else
                return {
                    ShowPassFail: true,
                    ShowScore: true,
                    UsePercentage: true,
                    ShowQuestions: true,
                    ShowUserAnswers: true,
                    ShowCorrectAnswers: true,
                    ShowQuestionFeedback: true
                }
        }

        function ProcessTopics(dataTopics) {
            var topics = {};
            for (var url in dataTopics) {
                var pUrl = ProcessTopicUrl(url);
                topics[pUrl] = dataTopics[url];
            }

            return topics;
        }

        function ProcessTopicUrl(url) {
            var processedUrl = helpSystem.GetPath() + encodeURI(url);
            return MadCap.Utilities.Url.GetAbsolutePath(processedUrl);
        }

        function GetQuestionAnswerData(processedTopicData) {
            var processedQuestions = {};
            for (var url in processedTopicData) {
                var topic = processedTopicData[url];
                for (id in topic.questions) {
                    var questionData = topic.questions[id];
                    questionData.title = topic.title;
                    questionData.sourceUrl = url;
                    processedQuestions[id] = questionData;
                }
            }
            return processedQuestions;
        }
    };

    this.GetMergedAliasIDs = function (OnCompleteFunc) {
        mAliasFile.Load(function () {
            function OnGetIDs(ids) {
                for (var i = 0, length2 = ids.length; i < length2; i++) {
                    mergedIDs[mergedIDs.length] = ids[i];
                }

                    completed++;

                    if (completed == length + 1) {
                        OnCompleteFunc(mergedIDs);
                    }
                }

                var mergedIDs = new Array();
                var length = mSubsystems.length;
                var completed = 0;
                var ids = mAliasFile.GetIDs();

                OnGetIDs(ids);

                for (var i = 0; i < length; i++) {
                    mSubsystems[i].GetMergedAliasIDs(OnGetIDs);
                }
            });
        };

        this.GetMergedAliasNames = function (OnCompleteFunc) {
            mAliasFile.Load(function () {
                function OnGetNames(names) {
                    for (var i = 0, length2 = names.length; i < length2; i++) {
                        mergedNames[mergedNames.length] = names[i];
                    }

                    completed++;

                    if (completed == length + 1) {
                        OnCompleteFunc(mergedNames);
                    }
                }

                var mergedNames = new Array();
                var length = mSubsystems.length;
                var completed = 0;
                var names = mAliasFile.GetNames();

                OnGetNames(names);

                for (var i = 0, length = mSubsystems.length; i < length; i++) {
                    mSubsystems[i].GetMergedAliasNames(OnGetNames);
                }
            });
        };

        this.LookupCSHID = function (id, OnCompleteFunc) {
            mAliasFile.Load(function () {
                function OnLookupCSHID(idInfo) {
                    if (found)
                        return;

                    completed++;

                    if (idInfo.Found) {
                        found = true;

                        OnCompleteFunc(idInfo);

                        return;
                    }

                    if (completed == length) {
                        OnCompleteFunc(cshIDInfo);
                    }
                }

                var cshIDInfo = mAliasFile.LookupID(id);

                if (cshIDInfo.Found) {
                    cshIDInfo.Topic = mSelf.GetPath() + cshIDInfo.Topic;

                    OnCompleteFunc(cshIDInfo);

                    return;
                }

                if (mSubsystems.length == 0) {
                    OnCompleteFunc(cshIDInfo);
                    return;
                }

                var found = false;
                var completed = 0;

                for (var i = 0, length = mSubsystems.length; i < length; i++) {
                    mSubsystems[i].LookupCSHID(id, OnLookupCSHID);
                }
            });
        };

        this.GetTocFile = function () {
            return mTocFile;
        };

        this.GetBrowseSequenceFile = function () {
            return mBrowseSequenceFile;
        };

        this.IsMerged = function () {
            return (mSubsystems.length > 0);
        };

        this.IsRoot = function () {
            return mParentSubsystem == null;
        };

        this.IsTabletLayout = function (width) {
            return this.isGivenLayout('Tablet', width);
        };

        this.getLayout = function (width) {
            if(this.isGivenLayout('Mobile', width)) {
                return 'Mobile';
            } else if (this.isGivenLayout('Tablet', width)) {
                return 'Tablet'
            } else {
                return 'Web';
            }
        };

        this.isGivenLayout = function (layoutName, width) {
            if (this.IsResponsive && this.Breakpoints) {
                var breakpoint = this.Breakpoints.mediums[layoutName];
                var prop = this.Breakpoints.prop;

                if (prop == "max-width") {
                    if (!width)
                        width = window.innerWidth;

                    return width <= breakpoint;
                }
                else {
                    return window.matchMedia('(' + prop + ': ' + breakpoint + 'px)').matches;
                }
            }

            return false;
        };

        this.ProcessMicroContentXhtml = function (html, url) {
            var sanitized = MadCap.Utilities.SanitizeHtml('<div>' + html + '</div>');

            var microContentRoot = new MadCap.Utilities.Url(url).ToFolder();

            var isTriPane = MadCap.Utilities.HasRuntimeFileType("TriPane");
            var hrefPrefix = isTriPane ? '#' : '';

            var $content = MadCap.Utilities.FixLinks(sanitized, microContentRoot, hrefPrefix, this.ContentFolder);
            return $content;
        };

        this.LoadLanguage = function (onCompleteFunc, loadContextObj) {
            var self = this;

            require([this.LanguageUrl], function (language) {
                self.Language = language;

                onCompleteFunc.call(loadContextObj, language);
            });
        };

        this.LoadBreakpoints = function (onCompleteFunc, loadContextObj) {
            if (this.IsResponsive && this.IsRoot()) {
                var self = this;

                require([this.BreakpointsUrl], function (breakpoints) {
                    self.Breakpoints = breakpoints;

                    onCompleteFunc.call(loadContextObj, breakpoints);
                });
            }
            else {
                onCompleteFunc.call(loadContextObj, null);
            }
        };

        this.LoadConcepts = function () {
            var deferred = $.Deferred();

            require([mPath + "Data/Concepts.js"], function (concepts) {
                mConceptMap = concepts;
                deferred.resolve(mConceptMap);
            });

            return deferred.promise();
        };

        this.LoadAllConcepts = function (OnCompleteFunc) {
            function OnLoadConceptsComplete() {
                completed++;

                if (completed == length + 1)
                    OnCompleteFunc();
            }

            var completed = 0;
            var length = mSubsystems.length;

            this.LoadConcepts().then(OnLoadConceptsComplete);

            for (var i = 0; i < length; i++) {
                var subsystem = mSubsystems[i];

                if (!subsystem.GetExists()) {
                    OnLoadConceptsComplete();
                    continue;
                }

                subsystem.LoadAllConcepts(OnLoadConceptsComplete);
            }
        };

        this.GetConceptsLinks = function (conceptTerms){
            var deferred = $.Deferred();
            var links = [];

            if (this.IsWebHelpPlus) {
                function OnGetTopicsForConceptsComplete(xmlDoc, args) {
                    var nodes = xmlDoc.documentElement.getElementsByTagName("Url");
                    var nodeLength = nodes.length;

                    for (var i = 0; i < nodeLength; i++) {
                        var node = nodes[i];
                        var title = node.getAttribute("Title");
                        var url = node.getAttribute("Source");

                        url = mPath + ((url.charAt(0) == "/") ? url.substring(1, url.length) : url);

                        links[links.length] = { Title: title, Link: url };
                    }

                    deferred.resolve(links);
                }

                MadCap.Utilities.Xhr.CallWebService(mPath + "Service/Service.asmx/GetTopicsForConcepts?Concepts=" + conceptTerms, true, OnGetTopicsForConceptsComplete);
            }
            else {
                function UnionLinks(links2) {
                    links = links.Union(links2);
                }

                conceptTerms = conceptTerms.replace("\\;", "%%%%%");

                if (conceptTerms == "")
                    deferred.resolve(links);
                else {
                    var concepts = conceptTerms.split(";");

                    var deferreds = [];

                    deferreds.push(this.GetConceptsLinksLocal(concepts).then(UnionLinks));

                    for (var i = 0; i < mSubsystems.length; i++) {
                        var subsystem = mSubsystems[i];

                        if (subsystem.GetExists())
                            deferreds.push(subsystem.GetConceptsLinks(conceptTerms).then(UnionLinks));
                    }

                    $.when.apply(this, deferreds).done(function () {
                        deferred.resolve(links);
                    });
                }
            }

            return deferred.promise();
        };

        this.GetConceptsLinksLocal = function (concepts) {
            var links = [];
            var deferreds = [];

            for (var i = 0; i < concepts.length; i++) {
                var concept = concepts[i];

                concept = concept.replace("%%%%%", ";");
                //concept = concept.toLowerCase();

                deferreds.push(this.GetConceptLinks(concept).then(function(currLinks) {
                    links = links.concat(currLinks);
                }));
            }

            var deferred = $.Deferred();

            $.when.apply(this, deferreds).done(function () {
                deferred.resolve(links);
            });

            return deferred.promise();
        };

        this.LoadTopicChunk = function (chunkId) {
            var deferred = $.Deferred();

            MadCap.Utilities.Require([mPath + "Data/SearchTopic_Chunk" + chunkId + ".js"], function (data) {
                deferred.resolve(data);
            });

            return deferred.promise();
        }

        this.GetSearchChunkIds = function (topicIds) {
            var deferred = $.Deferred();

            MadCap.Utilities.Require([mPath + "Data/Search.js"], function (data) {
                var topicChunkMap = data.t;

                var searchChunks = [];
                // find search chunks to load
                for (var i = 0; i < topicIds.length; i++)
                    searchChunks.push(MadCap.Utilities.GetChunkId(topicChunkMap, topicIds[i], function (a, b) {
                        if (a < b)
                            return -1;
                        else if (a === b)
                            return 0;
                        else
                            return 1;
                    }));

                deferred.resolve(searchChunks);
            });

            return deferred.promise();
        }

        this.GetConceptLinks = function (concept) {
            var deferred = $.Deferred();
            var self = this;

            this.LoadConcepts().done(function () {
                var links = [];
                var topicIds = mConceptMap[concept];

                if (!topicIds)
                    deferred.resolve(links);
                else {
                    self.GetSearchChunkIds(topicIds).then(function (chunkIds) {
                        var deferreds = [];

                        // load search chunks
                        for (var i = 0; i < chunkIds.length; i++) {
                            var chunkId = chunkIds[i];
                            deferreds.push(self.LoadTopicChunk(chunkId).then(function (data) {
                                for (var i = 0; i < topicIds.length; i++) {
                                    var topic = data[topicIds[i]];
                                    if (topic) {
                                        topic.Url = self.GetTopicPath(topic.u);
                                        links.push(topic);
                                    }
                                }
                            }));
                        }

                        $.when.apply(this, deferreds).done(function () {
                            deferred.resolve(links);
                        });
                    });
                }
            });

            return deferred.promise();
        };

        this.LoadToc = MadCap.Utilities.Memoize(function (tocArray) {
            var deferred = $.Deferred();
            var type = tocArray[0];
            var url = tocArray[1];

            this.GetToc(type, url, function (toc) {
                deferred.resolve(toc);
            });

            return deferred.promise();
        });

        this.GetToc = function (type, tocUrl, OnCompleteFunc) {
            var self = this;
            var url = this[type + "Url"];
            if (tocUrl) {
                url = GetComputedDataFileUrl(tocUrl);
            }

            require([url], function (toc) {
                if (typeof toc == 'undefined' || (self[type] && toc.chunks)) {
                    OnCompleteFunc(toc);
                    return;
                }

                self[type] = toc;

                toc.type = type;
                toc.helpSystem = self;

                // Create chunks array
                toc.chunks = [];
                toc.entries = [];
                toc.nodes = {};
                var dataPath = new MadCap.Utilities.Url(url).ToFolder();

                for (var c = 0; c < toc.numchunks; c++) {
                    toc.chunks[c] = {
                        path: dataPath.AddFile(toc.prefix + c + '.js').FullPath,
                        loaded: false
                    };
                }

                // Traverse toc nodes and set relationships, store nodes to merge
                var node = toc.tree;
                var merge = {};
                toc.automerge = toc.tree;

                while (node != null) {
                    node.toc = toc;
                    node.childrenLoaded = false;

                    toc.nodes[node.i] = node;

                    if (typeof node.m !== 'undefined') {
                        merge[node.m] = node;
                    }

                    if (typeof node.a !== 'undefined') {
                        toc.automerge = node;
                    }

                    if (typeof node.n !== 'undefined') {
                        for (var i = 0; i < node.n.length; i++) {
                            node.n[i].parent = node; // parent
                            if (i < node.n.length - 1)
                                node.n[i].next = node.n[i + 1]; // next sibling
                            if (i > 0)
                                node.n[i].previous = node.n[i - 1]; // previous sibling
                        }
                    }

                    node = GetNextNode(node);
                }

                // If we have no subsystems, return
                var mergeSystems = [];
                var hasAutoMergeSystems = false;

                for (var i = 0; i < mSubsystems.length; i++) {
                    var subsystem = mSubsystems[i];

                    if (subsystem.GetExists()) {
                        if (!subsystem.IsAutoMerged)
                            subsystem.MergeNode = merge[i];
                        else
                            hasAutoMergeSystems = true;

                        if (subsystem.IsAutoMerged || typeof subsystem.MergeNode !== 'undefined')
                            mergeSystems.push(subsystem);
                    }
                    else {
                        RemoveNode(merge[i]);
                    }
                }

                if (!hasAutoMergeSystems && toc.automerge.a == 'replace')
                    RemoveNode(toc.automerge);

                if (mergeSystems.length == 0) {
                    OnCompleteFunc(toc);
                    return;
                }

                // Merge tocs
                MadCap.Utilities.AsyncForeach(mergeSystems, function (system, callback) {
                    MergeTocs(toc, system, callback);
                }, function () {
                    OnCompleteFunc(toc);
                });
            });
        };

        function RemoveNode(node) {
            var parent = node.parent;
            var previous = node.previous;
            var next = node.next;

            if (previous) {
                previous.next = next;
                node.previous = null;
            }
            if (next) {
                next.previous = previous;
                node.next = null;
            }
            if (parent) {
                var i = node.parent.n.indexOf(node);
                parent.n.splice(i, 1);
                node.parent = null;
            }
        }

        function GetNextNode(node) {
            var next = null;

            if (typeof node.n != 'undefined') {
                next = node.n[0]; // first child
            }
            else if (typeof node.next != 'undefined') {
                next = node.next;
            }
            else {
                next = node;

                while (typeof next.parent != 'undefined') {
                    if (typeof next.parent.next != 'undefined') {
                        next = next.parent.next;
                        break;
                    }
                    else {
                        next = next.parent;
                    }
                }

                if (typeof next.parent == 'undefined') { // reached root
                    next = null;
                }
            }

            return next;
        }

        function GetPreviousNode(node) {
            var previous = null;

            if (typeof node.previous != 'undefined') {
                previous = node.previous;

                while (typeof previous.n !== 'undefined' && previous.n.length > 0) {
                    previous = previous.n[previous.n.length - 1];
                }
            }
            else if (typeof node.parent != 'undefined') {
                previous = node.parent;
            }

            return previous;
        }

        function GetTocPath(node, encode) {
            var tocPath = "";
            var linkNodeIndex = -1;
            var pathSeparator = encode ? "%7C" : "|";

            if (node.n && node.n.length > 0) {
                tocPath = node.toc.entries[node.i].title;
                if (encode)
                    tocPath = encodeURIComponent(tocPath);

                linkNodeIndex = 0;
            }
            else {
                linkNodeIndex = node.parent.n.indexOf(node) + 1;
            }

            if (tocPath.length > 0)
                tocPath += pathSeparator;

            tocPath += ("_____" + linkNodeIndex);

            for (var currNode = node.parent; currNode && typeof currNode.i !== 'undefined'; currNode = currNode.parent) {
                if (tocPath == null)
                    tocPath = "";

                if (tocPath.length > 0)
                    tocPath = pathSeparator + tocPath;

                var entry = currNode.toc.entries[currNode.i];
                if (entry) {
                    var tocTitle = entry.title;
                    if (encode)
                        tocTitle = encodeURIComponent(tocTitle);
                    tocPath = tocTitle + tocPath;
                }
            }

            return tocPath;
        }

        function MergeTocs(toc1, subsystem, OnCompleteFunc) {
            subsystem.GetToc(toc1.type, null, function (toc2) {
                if (typeof toc2 == 'undefined') {
                    OnCompleteFunc();
                    return;
                }

                var node = subsystem.IsAutoMerged ? toc1.automerge : subsystem.MergeNode;
                var newNode = toc2.tree;

                if (newNode.n !== undefined && node !== undefined) {
                    var replace = node.r == 1 || (subsystem.IsAutoMerged && node.a == 'replace');
                    var insertBefore = replace || (subsystem.IsAutoMerged && (node.a == 'before-head' || node.a == 'after-head'));
                    var insertSibling = replace || (subsystem.IsAutoMerged && (node.a == 'before-head' || node.a == 'after-tail'));
                    var parent = insertSibling ? node.parent : node;

                    if (typeof parent.n == 'undefined') {
                        parent.n = [];
                    }

                    var childIndex = insertSibling ? parent.n.indexOf(node) + (insertBefore ? 0 : 1)
                                                   : insertBefore ? 0 : parent.n.length;
                    var length = newNode.n.length;

                    for (var i = 0; i < length; i++) {
                        newNode.n[i].parent = parent;
                        parent.n.splice(childIndex + i, 0, newNode.n[i]);
                    }

                    if (replace) {
                        parent.n.splice(childIndex + length, 1);
                    }

                    if (childIndex > 0) {
                        parent.n[childIndex].previous = parent.n[childIndex - 1];
                        parent.n[childIndex - 1].next = parent.n[childIndex];
                    }

                    var lastChildIndex = childIndex + length - (replace ? 1 : 0) - 1;
                    if (lastChildIndex >= 0 && lastChildIndex + 1 < parent.n.length) {
                        parent.n[lastChildIndex].next = parent.n[lastChildIndex + 1];
                        parent.n[lastChildIndex + 1].previous = parent.n[lastChildIndex];
                    }

                    if (subsystem.IsAutoMerged) {
                        toc1.automerge = parent.n[childIndex + length - 1];
                        toc1.automerge.a = 'after-tail';
                    }
                }

                OnCompleteFunc();
            });
        }

        this.LoadTocChunk = function (toc, chunkIndex) {
            var deferred = $.Deferred();
            require([toc.chunks[chunkIndex].path], function (chunk) {
                // load entries from chunk if not loaded already
                if (!toc.chunks[chunkIndex].loaded) {
                    for (var link in chunk) {
                        for (var i = 0; i < chunk[link].i.length; i++) {
                            toc.entries[chunk[link].i[i]] = {
                                link: link,
                                title: chunk[link].t[i],
                                bookmark: chunk[link].b[i]
                            };
                        }
                    }

                    toc.chunks[chunkIndex].loaded = true;
                }

                return deferred.resolve(chunk);
            });

            return deferred.promise();
        }

        this.GetTocEntryHref = function (entry, tocType, useHash, appendTocPath) {
            var href = null;
            var toc = entry.toc;
            var e = toc.entries[entry.i];

            if (e) {
                var link = e.link + e.bookmark;

                if (typeof entry.m == 'undefined' && link != '___') { // '___' is a placeholder for no href
                    var url = null;
                    var linkUrl = new MadCap.Utilities.Url(link);
                    var helpSystem = toc.helpSystem;
                    var helpSystemPath = helpSystem.GetPath();
                    var root = helpSystem.GetMasterHelpSystem().GetContentPath();
                    var hasFrame = typeof entry.f != 'undefined';

                    if (!linkUrl.IsAbsolute) {
                        if (!MadCap.String.IsNullOrEmpty(helpSystemPath)) {
                            linkUrl = new MadCap.Utilities.Url(helpSystemPath).CombinePath(link);

                            url = linkUrl.ToRelative(root);
                        }
                        else {
                            linkUrl = linkUrl.ToRelative("/" + root);
                            url = linkUrl;
                        }
                    }
                    else {
                        url = linkUrl;
                    }

                    if (hasFrame || !useHash) {
                        if (url.IsAbsolute)
                            href = url.FullPath;
                        else
                            href = root + url.FullPath;
                    }
                    else if (MadCap.Utilities.HasRuntimeFileType("TriPane")) {
                        href = "#" + url.FullPath;
                    } else {
                        href = linkUrl.FullPath;
                    }
                }
            }

            if (href != null && tocType && appendTocPath) {
                var isEmbeddedTopic = window.name == "topic" && !MadCap.Utilities.HasRuntimeFileType("Default");
                var tocPath = GetTocPath(entry, true);
                if (MadCap.Utilities.HasRuntimeFileType("TriPane")) {
                    href += encodeURI('?' + tocType + 'Path=' + tocPath);
                } else {
                    var hrefUrl = new MadCap.Utilities.Url(href);
                    if (isEmbeddedTopic) {
                        href = hrefUrl.PlainPath + encodeURI('?' + tocType + 'Path=' + tocPath) + hrefUrl.Fragment;
                    } else {
                        href = hrefUrl.PlainPath + '?' + (tocType + 'Path=' + tocPath) + hrefUrl.Fragment;
                    }
                }
            }

            return href;
        }

    this.LoadTocDataFromQuery = function () {
        var mHelpSystem = this.GetMasterHelpSystem();
        var contentFolder = mHelpSystem.GetContentPath();
        var current = new MadCap.Utilities.Url(document.location.href);

        if (current.isFolder) {
            var ext = new MadCap.Utilities.Url(mHelpSystem.DefaultStartTopic).Extension;
            current = current.AddFile("default." + ext);  // if current path is folder, check for default.htm
        }

        var contentPath = current.ToFolder().CombinePath(contentFolder);
        var href = current.ToRelative(contentPath);

        return this.GetTocData(new MadCap.Utilities.Url(href.FullPath));
    }

    this.GetTocData = function (href) {
        var tocType = null, tocPath = null, bsPath = null;
        var isTriPane = MadCap.Utilities.HasRuntimeFileType("TriPane");

            if (isTriPane && !MadCap.String.IsNullOrEmpty(href.Fragment) && href.Fragment.length > 1 || !isTriPane) {

                var queryMapUrl = (isTriPane && !(href.QueryMap.GetItem('TocPath') || href.QueryMap.GetItem('BrowseSequencesPath')) && !MadCap.String.IsNullOrEmpty(href.Fragment)) ? new MadCap.Utilities.Url(href.Fragment) : href;
                tocPath = queryMapUrl.QueryMap.GetItem('TocPath');


                if (tocPath != null) {
                    tocType = 'Toc';
                }
                else {
                    bsPath = queryMapUrl.QueryMap.GetItem('BrowseSequencesPath');

                    if (bsPath != null) {
                        tocType = 'BrowseSequences';
                    }
                }

                if (href.HashMap.GetItem('cshid') == null) {
                    var iq1 = href.Query.indexOf('?');
                    var iq2 = href.Query.lastIndexOf('?');
                    var query = '';
                    if (iq1 != iq2) {
                        query = href.Query.substr(iq1, iq2);
                    }
                    if (isTriPane)
                        href = new MadCap.Utilities.Url(href.Fragment.substr(1));
                    if (!MadCap.String.IsNullOrEmpty(query)) {
                        href.Query = query;
                    }
                }
            }
            else {
                href = new MadCap.Utilities.Url(this.DefaultStartTopic).ToRelative(this.GetContentPath());
            }

            return { TocType: tocType, TocPath: tocPath, BrowseSequencesPath: bsPath, Href: href };
        }

        this.FindTocNode = function (title, href, onCompleteFunc, tocUrl) {
            mSelf.FindNode("Toc", title, href, onCompleteFunc, tocUrl);
        }

        this.FindBrowseSequenceNode = function (title, href, onCompleteFunc) {
            mSelf.FindNode("BrowseSequences", title, href, onCompleteFunc);
        }

        this.FindNode = function (tocType, tocPath, href, onCompleteFunc, tocUrl) {
            mSelf.FindNodeInToc(tocType, tocPath, href, onCompleteFunc, tocUrl, true);
        }

        this.FindNodeInToc = function (tocType, tocPath, href, onCompleteFunc, tocUrl, loadSubsystems) {
            mSelf.LoadToc([tocType, tocUrl]).then(function (toc) {
                var root = new MadCap.Utilities.Url(mSelf.GetMasterHelpSystem().GetContentPath());
                var url = href;
                var chunkIndex = 0;
                var foundNode;

                if (!href.IsAbsolute) {
                    var url = !MadCap.String.IsNullOrEmpty(root.FullPath) ? root.CombinePath(href) : href;
                    url = url.ToRelative(mSelf.GetPath());

                    url = new MadCap.Utilities.Url('/' + url.FullPath);
                }

                // find chunk
                for (var i = 1; i < toc.chunkstart.length; i++) {
                    if (toc.chunkstart[i] <= decodeURIComponent(url.PlainPath))
                        chunkIndex++;
                }

                mSelf.LoadTocChunk(toc, chunkIndex).then(function (chunk) {
                    var entry = chunk[decodeURIComponent(url.PlainPath)];

                    if (typeof entry !== 'undefined') {
                        var ids = [];

                        if (!foundNode)
                            foundNode = toc.nodes[entry.i[0]];

                        if (tocPath) {
                            var isTriPane = MadCap.Utilities.HasRuntimeFileType("TriPane");
                            for (var i = 0; i < entry.i.length; i++) {
                                var path = GetTocPath(toc.nodes[entry.i[i]]);
                                var comparePath = isTriPane || MadCap.Topic.IsEmbeddedTopic() ? path : MadCap.Utilities.Url.GetSafetyProcessedComponent(path);
                                if (comparePath == tocPath) {
                                    ids.push(entry.i[i]);
                                }
                            }
                        }
                        else {
                            for (var i = 0; i < entry.i.length; i++) {
                                if (entry.b[i].toLowerCase() == decodeURIComponent(url.Fragment).toLowerCase()) {
                                    ids.push(entry.i[i]);
                                }
                            }
                        }

                        if (ids.length) {
                            onCompleteFunc(toc.nodes[ids.pop()]);
                            return;
                        }
                    }

                    if (mSubsystems.length > 0 && loadSubsystems) {
                        MadCap.Utilities.AsyncForeach(mSubsystems,
                            function (subSystem, callback) {
                                subSystem.FindNode(tocType, tocPath, href, function (node) {
                                    if (typeof node !== 'undefined') {
                                        onCompleteFunc(node);
                                        return;
                                    }
                                    callback();
                                });
                            },
                            function () {
                                onCompleteFunc(foundNode);
                            }
                        );
                    }
                    else {
                        onCompleteFunc(foundNode);
                    }
                });
            });
        }

        this.NodeDepth = function (node) {
            var depth = 1;
            while (node.parent && node.c !== undefined) {
                depth++;
                node = node.parent;
            }

            return depth;
        }

        this.LoadGlossary = function (onCompleteFunc, loadContextObj) {
            if (typeof this.Glossary != 'undefined') {
                onCompleteFunc.call(loadContextObj, this.Glossary);
                return;
            }

            var self = this;

            this.GetGlossary(function (glossary) {
                // build term map
                if (glossary && glossary.terms) {
                    glossary.termMap = Object.create(null);

                    for (var i = 0; i < glossary.terms.length; i++) {
                        var term = glossary.terms[i];
                        glossary.termMap[term.t.toLowerCase()] = term;
                    }
                }

                self.Glossary = glossary;

                onCompleteFunc.call(loadContextObj, glossary);
            });
        }

        this.GetGlossary = function (OnCompleteFunc) {
            var self = this;

            require([this.GlossaryUrl], function (glossary) {
                function OnMergeGlossariesComplete() {
                    completed++;

                    if (completed == length) {
                        OnCompleteFunc(glossary);
                    }
                }

                if (typeof glossary == 'undefined') {
                    OnCompleteFunc(glossary);
                    return;
                }

                var completed = 0;
                var length = 0;

                // Create chunks array
                glossary.chunks = [];
                var dataPath = new MadCap.Utilities.Url(mSelf.GlossaryUrl).ToFolder();
                for (var c = 0; c < glossary.numchunks; c++) {
                    glossary.chunks.push({
                        helpSystem: self,
                        path: dataPath.AddFile(glossary.prefix + c + '.js').FullPath
                    });
                }

                // Calculate "length" first
                for (var i = 0; i < mSubsystems.length; i++) {
                    var subsystem = mSubsystems[i];

                    if (!subsystem.GetExists()) { continue; }

                    length++;
                }

                if (length == 0) {
                    OnCompleteFunc(glossary);
                    return;
                }

                for (var i = 0; i < mSubsystems.length; i++) {
                    var subsystem = mSubsystems[i];

                    if (!subsystem.GetExists()) {
                        continue;
                    }

                    MergeGlossaries(glossary, subsystem, OnMergeGlossariesComplete);
                }
            });
        };

        this.SearchGlossary = function (searchQuery) {
            var deferred = $.Deferred();
            var self = this;

            this.LoadGlossary(function (glossary) {
                var foundTerm = false;

                if (glossary && glossary.termMap) {
                    var term = glossary.termMap[searchQuery.toLowerCase()];
                    foundTerm = typeof term != 'undefined';
                    if (foundTerm) {
                        var chunk = glossary.chunks[term.c];
                        require([chunk.path], function (data) {
                            var entry = {
                                term: term.t,
                                definition: data[term.t].d,
                                link: data[term.t].l
                            };

                            if (entry.link) {
                                var helpSystem = chunk.helpSystem;
                                var link = (new MadCap.Utilities.Url("../")).CombinePath(entry.link).FullPath;
                                entry.link = helpSystem.GetTopicPath(link).FullPath;

                                helpSystem.SearchDB.LoadTopicByUrl(link).done(function (topicID, topic) {
                                    if (topic)
                                        entry.abstractText = topic.a;

                                    deferred.resolve(entry);
                                });
                            }
                            else {
                                deferred.resolve(entry);
                            }
                        });
                    }
                }

                if (!foundTerm)
                    deferred.resolve();
            }, this);

            return deferred.promise();
        };

        this.LoadIndex = function (onCompleteFunc, loadContextObj) {
            if (typeof this.Index !== 'undefined') {
                onCompleteFunc.call(loadContextObj, this.Index);
                return;
            }

            var self = this;

            this.GetIndex(function (index) {
                self.Index = index;

                onCompleteFunc.call(loadContextObj, index);
            });
        }

        this.GetIndex = function (OnCompleteFunc) {
            var self = this;

            require([this.IndexUrl], function (index) {
                function OnMergeIndexesComplete() {
                    completed++;

                    if (completed == length) {
                        OnCompleteFunc(index);
                    }
                }

                if (typeof index == 'undefined') {
                    OnCompleteFunc(index);
                    return;
                }

                var completed = 0;
                var length = 0;

                // Create chunks array
                index.chunks = [];
                var dataPath = new MadCap.Utilities.Url(mSelf.IndexUrl).ToFolder();
                for (var c = 0; c < index.numchunks; c++) {
                    index.chunks.push({
                        helpSystem: self,
                        path: dataPath.AddFile(index.prefix + c + '.js').FullPath
                    });
                }

                // Calculate "length" first
                for (var i = 0; i < mSubsystems.length; i++) {
                    var subsystem = mSubsystems[i];

                    if (!subsystem.GetExists()) { continue; }

                    length++;
                }

                if (length == 0) {
                    OnCompleteFunc(index);
                    return;
                }

                for (var i = 0; i < mSubsystems.length; i++) {
                    var subsystem = mSubsystems[i];

                    if (!subsystem.GetExists()) {
                        continue;
                    }

                    MergeIndexes(index, subsystem, OnMergeIndexesComplete);
                }
            });
        };

        this.LoadRootIndexEntry = function (rootEntry, onCompleteFunc) {
            if (rootEntry.loaded) {
                if (onCompleteFunc)
                    onCompleteFunc(rootEntry);
                return;
            }

            this.LoadIndex(function (index) {
                var chunks = typeof rootEntry.c == 'number' ? [rootEntry.c] : rootEntry.c;

                MadCap.Utilities.AsyncForeach(chunks, function (c, callback) {
                    var chunk = index.chunks[c];

                    // Load chunk
                    require([chunk.path], function (data) {
                        var entry = data[rootEntry.t];

                        mSelf.SetIndexEntryHelpSystem(entry, chunk.helpSystem);
                        mSelf.MergeIndexEntries(rootEntry, entry);

                        callback();
                    });
                },
                function () {
                    mSelf.LoadIndexEntry(rootEntry);

                    if (onCompleteFunc)
                        onCompleteFunc(rootEntry);
                });
            });
        }

        this.SetIndexEntryHelpSystem = function (entry, helpSystem) {
            if (entry.l) {
                $.each(entry.l, function (index, link) {
                    link.helpSystem = helpSystem;
                });
            }

            if (entry.e) {
                $.each(entry.e, function (term, entry) {
                    mSelf.SetIndexEntryHelpSystem(entry, helpSystem);
                });
            }
        }

        this.LoadIndexEntry = function (entry) {
            // Load link (if exists)
            if (entry.l) {
                var linkList = [];

                $.each(entry.l, function (i, indexLink) {
                    var link = { Title: indexLink.t, Link: indexLink.helpSystem.GetTopicPath(".." + indexLink.u).FullPath };

                    linkList[linkList.length] = link;
                });

                entry.linkList = mSelf.SortLinkList(linkList);
            }

            if (entry.e) {
                $.each(entry.e, function (term, entry) {
                    mSelf.LoadIndexEntry(entry);
                });
            }

            entry.loaded = true;
        }

        this.MergeIndexEntries = function (entry1, entry2) {
            // merge links
            if (entry2.l) {
                if (typeof entry1.l == 'undefined') {
                    entry1.l = entry2.l;
                }
                else {
                    entry1.l = entry1.l.concat(entry2.l);
                }
            }

            // merge see also
            if (entry2.r) {
                if (typeof entry1.r == 'undefined') {
                    entry1.r = entry2.r;
                }
                else if (entry1.r == 'SeeAlso' || entry2.r == 'SeeAlso') {
                    entry1.r = 'SeeAlso';
                }

                if (typeof entry1.f == 'undefined') {
                    entry1.f = entry2.f;
                }
                else {
                    var seeAlso1 = entry1.f.split(';');
                    var seeAlso2 = entry2.f.split(';');
                    $.each(seeAlso2, function (i, seeAlso) {
                        if ($.inArray(seeAlso1, seeAlso)) {
                            seeAlso1.push(seeAlso);
                        }
                    });
                    seeAlso1.sort(function (term1, term2) {
                        var t1 = term1.toLowerCase();
                        var t2 = term2.toLowerCase();

                        return t1 < t2 ? -1 : t1 > t2 ? 1 : 0;
                    });
                    entry1.f = seeAlso1.join('; ');
                }
            }

            // merge subentries
            if (entry2.e) {
                if (typeof entry1.e == 'undefined') {
                    entry1.e = {};
                }

                $.each(entry2.e, function (term, e) {
                    if (typeof entry1.e[term] !== 'undefined') {
                        mSelf.MergeIndexEntries(entry1.e[term], e);
                    }
                    else {
                        entry1.e[term] = e;
                    }
                });
            }
        }

        this.FindIndexEntry = function (terms, onCompleteFunc) {
            mSelf.LoadIndex(function (index) {
                if (!index.entries) {
                    index.entries = {};
                    $.each(index.terms, function (i, entry) {
                        index.entries[entry.t] = entry;
                    });
                }

                var termList = terms.split(':');
                var termCount = termList.length;
                var rootEntry = index.entries[termList[0]];

                if (rootEntry) {
                    mSelf.LoadRootIndexEntry(rootEntry, function (rootEntry) {
                        var entry = rootEntry;

                        for (var i = 1; i < termCount; i++) {
                            entry = entry.e[termList[i]];
                            if (!entry)
                                break;
                        }

                        if (onCompleteFunc)
                            onCompleteFunc(rootEntry, entry);
                    });
                }
                else {
                    if (onCompleteFunc)
                        onCompleteFunc();
                }
            });
        }

        this.SortLinkList = function (links) {
            links.sort(function (link1, link2) {
                var title1 = link1.Title.toLowerCase();
                var title2 = link2.Title.toLowerCase();

                return title1 < title2 ? -1 : title1 > title2 ? 1 : 0;
            });

            return links;
        }

        this.GetSearchDBs = function (OnCompleteFunc) {
            var searchDBs = new Array();
            var self = this;
            require([mPath + "Data/Search.js"], function(obj) {
                function OnGetSearchDBsComplete(searchDBs2) {
                    if (searchDBs2 != null) {
                        for (var i = 0; i < searchDBs2.length; i++) {
                            searchDBs[searchDBs.length] = searchDBs2[i];
                        }
                    }

                    completed++;

                    if (completed == length) {
                        OnCompleteFunc(searchDBs);
                    }
                }

                var completed = 0;
                var length = mSubsystems.length;

                var searchDB = new MadCap.WebHelp.Search.SearchDB(self);
                self.SearchDB = searchDB;
                searchDBs[searchDBs.length] = searchDB;
                searchDB.Load(obj, function () {
                    var preMerged = obj["pm"]; // TODO: add premerged variable in json file
                    if (preMerged || length == 0) {
                        OnCompleteFunc(searchDBs);
                    }
                    else {
                        for (var i = 0; i < length; i++) {
                            var subsystem = mSubsystems[i];

                            if (!subsystem.GetExists()) {
                                OnGetSearchDBsComplete(null);
                                continue;
                            }

                            subsystem.GetSearchDBs(OnGetSearchDBsComplete);
                        }
                    }
                });
            });
        };

        this.GetConcepts = function () {
            return mConceptMap;
        };

        this.GetSearchFilters = function () {
            return mFilterMap.map;
        };

        this.ParseSearchFilterDoc = function (xmlDoc) {
            filterMap = Object.create(null);

            if (xmlDoc != null) {
                var filters = xmlDoc.getElementsByTagName("SearchFilter");

                for (var i = 0; i < filters.length; i++) {
                    var filter = filters[i];
                    var name = filter.getAttribute("Name");
                    var order = filter.getAttribute("Order");
                    var concepts = filter.getAttribute("Concepts");

                    if (!concepts)
                        continue;

                    filterMap[name] = {
                        c: concepts,
                        o: order,
                        group: 0
                    };
                }
            }

            return filterMap;
        };

        this.LoadSearchFiltersLocal = function () {
            var deferred = $.Deferred();

            require([this.SearchFilterSetUrl], function (filterMap) {
                var filters = null;
                if (filterMap)
                    filters = { map: filterMap, count: 1 };

                deferred.resolve(filters);
            });

            return deferred.promise();
        };

        this.LoadSearchFilters = function () {
            var deferred = $.Deferred();

            if (!this.IsWebHelpPlus) {
                function MergeSearchFilters(filters) {
                    if (filters) {
                        if (!mergedFilters) {
                            mergedFilters = filters;

                            // initialize group (filter set id)
                            for (var filterName in filters.map)
                                filters.map[filterName].group = 0;
                        }
                        else {
                            for (var filterName in filters.map) {
                                if (!mergedFilters.map[filterName]) {
                                    mergedFilters.map[filterName] = filters.map[filterName];
                                    mergedFilters.map[filterName].group += mergedFilters.count;
                                }
                                else // filter exists, so merge
                                {
                                    var filter1 = mergedFilters.map[filterName];
                                    var filter2 = filters.map[filterName];

                                    // merge concepts
                                    var concepts1 = filter1.c.split(';');
                                    var concepts2 = filter2.c.split(';');
                                    filter1.c = concepts1.Union(concepts2).join(';');

                                    // merge comment
                                    if (MadCap.String.IsNullOrEmpty(filter1.cm))
                                        filter1.cm = filter2.cm;
                                }
                            }
                            mergedFilters.count += filters.count;
                        }
                    }
                }

                var mergedFilters;
                var deferreds = [];

                deferreds.push(this.LoadSearchFiltersLocal().then(MergeSearchFilters));

                for (var i = 0; i < mSubsystems.length; i++) {
                    var subsystem = mSubsystems[i];

                    if (subsystem.GetExists())
                        deferreds.push(subsystem.LoadSearchFilters().then(MergeSearchFilters));
                }

                $.when.apply(this, deferreds).done(function () {
                    mFilterMap = mergedFilters;

                    deferred.resolve(mergedFilters);
                });
            }
            else {
                var mSelf = this;

                MadCap.Utilities.Xhr.CallWebService(mPath + "Service/Service.asmx/GetSearchFilters", true, function (xmlDoc, args) {
                    var filterMap = mSelf.ParseSearchFilterDoc(xmlDoc);

                    deferred.resolve({ map: filterMap });
                });
            }

            return deferred.promise();
        };

        this.AdvanceTopic = function (tocType, moveType, tocPath, appendTocPath, href, CallBackFunc) {
            var file = null;

            mSelf.FindNode(tocType, tocPath, href, function (node) {
                if (node) {
                    function AdvanceNode(node, moveType) {
                        node = moveType == 'next' ? GetNextNode(node) : GetPreviousNode(node);

                        if (node && typeof node.i !== 'undefined') {
                            mSelf.LoadTocChunk(node.toc, node.c).then(function (chunk) {
                                var entry = node.toc.entries[node.i];
                                var link = mSelf.GetTocEntryHref(node, tocType, true, appendTocPath);

                                if (link) {
                                    if (MadCap.String.StartsWith(link, '#')) {
                                        link = link.substring(1);
                                    }

                                    CallBackFunc(link);
                                }
                                else {
                                    AdvanceNode(node, moveType);
                                }
                            });
                        }
                    }

                    AdvanceNode(node, moveType);
                }
            });
        };

        this.SetBrowseSequencePath = function (bsPath, href) {
            var $currentTopicIndex = $(".current-topic-index-button");

            if (bsPath != null) {
                this.FindBrowseSequenceNode(bsPath, href, function (node) {
                    if (node && node.parent) {
                        $currentTopicIndex.removeClass("disabled");

                        $(".sequence-index").text(node.parent.n.indexOf(node) + 1);
                        $(".sequence-total").text(node.parent.n.length);
                    }
                    else {
                        $currentTopicIndex.addClass("disabled");
                    }
                });
            }
            else {
                $currentTopicIndex.addClass("disabled");
            }
        }

        this.GetSkins = function()
        {
           var skins = [];
           mSkinMap.ForEach( function( key, value )
           {
               skins.push(value);
           });

           return skins;
        }

        this.IsCentralLiveHelpServerType = function()
        {
            return this.LiveHelpServerType == "Central";
        }

        this.GetLanguageString = function (elementName, isComponent, componentName, propertyName, defaultValue) {
            if (MadCap.Utilities.HasRuntimeFileType("SkinPreview"))
                return defaultValue;

            var value;
            var elementProp = elementName + "/" + propertyName;
            if (isComponent && this.Language.component[componentName])
                value = this.Language.component[componentName][elementProp];
            else if (this.Language.skin["default"])
                value = this.Language.skin["default"][elementProp];

            return value ? value : defaultValue;
        }

        // Private member functions

        function GetDataFileUrl(xmlDoc, att) {
            var url = xmlDoc.documentElement.getAttribute(att);
            return GetComputedDataFileUrl(url);
        }

        function GetComputedDataFileUrl(url) {
            if (url == null) {
                return null;
            }

            var root = new MadCap.Utilities.Url(mPath);

            if (!root.IsAbsolute) {
                return mPath + url;
            }

            return root.AddFile(url).ToRelative(document.location.href).FullPath;
        }

        function CacheSkins(xmlDoc) {
            var skinNodes = $("CatapultSkin", xmlDoc.documentElement);

            for (var i = 0, length = skinNodes.length; i < length; i++) {
                var skinNode = skinNodes[i];
                var $skinNode = $(skinNode);
                var skinID = $skinNode.attr("SkinID");
                var skinData = {};

                for (var j = 0, length2 = skinNode.attributes.length; j < length2; j++) {
                    var att = skinNode.attributes[j];
                    skinData[att.name] = att.value;
                }

                var children = $skinNode.children();

                for (var j = 0, length2 = children.length; j < length2; j++) {
                    var childNode = children[j];
                    var name = childNode.nodeName;
                    var nodeData = {};
                    skinData[name] = nodeData;

                    for (var k = 0, length3 = childNode.attributes.length; k < length3; k++) {
                        var att = childNode.attributes[k];
                        nodeData[att.name] = att.value;
                    }
                }

                mSkinMap.Add(skinID, skinData);
            }
        }

        function ConvertGlossaryPageEntryToAbsolute(glossaryPageEntry, path) {
            if (glossaryPageEntry.nodeName.toLowerCase() == "madcap:glossarychunkref") {
                var $glossaryPageEntry = $(glossaryPageEntry);
                var href = $glossaryPageEntry.attr("src");

                if (!MadCap.String.IsNullOrEmpty(href)) {
                    var url = new MadCap.Utilities.Url(path).CombinePath("../../Data/").CombinePath(href);

                    var encodedPath = MadCap.Utilities.EncodeHtml("../" + url.FullPath);
                    $glossaryPageEntry.attr("src", encodedPath);
                }
            }
            else {
                var entryNode = glossaryPageEntry.getElementsByTagName("a")[0];
                var href = $(entryNode).attr("href");

                if (!MadCap.String.IsNullOrEmpty(href)) {
                    var url = new MadCap.Utilities.Url(path).CombinePath("../../Content/").CombinePath(href);

                    var encodedPath = MadCap.Utilities.EncodeHtml("../" + url.FullPath);
                    $(entryNode).attr("href", encodedPath);
                }
            }
        }

        function ConvertIndexLinksToAbsolute(indexEntry) {
            for (var i = 0; i < indexEntry.childNodes.length; i++) {
                var currNode = indexEntry.childNodes[i];

                if (currNode.nodeName == "Entries") {
                    for (var j = 0; j < currNode.childNodes.length; j++)
                        ConvertIndexLinksToAbsolute(currNode.childNodes[j]);
                }
                else if (currNode.nodeName == "Links") {
                    for (var j = 0; j < currNode.childNodes.length; j++) {
                        if (currNode.childNodes[j].nodeType == 1) {
                            var link = MadCap.Dom.GetAttribute(currNode.childNodes[j], "Link");

                            link = mPath + ((link.charAt(0) == "/") ? link.substring(1, link.length) : link);
                            currNode.childNodes[j].setAttribute("Link", link);
                        }
                    }
                }
            }
        }

        function MergeConceptLinks(links1, links2) {
            if (!links2)
                return;

            for (var i = 0; i < links2.length; i++)
                links1[links1.length] = links2[i];
        }

        function MergeGlossaries(glossary1, subsystem, OnCompleteFunc) {
            subsystem.GetGlossary(function (glossary2) {
                if (typeof glossary2 == 'undefined') {
                    OnCompleteFunc();
                    return;
                }

                // Append chunk paths
                glossary1.chunks = glossary1.chunks.concat(glossary2.chunks);

                // Merge terms
                for (var i = 0, j = 0; i < glossary1.terms.length && j < glossary2.terms.length; ) {
                    var entry1 = glossary1.terms[i];
                    var entry2 = glossary2.terms[j];

                    var term1 = entry1.t;
                    var term2 = entry2.t;

                    if (term1.toLowerCase() == term2.toLowerCase()) {
                        i++;
                        j++;
                    }
                    else if (term1.toLowerCase() > term2.toLowerCase()) {
                        entry2.c += glossary1.numchunks;
                        glossary1.terms.splice(i, 0, entry2);

                        j++;
                    }
                    else {
                        i++;
                    }
                }

                // Append remaining nodes.
                for (; j < glossary2.terms.length; j++) {
                    var entry = glossary2.terms[j];
                    entry.c += glossary1.numchunks;
                    glossary1.terms.push(entry);
                }

                glossary1.numchunks = glossary1.chunks.length;

                OnCompleteFunc();
            });
        }

        function MergeIndexes(index1, subsystem, OnCompleteFunc) {
            subsystem.GetIndex(function (index2) {
                if (typeof index2 == 'undefined') {
                    OnCompleteFunc();
                    return;
                }

                // Append chunk paths
                index1.chunks = index1.chunks.concat(index2.chunks);

                // Merge terms
                for (var i = 0, j = 0; i < index1.terms.length && j < index2.terms.length; ) {
                    var entry1 = index1.terms[i];
                    var entry2 = index2.terms[j];

                    var term1 = entry1.s || entry1.t; // sort as or term
                    var term2 = entry2.s || entry2.t;

                    if (term1 == term2 && entry1.t == entry2.t) {
                        if (typeof entry1.c == 'number') {
                            entry1.c = [entry1.c];
                        }

                        var chunks = entry2.c;
                        if (typeof entry2.c == 'number') {
                            chunks = [entry2.c];
                        }

                        $.each(chunks, function (index, chunk) {
                            entry1.c.push(chunk + index1.numchunks);
                        });

                        entry1.$ = (entry1.$ === 1 && entry2.$ === 1) ? 1 : 0;

                        i++;
                        j++;
                    }
                    else if (term1.toLowerCase() > term2.toLowerCase() ||
                        (term1.toLowerCase() == term2.toLowerCase() && entry1.t.toLowerCase() > entry2.t.toLowerCase())) {
                        entry2.c += index1.numchunks;
                        index1.terms.splice(i, 0, entry2);

                        j++;
                    }
                    else {
                        i++;
                    }
                }

                // Append remaining nodes.
                for (; j < index2.terms.length; j++) {
                    var entry = index2.terms[j];
                    entry.c += index1.numchunks;
                    index1.terms.push(entry);
                }

                index1.numchunks = index1.chunks.length;

                OnCompleteFunc();
            });
        }
    };

    (function () {
        MadCap.WebHelp.HelpSystem.LoadHelpSystem = MadCap.Utilities.Memoize(function(path) {
            var deferred = $.Deferred();

            var helpSystem = new MadCap.WebHelp.HelpSystem(null, null, path, null, null);
            helpSystem.Load(function () {
                deferred.resolve(helpSystem);
            });

            return deferred.promise();
        });

        MadCap.WebHelp.HelpSystem.LoadHelpSystemDefault = function () {
            var pathToHelpSystem = $(document.documentElement).attr('data-mc-path-to-help-system');
            var helpSystemPath = "Data/HelpSystem.xml";

            if (pathToHelpSystem)
                helpSystemPath = pathToHelpSystem + helpSystemPath;
            return MadCap.WebHelp.HelpSystem.LoadHelpSystem(helpSystemPath);
        }

        MadCap.WebHelp.HelpSystem.LoadHelpSystemPromise = function () {
            return new Promise(function (resolve) {
                MadCap.WebHelp.HelpSystem.LoadHelpSystemDefault().done(resolve);
            });
        }
    })();

    //
    //    End class HelpSystem
    //

    //
    //    Class TocFile
    //

    MadCap.WebHelp.TocFile = function (helpSystem, tocType)
    {
        // Private member variables

        var mSelf = this;
        var mHelpSystem = helpSystem;
        var mTocType = tocType;
        var mInitialized = false;
        var mRootNode = null;
        var mInitOnCompleteFuncs = new Array();
        var mTocPath = null;
        var mTocHref = null;
        var mOwnerHelpSystems = new Array();

        // Public properties

        // Constructor

        (function ()
        {
        })();

        // Public member functions

        this.Init = function (OnCompleteFunc)
        {
            if (mInitialized)
            {
                if (OnCompleteFunc != null)
                    OnCompleteFunc();

                return;
            }

            //

            if (OnCompleteFunc != null)
                mInitOnCompleteFuncs.push(OnCompleteFunc);

            //

            var fileName = null;

            if (tocType == MadCap.WebHelp.TocFile.TocType.Toc)
                fileName = "Toc.xml";
            else if (tocType == MadCap.WebHelp.TocFile.TocType.BrowseSequence)
                fileName = "BrowseSequences.xml";

            this.LoadToc(mHelpSystem.GetPath() + "Data/" + fileName, OnLoadTocComplete);

            function OnLoadTocComplete(xmlDoc)
            {
                mInitialized = true;

                mRootNode = xmlDoc.documentElement;

                InitOnComplete();
            }
        };

        this.LoadToc = function (xmlFile, OnCompleteFunc)
        {
            if (mTocType == MadCap.WebHelp.TocFile.TocType.Toc && mHelpSystem.IsWebHelpPlus)
            {
                MadCap.Utilities.Xhr.CallWebService(mHelpSystem.GetPath() + "Service/Service.asmx/GetToc", true, OnTocXmlLoaded, null);
            }
            else if (mTocType == MadCap.WebHelp.TocFile.TocType.BrowseSequence && mHelpSystem.IsWebHelpPlus)
            {
                MadCap.Utilities.Xhr.CallWebService(mHelpSystem.GetPath() + "Service/Service.asmx/GetBrowseSequences", true, OnTocXmlLoaded, null);
            }
            else
            {
                var xmlPath = (xmlFile.indexOf("/") == -1) ? mHelpSystem.GetPath() + "Data/" + xmlFile : xmlFile;

                MadCap.Utilities.Xhr.Load(xmlPath, false, OnTocXmlLoaded, null, null);
            }

            function OnTocXmlLoaded(xmlDoc, args)
            {
                if (!xmlDoc || !xmlDoc.documentElement)
                {
                    if (OnCompleteFunc != null)
                        OnCompleteFunc(xmlDoc);

                    return;
                }

                //

                if (OnCompleteFunc != null)
                    OnCompleteFunc(xmlDoc);
            }
        };

        this.LoadChunk = function (parentNode, xmlFile, OnCompleteFunc)
        {
            var xmlPath = (xmlFile.indexOf("/") == -1) ? mHelpSystem.GetPath() + "Data/" + xmlFile : xmlFile;

            MadCap.Utilities.Xhr.Load(xmlFile, true, OnTocXmlLoaded, null, null);

            function OnTocXmlLoaded(xmlDoc, args)
            {
                if (!xmlDoc || !xmlDoc.documentElement)
                {
                    if (OnCompleteFunc != null)
                        OnCompleteFunc(parentNode);

                    return;
                }

                parentNode.removeAttribute("Chunk");

                var rootNode = xmlDoc.documentElement;

                for (var i = 0, length = rootNode.childNodes.length; i < length; i++)
                {
                    var childNode = rootNode.childNodes[i];

                    if (childNode.nodeType != 1) { continue; }

                    var importedNode = null;

                    if (typeof (xmlDoc.importNode) == "function")
                        importedNode = xmlDoc.importNode(childNode, true);
                    else
                        importedNode = childNode.cloneNode(true);

                    parentNode.appendChild(importedNode);
                }

                //

                if (OnCompleteFunc != null)
                    OnCompleteFunc(parentNode);
            }
        }

        this.LoadMerge = function (parentNode, OnCompleteFunc)
        {
            var mergeHint = MadCap.Dom.GetAttributeInt(parentNode, "MergeHint", -1);

            if (mergeHint == -1)
            {
                OnCompleteFunc(parentNode, false, null, null);

                return;
            }

            parentNode.removeAttribute("MergeHint");

            var ownerHelpSystem = GetOwnerHelpSystem(parentNode);
            var subsystem = ownerHelpSystem.GetSubsystem(mergeHint);
            var replace = MadCap.Dom.GetAttributeBool(parentNode, "ReplaceMergeNode", false);

            if (!replace)
                parentNode.setAttribute("ownerHelpSystemIndex", mOwnerHelpSystems.length);

            mOwnerHelpSystems[mOwnerHelpSystems.length] = subsystem;

            var xmlPath = subsystem.GetPath() + "Data/" + (mTocType == MadCap.WebHelp.TocFile.TocType.Toc ? "Toc.xml" : "BrowseSequences.xml");
            var xmlDoc = MadCap.Utilities.Xhr.Load(xmlPath, true, OnTocXmlLoaded);

            function OnTocXmlLoaded(xmlDoc, args)
            {
                if (!xmlDoc || !xmlDoc.documentElement)
                {
                    if (OnCompleteFunc != null)
                        OnCompleteFunc(parentNode, false, null, null);

                    return;
                }

                var rootNode = xmlDoc.documentElement;
                var currNode = null;
                var isFirst = true;
                var firstNode = null;
                var lastNode = null;
                var parentXmlDoc = parentNode.ownerDocument;

                for (var i = 0, length = rootNode.childNodes.length; i < length; i++)
                {
                    var childNode = rootNode.childNodes[i];

                    if (childNode.nodeType != 1) { continue; }

                    var importedNode = null;

                    if (typeof (parentXmlDoc.importNode) == "function")
                        importedNode = parentXmlDoc.importNode(childNode, true);
                    else
                        importedNode = childNode.cloneNode(true);

                    if (replace)
                    {
                        importedNode.setAttribute("ownerHelpSystemIndex", mOwnerHelpSystems.length - 1);

                        if (isFirst)
                        {
                            isFirst = false;

                            parentNode.parentNode.replaceChild(importedNode, parentNode);

                            firstNode = importedNode;
                            lastNode = firstNode;
                        }
                        else
                        {
                            currNode.parentNode.insertBefore(importedNode, currNode.nextSibling);

                            lastNode = importedNode;
                        }

                        currNode = importedNode
                    }
                    else
                    {
                        parentNode.appendChild(importedNode);
                    }
                }

                //

                if (OnCompleteFunc != null)
                    OnCompleteFunc(parentNode, replace, firstNode, lastNode);
            }
        }

        this.AdvanceTopic = function (moveType, tocPath, href, CallBackFunc)
        {
            this.GetTocNode(tocPath, href, OnComplete);

            function OnComplete(tocNode)
            {
                if (tocNode == null)
                {
                    CallBackFunc(null);
                    return;
                }

                var moveNode = null;

                GetMoveTocTopicNode(moveType, tocNode, OnGetMoveTocNodeComplete);

                function OnGetMoveTocNodeComplete(moveNode)
                {
                    var href = null;

                    if (moveNode != null)
                    {
                        href = MadCap.Dom.GetAttribute(moveNode, "Link");

                        //if (FMCIsHtmlHelp())
                        //    href = href.substring("/Content/".length);
                        //else
                        href = href.substring("/".length);

                        var hrefUrl = new MadCap.Utilities.Url(href);

                        // CHMs don't support query strings in links
                        //if (!FMCIsHtmlHelp())
                        {
                            var prefix = null;

                            if (mTocType == MadCap.WebHelp.TocFile.TocType.Toc)
                                prefix = "TocPath";
                            else if (mTocType == MadCap.WebHelp.TocFile.TocType.BrowseSequence)
                                prefix = "BrowseSequencePath";

                            var tocPath = GetTocPath(moveNode, false);
                            var newHrefUrl = hrefUrl.ToQuery(prefix + "=" + encodeURIComponent(tocPath));

                            href = newHrefUrl.FullPath;
                        }

                        var subsystem = GetOwnerHelpSystem(moveNode);

                        href = subsystem.GetPath() + href;

                        CallBackFunc(href);
                    }
                    else
                    {
                        CallBackFunc(href);
                    }
                }
            }
        };

        this.GetRootNode = function (onCompleteFunc)
        {
            this.Init(OnInit);

            function OnInit()
            {
                onCompleteFunc(mRootNode);
            }
        };

        this.GetTocNode = function (tocPath, href, onCompleteFunc)
        {
            this.Init(OnInit);

            function OnInit()
            {
                mTocPath = tocPath;
                mTocHref = href;

                //

                var steps = (tocPath == "") ? new Array(0) : tocPath.split("|");
                var linkNodeIndex = -1;

                if (steps.length > 0)
                {
                    var lastStep = steps[steps.length - 1];

                    if (MadCap.String.StartsWith(lastStep, "_____"))
                    {
                        linkNodeIndex = parseInt(lastStep.substring("_____".length));
                        steps.splice(steps.length - 1, 1);
                    }
                }

                var tocNode = mRootNode;

                for (var i = 0, length = steps.length; i < length; i++)
                {
                    if (CheckChunk(tocNode))
                        return;

                    if (CheckMerge(tocNode))
                        return;

                    //

                    tocNode = FindBook(tocNode, decodeURIComponent(steps[i]));
                }

                if (tocNode == null)
                {
                    onCompleteFunc(null);

                    return;
                }

                if (CheckChunk(tocNode))
                {
                    return;
                }

                if (CheckMerge(tocNode))
                {
                    return;
                }

                if (linkNodeIndex >= 0)
                {
                    if (linkNodeIndex == 0)
                        foundNode = tocNode;
                    else
                        foundNode = $(tocNode).children("TocEntry")[linkNodeIndex - 1];
                }
                else
                {
                    var ownerHelpSystem = GetOwnerHelpSystem(tocNode);
                    var relHref = href.ToRelative(new MadCap.Utilities.Url(ownerHelpSystem.GetPath()));
                    var foundNode = FindLink(tocNode, relHref.FullPath.toLowerCase(), true);

                    if (!foundNode)
                        foundNode = FindLink(tocNode, relHref.PlainPath.toLowerCase(), false);
                }

                //

                mTocPath = null;
                mTocHref = null;

                //

                onCompleteFunc(foundNode);
            }

            function CheckChunk(tocNode)
            {
                var chunk = MadCap.Dom.GetAttribute(tocNode, "Chunk");

                if (chunk != null)
                {
                    mSelf.LoadChunk(tocNode, chunk,
					    function (tocNode)
					    {
					        mSelf.GetTocNode(mTocPath, mTocHref, onCompleteFunc)
					    }
				    );

                    return true;
                }

                return false;
            }

            function CheckMerge(tocNode)
            {
                var mergeHint = $(tocNode).attr("MergeHint") || -1;

                if (mergeHint >= 0)
                {
                    mSelf.LoadMerge(tocNode,
					    function (tocNode)
					    {
					        mSelf.GetTocNode(mTocPath, mTocHref, onCompleteFunc)
					    }
				    );

                    return true;
                }

                return false;
            }
        };

        this.GetEntrySequenceIndex = function (tocPath, href, onCompleteFunc)
        {
            this.GetTocNode(tocPath, href, OnCompleteGetTocNode);

            function OnCompleteGetTocNode(tocNode)
            {
                var sequenceIndex = -1;

                if (tocNode != null)
                    sequenceIndex = ComputeEntrySequenceIndex(tocNode);

                onCompleteFunc(sequenceIndex);
            }
        };

        this.GetIndexTotalForEntry = function (tocPath, href, onCompleteFunc)
        {
            this.GetTocNode(tocPath, href, OnCompleteGetTocNode);

            function OnCompleteGetTocNode(tocNode)
            {
                var total = -1;

                if (tocNode != null)
                {
                    var currNode = tocNode;

                    while (currNode.parentNode != mRootNode)
                    {
                        currNode = currNode.parentNode;
                    }

                    total = MadCap.Dom.GetAttributeInt(currNode, "DescendantCount", -1);
                }

                onCompleteFunc(total);
            }
        };

        // Private member functions

        function InitOnComplete()
        {
            for (var i = 0, length = mInitOnCompleteFuncs.length; i < length; i++)
            {
                mInitOnCompleteFuncs[i]();
            }
        }

        function FindBook(tocNode, step)
        {
            var foundNode = null;

            for (var i = 0; i < tocNode.childNodes.length; i++)
            {
                if (tocNode.childNodes[i].nodeName == "TocEntry" && MadCap.Dom.GetAttribute(tocNode.childNodes[i], "Title") == step)
                {
                    foundNode = tocNode.childNodes[i];

                    break;
                }
            }

            return foundNode;
        }

        function FindLink(node, bodyHref, exactMatch)
        {
            var foundNode = null;
            var bookHref = MadCap.Dom.GetAttribute(node, "Link");

            if (bookHref != null)
            {
                //            if (FMCIsHtmlHelp())
                //                bookHref = bookHref.substring("/Content/".length);
                //            else
                bookHref = bookHref.substring("/".length);

                bookHref = bookHref.replace(/%20/g, " ");
                bookHref = bookHref.toLowerCase();
            }

            if (bookHref == bodyHref)
            {
                foundNode = node;
            }
            else
            {
                for (var k = 0; k < node.childNodes.length; k++)
                {
                    var currNode = node.childNodes[k];

                    if (currNode.nodeType != 1) { continue; }

                    var currTopicHref = MadCap.Dom.GetAttribute(currNode, "Link");

                    if (currTopicHref == null)
                        continue;

                    //                if (FMCIsHtmlHelp())
                    //                    currTopicHref = currTopicHref.substring("/Content/".length);
                    //                else
                    currTopicHref = currTopicHref.substring("/".length);

                    currTopicHref = currTopicHref.replace(/%20/g, " ");
                    currTopicHref = currTopicHref.toLowerCase();

                    if (!exactMatch)
                    {
                        var hashPos = currTopicHref.indexOf("#");

                        if (hashPos != -1)
                            currTopicHref = currTopicHref.substring(0, hashPos);

                        var searchPos = currTopicHref.indexOf("?");

                        if (searchPos != -1)
                            currTopicHref = currTopicHref.substring(0, searchPos);
                    }

                    if (currTopicHref == bodyHref)
                    {
                        foundNode = currNode;

                        break;
                    }
                }
            }

            return foundNode;
        }

        function GetMoveTocTopicNode(moveType, tocNode, onCompleteFunc)
        {
            if (moveType == "previous")
                GetPreviousNode(tocNode);
            else if (moveType == "next")
                GetNextNode(tocNode);

            function OnCompleteGetNode(moveNode)
            {
                var moveTopicNode = null;

                if (moveNode != null)
                {
                    var link = MadCap.Dom.GetAttribute(moveNode, "Link");

                    if (link == null)
                    {
                        GetMoveTocTopicNode(moveType, moveNode, onCompleteFunc);

                        return;
                    }

                    var linkUrl = new MadCap.Utilities.Url(link);
                    var ext = linkUrl.Extension.toLowerCase();
                    var masterHS = mHelpSystem.GetMasterHelpSystem();

                    if (masterHS.UseCustomTopicFileExtension)
                    {
                        if (ext != masterHS.CustomTopicFileExtension)
                        {
                            GetMoveTocTopicNode(moveType, moveNode, onCompleteFunc);
                            return;
                        }
                    }
                    else if (ext != "htm" && ext != "html")
                    {
                        GetMoveTocTopicNode(moveType, moveNode, onCompleteFunc);
                        return;
                    }

                    moveTopicNode = moveNode;
                }

                onCompleteFunc(moveTopicNode);
            }

            function GetPreviousNode(tocNode)
            {
                function OnLoadChunk(tNode)
                {
                    var childTocNode = GetDeepestChild(tNode, "TocEntry");

                    if (childTocNode == null)
                    {
                        previousNode = tNode;
                    }
                    else
                    {
                        previousNode = childTocNode;

                        if (CheckChunk(childTocNode, OnLoadChunk))
                        {
                            return;
                        }

                        if (CheckMerge(childTocNode, OnLoadMerge))
                            return;
                    }

                    OnCompleteGetNode(previousNode);
                }

                function OnLoadMerge(tNode, replaced, firstNode, lastNode)
                {
                    if (replaced)
                        OnLoadChunk(lastNode);
                    else
                        OnLoadChunk(tNode);
                }

                var previousNode = null;

                for (var currNode = tocNode.previousSibling; currNode != null; currNode = currNode.previousSibling)
                {
                    if (currNode.nodeName == "TocEntry")
                    {
                        previousNode = currNode;
                        break;
                    }
                }

                if (previousNode != null)
                {
                    if (CheckChunk(previousNode, OnLoadChunk))
                        return;

                    if (CheckMerge(previousNode, OnLoadMerge))
                        return;

                    OnLoadChunk(previousNode);

                    return;
                }
                else
                {
                    if (tocNode.parentNode.nodeType == 1)
                        previousNode = tocNode.parentNode;
                }

                OnCompleteGetNode(previousNode);
            }

            function GetNextNode(tocNode)
            {
                function OnLoadChunk(tNode)
                {
                    var nextNode = $(tNode).children("TocEntry")[0];

                    for (var currNode = tNode; currNode != null && nextNode == null; currNode = currNode.parentNode)
                    {
                        nextNode = $(currNode).next("TocEntry")[0];
                    }

                    OnCompleteGetNode(nextNode);
                }

                function OnLoadMerge(tNode, replaced, firstNode, lastNode)
                {
                    if (replaced)
                    {
                        OnCompleteGetNode(firstNode);

                        return;
                    }

                    OnLoadChunk(tNode);
                }

                var nextNode = null;

                if (CheckChunk(tocNode, OnLoadChunk))
                    return;

                if (CheckMerge(tocNode, OnLoadMerge))
                    return;

                OnLoadChunk(tocNode);
            }

            function CheckChunk(tocNode, OnCompleteFunc)
            {
                var chunk = MadCap.Dom.GetAttribute(tocNode, "Chunk");

                if (chunk != null)
                {
                    mSelf.LoadChunk(tocNode, chunk, OnCompleteFunc);

                    return true;
                }

                return false;
            }

            function CheckMerge(tocNode, OnCompleteFunc)
            {
                var mergeHint = $(tocNode).attr("MergeHint") || -1;

                if (mergeHint >= 0)
                {
                    mSelf.LoadMerge(tocNode, OnCompleteFunc);

                    return true;
                }

                return false;
            }
        }

        function GetDeepestChild(tocNode, nodeName)
        {
            var node = $(tocNode).children(nodeName + ":last")[0];

            if (node != null)
            {
                var nodeChild = GetDeepestChild(node, nodeName);

                if (nodeChild != null)
                    return nodeChild;

                return node;
            }

            return null;
        }

        function GetOwnerHelpSystem(tocNode)
        {
            var ownerHelpSystem = null;
            var currNode = tocNode;

            while (true)
            {
                if (currNode == currNode.ownerDocument.documentElement)
                {
                    ownerHelpSystem = mHelpSystem;

                    break;
                }

                var ownerHelpSystemIndex = MadCap.Dom.GetAttributeInt(currNode, "ownerHelpSystemIndex", -1);

                if (ownerHelpSystemIndex >= 0)
                {
                    ownerHelpSystem = mOwnerHelpSystems[ownerHelpSystemIndex];

                    break;
                }

                currNode = currNode.parentNode;
            }

            return ownerHelpSystem;
        }

        function GetTocPath(tocNode)
        {
            var tocPath = "";
            var linkNodeIndex = -1;
            var childNode = $(tocNode).children("TocEntry")[0];

            if (childNode != null)
            {
                tocPath = encodeURIComponent(MadCap.Dom.GetAttribute(tocNode, "Title"));

                linkNodeIndex = 0;
            }
            else
            {
                linkNodeIndex = $(tocNode).index() + 1;
            }

            if (tocPath.length > 0)
                tocPath += "|";

            tocPath += ("_____" + linkNodeIndex);

            for (var currNode = tocNode.parentNode; currNode != null && currNode.parentNode.nodeType == 1; currNode = currNode.parentNode)
            {
                if (tocPath == null)
                    tocPath = "";

                if (tocPath.length > 0)
                    tocPath = "|" + tocPath;

                tocPath = encodeURIComponent(MadCap.Dom.GetAttribute(currNode, "Title")) + tocPath;
            }

            return tocPath;
        }

        function ComputeEntrySequenceIndex(tocNode)
        {
            if (tocNode.parentNode == tocNode.ownerDocument.documentElement)
                return 0;

            var sequenceIndex = 0;

            var link = MadCap.Dom.GetAttribute(tocNode, "Link");

            if (link != null)
                sequenceIndex++;

            for (var currNode = tocNode.previousSibling; currNode != null; currNode = currNode.previousSibling)
            {
                if (currNode.nodeType != 1) { continue; }

                var descendantCount = MadCap.Dom.GetAttributeInt(currNode, "DescendantCount", 0);

                sequenceIndex += descendantCount;

                var link = MadCap.Dom.GetAttribute(currNode, "Link");

                if (link != null)
                {
                    var linkUrl = new MadCap.Utilities.Url(link);
                    var ext = linkUrl.Extension.toLowerCase();

                    if (ext == "htm" || ext == "html")
                        sequenceIndex++;
                }
            }

            return sequenceIndex + ComputeEntrySequenceIndex(tocNode.parentNode);
        }
    };

    // Enumerations

    MadCap.WebHelp.TocFile.TocType =
    {
        "Toc": 0,
        "BrowseSequence": 1
    };

    //
    //    End class TocFile
    //

    //
    //    Class IndexEntry
    //

    MadCap.WebHelp.IndexEntry = function (indexEntry, level)
    {
        // Public properties

        var indexLinks = MadCap.Dom.GetChildNodeByTagName(indexEntry, "Links", 0).childNodes;
        var numNodes = indexLinks.length;
        var nodeCount = 0;

        this.Term = MadCap.Dom.GetAttribute(indexEntry, "Term");
        this.IndexLinks = new Array();
        this.Level = level;
        this.GeneratedReferenceType = MadCap.Dom.GetAttribute(indexEntry, "GeneratedReferenceType");

        for (var i = 0; i < numNodes; i++)
        {
            var indexLink = indexLinks[i];

            if (indexLink.nodeType != 1) { continue; }

            this.IndexLinks[nodeCount] = new MadCap.WebHelp.IndexLink(indexLink);

            nodeCount++;
        }
    };

    //
    //    End class IndexEntry
    //

    //
    //    Class IndexLink
    //

    MadCap.WebHelp.IndexLink = function (indexLink)
    {
        this.Title = MadCap.Dom.GetAttribute(indexLink, "Title");
        this.Link = MadCap.Dom.GetAttribute(indexLink, "Link");
    };

    //
    //    End class IndexLink
    //
})();