// import Divmod
// import Divmod.Runtime

// import Nevow
// import Nevow.Athena

// import Mantissa

Mantissa.LiveForm.BadInputName = Divmod.Error.subclass("Mantissa.LiveForm.BadInputName");
/**
 * Thrown when there is an input name passed to one of L{FormWidget}'s methods
 * doesn't correspond to an input in the widget's document
 */
Mantissa.LiveForm.BadInputName.methods(
    function __init__(self, name) {
        self.name = name;
    },

    function toString(self) {
        return "no element with name " + self.name;
    });

Mantissa.LiveForm.NodeCountMismatch = Divmod.Error.subclass("Mantissa.LiveForm.NodeCountMismatch");
/**
 * Thrown where there is a mismatch between the number of values in one of the
 * input lists passed to L{FormWidget.setInputValues} and the number of actual
 * nodes in the document for that key
 */
Mantissa.LiveForm.NodeCountMismatch.methods(
    function __init__(self, nodeName, givenNodes, actualNodes) {
        self.nodeName = nodeName;
        self.givenNodes = givenNodes;
        self.actualNodes = actualNodes;
    },

    function toString(self) {
        return "you supplied " + self.givenNodes +
               " values for input " + self.nodeName +
               " but there are " + self.actualNodes +
               " nodes";
    });

Mantissa.LiveForm.FormWidget = Nevow.Athena.Widget.subclass('Mantissa.LiveForm.FormWidget');
Mantissa.LiveForm.FormWidget.DOM_DESCEND = Divmod.Runtime.Platform.DOM_DESCEND;
Mantissa.LiveForm.FormWidget.DOM_CONTINUE = Divmod.Runtime.Platform.DOM_CONTINUE;

Mantissa.LiveForm.MessageFader = Divmod.Class.subclass("Divmod.Class.MessageFader");

/**
   Fade a node in, then out again.
 */
Mantissa.LiveForm.MessageFader.methods(
    function __init__(self, node) {
        self.node = node;
        self.timer = null;
        self.inRate = 1.0;      // in opacity / second
        self.outRate = 0.5;
        self.messageDelay = 5.0; // number of seconds message is left on-screen
    },

    /*
     * Cause the referenced Node to become fully opaque.  It must currently be
     * fully transparent.  Returns a Deferred which fires when this has been
     * done.
     */
    function fadeIn(self) {
        var currentOpacity = 0.0;
        var TICKS_PER_SECOND = 30.0;
        var fadedInDeferred = Divmod.Defer.Deferred();
        var inStep = function () {
            currentOpacity += (self.inRate / TICKS_PER_SECOND)
            if (currentOpacity > 1.0) {
                self.node.style.opacity = '1.0';
                self.timer = null;
                fadedInDeferred.callback(null);
            } else {
                self.node.style.opacity = currentOpacity;
                self.timer = setTimeout(inStep, 1000 * 1.0 / TICKS_PER_SECOND);
            }
        };

        /* XXX TODO - "block" is not the right thing to do here. The wrapped
         * node might be a table cell or something.
         */
        self.node.style.display = 'block';
        inStep();
        return fadedInDeferred;
    },

    /*
     * Cause the referenced Node to become fully transparent.  It must
     * currently be fully opaque.  Returns a Deferred which fires when this
     * has been done.
     */
    function fadeOut(self) {
        var fadedOutDeferred = Divmod.Defer.Deferred();
        var currentOpacity = 0.0;
        var TICKS_PER_SECOND = 30.0;

        var outStep = function () {
            currentOpacity -= (self.outRate / TICKS_PER_SECOND);
            if (currentOpacity < 0.0) {
                self.node.style.display = 'none';
                self.timer = null;
                fadedOutDeferred.callback(null);
            } else {
                self.node.style.opacity = currentOpacity;
                self.timer = setTimeout(outStep, 1000 * 1.0 / TICKS_PER_SECOND);
            }
        };

        outStep();
        return fadedOutDeferred;
    },

    /*
     * Go through one fade-in/fade-out cycle.  Return a Deferred which fires
     * when both steps have finished.
     */
    function start(self) {
        // kick off the timer loop
        return self.fadeIn().addCallback(function() { return self.fadeOut(); });
    });


