/**************************************** Overview ***************************************************/
// An option tree is a tree choices, where each choice has multiple options.
// The current selected path in the option tree can be represented to the user, and interacted with.
// Thru interaction, the selected path is modified.
//
// The option tree stores some additional state, on top of the selected option for each choice: variables.
// Variables are state which is scoped and applicable for all the sub-options under the option that it is defined on.
//
// Each leaf option (which does not have sub-option) has a representation.
// This representation is a sentence, and is composed of a sequence of segments.
//
// One way to look at the object model:
// OptionTree -> Choice [with Variables] -> Options (-> more Choices and Options...)* -> Sentence -> Segments


/**************************************** TODO ***************************************************/


/**************************************** OptionTree ***************************************************/
// OptionTree is the entry point for this library.
// The OptionTree should offer all the APIs you need, for all the basic scenarios.
//
// You start by creating an OptionTree from an XML declaration
// Then you render the OptionTree as a sentence, which is a DOM node
// Then you query the current state of the OptionTree (options and variables)



// onValueSet notifies you of changes made by the user. 
// Note that in many cases, more state is likely to have changed than this one variable or option.
// void onValueSet(name, value)
var OptionTree = function(xmlDoc, onValueSet) 
{
    var rootChoice = new Choice(xmlDoc.documentElement);  
    
    // returns a property bag with all the current state of the OptionTree
    this.dumpState = function() 
    {
        var propBag = new Object();
        return rootChoice.dumpState(propBag);   
    }
    
    // programmatically update the state of the OptionTree    
    this.setValue = function(name, value) 
    {
        rootChoice.setValue(name, value);
        
        if (onValueSet != null && typeof(onValueSet) == "function") {
            onValueSet(name, value);
        }
    }
    
    this.getSentence = function() 
    {
        return rootChoice.getSentence();
    }
    
    this.getVariableType = function(name) 
    {
        return rootChoice.getVariableType(name);
    }

    // render the current active sentence to a DOM node in your page
    this.render = function(nodeID, updatedVarName) 
    {
        var self = this; // capture instance for use in callback
        var doc = window.document;
        var newNode = doc.createElement("span");
        
        // allows a segment to set a value and, optionally, re-render the whole tree
        var segmentCallback = function(variableName, variableValue, doRender) 
        {
            self.setValue(variableName, variableValue);
            if (doRender) {
                self.render(nodeID, variableName);
            }
        }

        // render each segment
        var segments = this.getSentence().getSegments();
        var focusStack = new Array();
        for (var i = 0; i < segments.length; i++) 
        {
            var focusFunc = segments[i].render(newNode, this, segmentCallback);
            focusStack.push(focusFunc);
        }
        
        // display the DOM tree that was built
        var destNode = doc.getElementById(nodeID);
        if (destNode.hasChildNodes()) {
            destNode.replaceChild(newNode, destNode.firstChild);
        } else {
            destNode.appendChild(newNode);
        }
        
        // set focus on the proper element, by giving each segment a chance
        for (var i = 0; i < focusStack.length; i++) {
            var focusFunc = focusStack[i];
            if (focusFunc != null && typeof(focusFunc) == "function") 
            {
                focusFunc(updatedVarName);
            }
        }
        
        // avoid circular memory references across JS heap and DOM
        newNode = null;
        destNode = null;
        focusStack = null;
    }
         
    // print the current state of the OptionTree into the given div (for debugging purpose)
    this.printState = function(divID) 
    {
        var variables = this.dumpState();
        var outputDiv = window.document.getElementById(divID);
        
        if (outputDiv.hasChildNodes()) {
            outputDiv.removeChild(outputDiv.firstChild);
        }
        
        var outputText = "{ ";
        var first = true;
        
        for (var variableName in variables) 
        {
            if (first) { first = false; } else { outputText += " , "; }
            outputText += variableName + " : '" + variables[variableName] + "'";
        } 
        
        outputText += " }";
        
        outputDiv.appendChild(document.createTextNode(outputText));
    }   
    
    var InitInputRenderMap = function() 
    {
        // The InputRenderMap is the main extensibility point for the TextWidget
        // Two types of input segment are supported by default (static and text).
        // You can add your own (pick a date from a calendar, location from a map, contact from a list, etc.)
        // 
        // For example, you could render and input dates and times in good english, based on the http://www.datejs.com/ JS library.
        var ret = new Object();
        ret["static"] = renderStaticInputSegment;
        ret["text"] = renderTextInputSegment;
        
        return ret;
    }
    
    this.inputRenderMap = InitInputRenderMap();
}