Mantissa.LiveForm.FormWidget.methods(
    function __init__(self, node, formName) {
        Mantissa.LiveForm.FormWidget.upcall(self, '__init__', node);
        self.formName = formName;
    },

    function submit(self) {
        var d = self.callRemote('invoke', self.gatherInputs());

        self.showProgressMessage();

        d.addCallback(function(result) {
            return self.submitSuccess(result);
        });
        d.addErrback(function(err) {
            return self.submitFailure(err);
        });
        return d;
    },

    /**
     * Try to find the first node with class=C{className}, and display it as a
     * block element if we find it
     */
    function _showMessage(self, className) {
        var m = self.nodeByAttribute("class", className, null);
        if(m != null) {
            m.style.display = "block";
        }
    },

    /**
     * Try to find the first node with class=C{className}, and hide it if we
     * find it
     */
    function _hideMessage(self, className) {
        var m = self.nodeByAttribute("class", className, null);
        if(m != null) {
            m.style.display = "none";
        }
    },

    function showProgressMessage(self) {
        self._showMessage("progress-message");
    },

    function hideProgressMessage(self) {
        self._hideMessage("progress-message");
    },

    function showSuccessMessage(self) {
        self._showMessage("success-message");
    },

    function hideSuccessMessage(self) {
        self._hideMessage("success-message");
    },

    function submitSuccess(self, result) {
        var resultstr;

        if (!result) {
            resultstr = 'Success!';
        } else {
            resultstr = ''+result;
        }

        Divmod.log('liveform', 'Form submitted: ' + resultstr);

        if(self.node.tagName.toLowerCase() == "form") {
            self.node.reset();
        }
        self.hideProgressMessage();

        var succm = self.nodeByAttribute('class', 'success-message', null);
        if (succm === null) {
            return Divmod.Defer.succeed(null);
        }
        succm.appendChild(document.createTextNode(resultstr));

        self.showSuccessMessage();
        var deferred = Divmod.Defer.Deferred();
        setTimeout(
            function() {
                self.hideSuccessMessage();
                deferred.callback(null);
            }, 800);
        return deferred;
    },

    // Not the best factoring, but we use this as a hook in
    // Mantissa.Validate.SignupForm - if you can factor this better please do
    // so.

    function runFader(self, fader) {
        return fader.start();
    },

    function submitFailure(self, err) {
        Divmod.log('liveform', 'Error submitting form: ' + err);
        return err;
    },

    function traverse(self, visitor) {
        return Divmod.Runtime.theRuntime.traverse(self.node, visitor);
    },

    /**
     * Helper function which returns an object with C{get} and C{set} members
     * for getting and setting the value(s) of a <select> element.  For a
     * <select> with single selection, the return value of C{get} and the
     * argument passed to C{set} will be atoms.  For multiple-select nodes,
     * they will be lists.  For multiple-select nodes, it will unselect all
     * selected <option> nodes whose values aren't in the list it is passed.
     */
    function _getAccessorsForSelectNode(self, aNode) {
        var set = function(values) {
            values = Divmod.objectify(values, values);
            for(var i = 0; i < aNode.options.length; i++) {
                aNode.options[i].selected = (aNode.options[i].value in values);
            }
        }

        if (aNode.type == 'select-one') {
            return {get: function() {
                        return aNode.value
                    },
                    set: function(value) {
                        set([value]);
                    }};
        }

        var get = function() {
            var values = [];
            for (var i = 0; i < aNode.options.length; i++) {
                if (aNode.options[i].selected) {
                    values.push(aNode.options[i].value);
                }
            }
            return values;
        }

        return {get: get, set: set};
    },

    /**
     * Gather all form input nodes below C{self.node}, without traversing into
     * child widgets.  "Form input nodes" means <input>, <textarea> and
     * <select>
     *
     * @return: mapping of node names to arrays of objects with C{get} and
     * C{set} members, where C{get} is thunk which returns the value of the
     * node, and C{set} is a one argument function which changes the value of
     * the node
     */
    function gatherInputAccessors(self) {
        var makeAttributeAccessors = function(attr, node) {
            return {get: function() { return node[attr] },
                    set: function(v) { node[attr] = v }};
        }
        var accessors = {};
        var pushAccessors = function(node, accs) {
            if (accessors[node.name] == undefined) {
                accessors[node.name] = [];
            }
            accessors[node.name].push(accs);
        }

        self.traverse(
            function(aNode) {
                if (aNode === self.node) {
                    return Mantissa.LiveForm.FormWidget.DOM_DESCEND;
                }
                if (Nevow.Athena.athenaIDFromNode(aNode) != null) {
                    // It's a widget.  We caught it in our other pass; let's
                    // not look at any of its nodes.
                    return Mantissa.LiveForm.FormWidget.DOM_CONTINUE;
                } else {
                    if (aNode.tagName) {
                        // It's an element
                        if (aNode.tagName.toLowerCase() == 'input') {
                            // It's an input

                            var attr = 'value';
                            // If it's a checkbox or radio, we care about its
                            // checked-ness.
                            if (aNode.type.toLowerCase() == 'checkbox' ||
                                aNode.type.toLowerCase() == 'radio') {
                                attr = 'checked';
                            }
                            pushAccessors(aNode, makeAttributeAccessors(attr, aNode));
                        } else if (aNode.tagName.toLowerCase() == 'textarea') {
                            pushAccessors(aNode, makeAttributeAccessors('value', aNode));
                        } else if (aNode.tagName.toLowerCase() == 'select') {
                            pushAccessors(aNode, self._getAccessorsForSelectNode(aNode));
                        } else {
                            // Examine the children, since it is some
                            // other kind of element.
                            return Mantissa.LiveForm.FormWidget.DOM_DESCEND;
                        }
                        // Inputs should not have sub-inputs; hooray a
                        // free optimization.
                        return Mantissa.LiveForm.FormWidget.DOM_CONTINUE;
                    }
                    // It's a text node... do we really need to
                    // descend?
                    return Mantissa.LiveForm.FormWidget.DOM_DESCEND;
                }
            });
        return accessors;
    },

    /**
     * Returns a mapping of input node names to arrays of input node values
     */
    function gatherInputs(self) {
        var inputs = {};

        var pushOneValue = function(name, value) {
            if (inputs[name] == undefined) {
                inputs[name] = [];
            }
            inputs[name].push(value);
        };

        // First we gather our widget children.
        for (var i = 0; i < self.childWidgets.length; i++) {
            var wdgt = self.childWidgets[i];
            if ((wdgt.formName != undefined)
                    && (wdgt.gatherInputs != undefined)) {
                var inputname = wdgt.formName;
                pushOneValue(inputname, wdgt.gatherInputs());
            }
        }

        var accessors = self.gatherInputAccessors();
        for(var nodeName in accessors) {
            for(i = 0; i < accessors[nodeName].length; i++) {
                pushOneValue(nodeName, accessors[nodeName][i].get());
            }
        }
        return inputs;
    },

    /**
     * Set the values of inputs whose names are keys in C{valueMap} to the
     * corresponding values.  Values should be lists, where the length of each
     * list is the same as the number of elements.  e.g.:
     *
     * >>> setInputValues({foo: [1, 2]})
     *
     * will set the first element with name="foo" to 1 and the second to 2.
     *
     * for multiple-select <select>s, the values should be lists of more
     * values:
     *
     * >>> setInputValues({foo: [["bar", "baz"]]})
     *
     * will select the <option>s inside the first (and only)
     * <select name="foo" /> which have value="bar" & value="baz"
     *
     * @param valueMap: mapping of input node names to values
     *
     * @raises L{Mantissa.LiveForm.BadInputName}: if a key in C{valueMap}
     * doesn't correspond to an input element of this form
     *
     * @raises L{Mantissa.LiveForm.NodeCountMismatch}: if the number of values
     * given for an input name doesn't match the number of inputs with that
     * name
     */
    function setInputValues(self, valueMap) {
        var i, accessors = self.gatherInputAccessors();
        for(var nodeName in valueMap) {
            if(accessors[nodeName] === undefined) {
                throw Mantissa.LiveForm.BadInputName(nodeName);
            }
            if(accessors[nodeName].length != valueMap[nodeName].length) {
                throw Mantissa.LiveForm.NodeCountMismatch(
                        nodeName,
                        valueMap[nodeName].length,
                        accessors[nodeName].length);
            }
            for(i = 0; i < accessors[nodeName].length; i++) {
                accessors[nodeName][i].set(valueMap[nodeName][i]);
            }
        }
    });


syntax highlighted by Code2HTML, v. 0.9.1