/**************************************** Sentence ***************************************************/
// A sentence is a list of segments. Each segment is either text, clickable text or input.
// When pushed, clickable segments change the state of the tree, by setting a specified variable to a certain value.
//
// Three kinds of segments are supported:
// -text
// -a 
// -input (extensible)

var Sentence = function(xmlNode) 
{
    var segments = createSegments(xmlNode);
    
    this.getSegments = function() 
    {
        return segments;   
    }

    function createSegments(xmlNode) 
    {
        var segments = new Array();
        for (var i = 0; i < xmlNode.childNodes.length; i++) 
        {
            var child = xmlNode.childNodes[i];

            switch (child.nodeName) {
                case "#text":
                    segments.push(new TextSegment(child));
                    break;
                case "a":
                    segments.push(new ClickableSegment(child));
                    break;
                case "input":
                    segments.push(new InputSegment(child));
                    break;
                default:
                    throw "Unsupported sentence format";
                    break;
            }
        }
        
        return segments;
    }
}


/**************************************** Segment ***************************************************/
// There are three kinds of segments supported at this point:
// -TextSegment: these are not interactive.
// -ClickableSegment: the only interaction allowed is clicking. It has a pre-defined action associated with it.
// -InputSegment: these are for the rest of the interactions, and can be extended with new types of input.
// 
// Segments should have the following interface:
// -Segment.type
// -Segment.render(containerNode, optionTree, segmentCallback) -> focusCallback

/**************************************** TextSegment ***************************************************/
var TextSegment = function(textNode) 
{
    if (textNode.nodeType != 3) 
    {
        throw "A Segment object can only be created using a text node";
    }
    
    this.type = "TextSegment";
    var textContent = textNode.nodeValue;

    this.render = function(containerNode, optionTree, segmentCallback) 
    {
        var doc = window.document;
        
        var textNode = doc.createTextNode(textContent);
        containerNode.appendChild(textNode);    

        // avoid circular memory references across JS heap and DOM
        textNode = null;
        
        return null;
    }
}


/**************************************** ClickableSegment ***************************************************/
var ClickableSegment = function(anchorNode) 
{
    this.type = "ClickableSegment";
    var textContent = anchorNode.firstChild.nodeValue;

    var variableNameVal = anchorNode.getAttribute("onclick");
    if (variableNameVal == null || variableNameVal.length == 0) 
    {
        throw "Invalid format for 'a' element in sentence node";
    }
    
    var splitVar = variableNameVal.split("=");
    if (splitVar.length != 2) 
    {
        throw "Invalid format for 'onclick' attribute on 'a' element in sentence node";    
    }

    // name of the variable to set when segment is clicked    
    var variableName = splitVar[0];
    // value that gets set on that variable when segment is clicked
    var variableValue = splitVar[1];

    this.setFocus = function(varName) 
    {
        if (varName == variableName) 
        {
            // TODO
            return true;
        }
    }

    // override this to customize rendering
    // void segmentCallback(varName, varValue)
    this.render = function(containerNode, optionTree, segmentCallback) 
    {
        var doc = window.document;
        var self = this; // keep it for callback function 
        
        var textNode = doc.createTextNode(textContent);
        var linkNode = doc.createElement("a");
        
        linkNode.setAttribute("href", "");
        linkNode.appendChild(textNode);
        linkNode.onclick = 
            function() { 
                segmentCallback(variableName, variableValue, true); 
                return false; 
            }
        containerNode.appendChild(linkNode);
        
        // avoid circular memory references across JS heap and DOM
        textNode = null;
        
        var setFocus = function(updatedVarName) {
            if (updatedVarName == variableName) {
                linkNode.focus();
            }
            
            // avoid circular memory references across JS heap and DOM
            linkNode = null; 
        }
        
        return setFocus;
    }
}

/**************************************** InputSegment ***************************************************/
var InputSegment = function(inputNode) 
{
    this.type = "InputSegment";
    // name
    this.inputName = inputNode.getAttribute("name");
    if (this.inputName == null || this.inputName.length == 0) 
    {
        throw "Input element needs a name attribute";
    }
    
    this.setFocus = function(varName) 
    {
        if (varName == this.inputName) 
        {
            // TODO
            return true;
        }
    }
    
    // override this to customize rendering
    // void segmentCallback(varName, varValue)
    this.render = function(containerNode, optionTree, segmentCallback) 
    {
        var variableType = optionTree.getVariableType(this.inputName);
        
        var renderMethod = optionTree.inputRenderMap[variableType];
        if (typeof(renderMethod) == "function") 
        {
            return renderMethod(containerNode, this, optionTree, segmentCallback);
        }
        else 
        {
            throw "This InputSegment uses an unsupported type";
        }
    }
}

/**************************************** InputRenderMap ***************************************************/
// The InputRenderMap let's you extend the types of InputSegment supported.
//
// By default, two input types are provided:
// -static: this is a non-interactive but programmable segment. It simply echos a value from the current OptionTree.
// -int: this is an integer value. The user can change the value represented by this segment.
//
// New input types can be added by implementing and registering a function of type:
//  focusCallback function(containerNode, inputSegment, optionTree, segmentCallback) 

// renders a non-clickable text element with the variable value
function renderStaticInputSegment(containerNode, inputSegment, optionTree, segmentCallback) 
{
    var doc = window.document;
    var value = optionTree.dumpState()[inputSegment.inputName];
    
    var textNode = doc.createTextNode(value);
    containerNode.appendChild(textNode);    
    
    // avoid circular memory references across JS heap and DOM
    textNode = null;
    
    return null;
}

// renders a clickable then editable element with the variable value
function renderTextInputSegment(containerNode, inputSegment, optionTree, segmentCallback)
{
    var doc = window.document;
    var value = optionTree.dumpState()[inputSegment.inputName];

    var handleBlanks = function(value) { return (value == "") ? "_" : value; }

    var textNode = doc.createTextNode(handleBlanks(value));
    var linkNode = doc.createElement("a");

    linkNode.setAttribute("href", "");
    linkNode.appendChild(textNode);
    linkNode.onclick = 
        function(e) { 
            var e = e ? e : window.event;
            var element = e.target ? e.target : e.srcElement;
            
            //alert("source element: " + element.nodeName);
            // TODO: this should be done inline, instead of using a prompt
            var newValue = prompt("Pick a new value", value);
            
            if (newValue == null) 
            { 
                // no change
                return false; 
            }
            
            textNode.nodeValue = handleBlanks(newValue); // update UI
            value = newValue; // update local context
            segmentCallback(inputSegment.inputName, newValue, false);

            return false; 
        }

    containerNode.appendChild(linkNode);

    // avoid circular memory references across JS heap and DOM
    doc = null;
    linkNode = null;
    //textNode = null;
    
    return null;
}

/**************************************** Choice ***************************************************/
/*
A choice gives an alternative between multiple options. Each option has a name.
The selected value of the choice is the name of one of the options.
A choice can also hold additional variables. 

The state of a choice is the combination of:
choiceName->choiceValue, (variableName->variableValue) for all variables, and the state of the selected option.

*/
var Choice = function(xmlNode) 
{
    var choiceName;
    var choiceValue;
    var subOptions;
    var variables;
    initChoice();
        
    this.dumpState = function(propBag) 
    {
        // record the local choice
        propBag[choiceName] = choiceValue;
        
        // record the local variables
        for (var variableName in variables) 
        {
            var variable = variables[variableName];
            variable.dumpState(propBag);
        }
        
        // recurse into sub-options
        var subOption = subOptions[choiceValue];
        return subOption.dumpState(propBag);
    }
    
    this.getVariableType = function(name) 
    {
        // check if the variable exists at this level
        var variable = variables[name];
        if (variable != null) 
        {
            return variable.getType();
        }
        
        // otherwise, try on the selected sub-option
        var subOption = subOptions[choiceValue];
        if (subOption != null) 
        {
            return subOption.getVariableType(name);
        }
    }
    
    this.setValue = function(name, value) 
    {
        if (name == choiceName) 
        {
            if (validateChoice(value)) {
                choiceValue = value;
                return;    
            }
            throw "'" + value + "' is not a valid value for this choice"; 
        } 
        
        // otherwise, check the variables
        var variable = variables[name];
        if (variable!= null) 
        {
            variable.setValue(value);
            return;
        }
        
        // otherwise, try on the appropriate sub-option
        var subOption = subOptions[choiceValue];
        if (subOption != null) 
        {
            subOption.setValue(name, value);
            return;
        }
        
        // bomb
        throw "Could not find where to set choice or variable '" + name + "'";
    }
 
    this.getSentence = function() 
    {
        var subOption = subOptions[choiceValue];
        if (subOption != null) 
        {
            return subOption.getSentence();
        }
    }
 
    function validateChoice(value) 
    {
        return (subOptions[value] != null);            
    }
    
    function initChoice() 
    {   
        if (xmlNode.nodeName != "choice") 
        {
            throw "You can only construct a Choice object with a choice xml node";
        }
     
        // initialize the choice name
        choiceName = xmlNode.getAttribute("name");
        if (choiceName == null || choiceName.length == 0) {
            throw "A choice node needs a name attribute";
        }
        
        // initialize the main choice value
        choiceValue = xmlNode.getAttribute("default");
        if (choiceValue == null) {
            // todo: we could pick a default value at random
            throw "A choice node needs to have default property";
        }
    
        subOptions = listSubOptions();
        variables = listVariables();
        
        if (!validateChoice(choiceValue)) 
        {
            throw "The default value for the choice node '" + choiceName + "' is not a valid option";
        }
    }
    
    function listSubOptions() {
        var childs = xmlNode.childNodes;
        var childOptions = new Object();
        
        for (var i = 0; i < childs.length; i++) {
            var child = childs[i];
            if (child.nodeName == "option") {
                var value = child.getAttribute("value");
                // todo: check that value attribute exists
                // if (child.getAttribute("value")
                childOptions[value] = new Option(child);
            }
        }
    
        if (childOptions.length == 0) {
            throw "Choices should have options child nodes";
        }    
        return childOptions;
    }    

    function listVariables() 
    {
        var childs = xmlNode.childNodes;
        var childVariables  = new Object();
        
        for (var i = 0; i < childs.length; i++) {
            var child = childs[i];
            if (child.nodeName == "variable") {
                var variable = new Variable(child);
                childVariables[variable.getName()] = variable;
            }
        }
        
        return childVariables;
    }
}



/**************************************** Option ***************************************************/
// Options can either have a sub-Choice or be terminal (leaf)
// Leaf options have a Sentence object which can represent them as a UI widget.
var Option = function(xmlNode) 
{
    // can either hold a sentence or a choice
    var subChoice;
    var sentence;
    initOption();
    
    function initOption() 
    {
        subChoice = getSubChoice();
        sentence = loadSentence();
        
        if (subChoice != null && sentence != null) 
        {
            throw "An option should either have a sub-choice or a sentence, but not both.";
        }
    }
    
    function loadSentence() {
        var childs = xmlNode.childNodes;
        var childSentences = new Array();
        
        for (var i = 0; i < childs.length; i++) {
            var child = childs[i];
            if (child.nodeName == "sentence") {
                childSentences.push(new Sentence(child));
            }
        }
    
        if (childSentences.length == 0) {
            return null;
        }
        if (childSentences.length == 1) {
            return childSentences[0];
        }
        
        throw "An option should have a maximum of one sentence child node";
    }
    
    function getSubChoice() {
        var childs = xmlNode.childNodes;
        var childChoices = new Array();
        
        for (var i = 0; i < childs.length; i++) {
            var child = childs[i];
            if (child.nodeName == "choice") {
                childChoices.push(new Choice(child));
            }
        }
    
        if (childChoices.length == 0) {
            return null;
        }
        if (childChoices.length == 1) {
            return childChoices[0];
        }
        
        throw "An option should have a maximum of one choice child node";
    }    
  
    
    this.dumpState = function (propBag) 
    {
        if (subChoice != null) 
        {
            return subChoice.dumpState(propBag);
        }
        
        return propBag;
    }

    this.setValue = function (name, value) 
    {
        // set the value on the right sub choice
        if (subChoice != null) 
        {
            subChoice.setValue(name, value);
            return;
        }
        
        // if no sub choice left, then this name is not valid
        throw "The value for '"+ name +"' is not valid in the current state";
    }
    
    this.getVariableType = function(name) 
    {
        if (subChoice != null) 
        {
            return subChoice.getVariableType(name);
        }
        
        // if no sub choice left, then this variable name is not valid
        throw "The variable '"+ name +"' does not exist in the current state";
    }
    
    this.getSentence = function() 
    {
        if (sentence != null) 
        {
            return sentence;
        }
        if (subChoice != null) 
        {
            return subChoice.getSentence();
        }
    }
}



/**************************************** Variable ***************************************************/
// A Choice not only knows which one of its Options is selected.
// It can also hold Variables.
// Variables can be modified by the user thru interaction within the selected Option selection.
var Variable = function(xmlNode) 
{
    var variableName;
    var variableValue;
    var variableType;
    
    initVariable(xmlNode);
    
    this.getName = function() { return variableName; }
    this.setValue = function(value) { variableValue = value; }
    
    this.getType = function() { return variableType; }
    
    this.dumpState = function(propBag) 
    {
        propBag[variableName] = variableValue;
        return propBag;
    }
    
    function initVariable() 
    {
        // initialize the variable name
        variableName = xmlNode.getAttribute("name");
        if (variableName== null || variableName.length == 0) {
            throw "A variable node needs a name attribute";
        }
        
        // initialize the variable value
        variableValue = xmlNode.getAttribute("default");
        if (variableValue == null) {
            throw "A variable node needs to have default value.";
        }
        
        // intialize the variable type
        // variableType = static renders the value as text
        // variableType = int renders the value as a link which allows editing the int
        variableType = xmlNode.getAttribute("type");
        if (variableType == null || variableType.length == 0) 
        {
            variableType = "static";
        }
    }
}


/**************************************** XMLParser ***************************************************/

var XMLParser = new Object();
XMLParser.loadString = function(str) {
    
    // TODO: fix bug with multiple whitespaces in Firefox
    if (typeof DOMParser != "undefined") {
        var parser = new DOMParser();
        return parser.parseFromString(str, "application/xml");
    }
	
	// IE
    if (typeof ActiveXObject != "undefined") {
        var d = new ActiveXObject("MSXML.DomDocument");
        d.preserveWhiteSpace = true;
        d.loadXML(str);
        return d;
    } 
    
    if (typeof XMLHttpRequest != "undefined") {
        var req = new XMLHttpRequest();
        req.open("GET", "data:application/xml" +
                     ";charset=utf-8," + encodeURIComponent(str), false);
        if (req.overrideMimeType) {
            req.overrideMimeType("application/xml");
        }
        req.send(null);
        return req.responseXML;
    }
}


XMLParser.listNodes = function(parent, name, ns) {
    // deal with the difference between IE and FF when it comes to elements with a namespace
    if (!parent.getElementsByTagNameNS && ns) {
        return parent.getElementsByTagName(ns + ':' + name);
    } else {
        return parent.getElementsByTagName(name)
    }
}

XMLParser.getText = function(node) {
    try {
        return node.firstChild.nodeValue;
    } catch (e) {
        alert(node.childNodes.length);
        return "";
    }
}

