/*
    Copyright © 2008
    Rob Manson <roBman@MobileOnlineBusiness.com.au>, 
    Sean McCarthy <sean@MobileOnlineBusiness.com.au> 
    and http://SOAPjr.org

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.


    Revision history
    ----------------
    v1.1.0 - 12 Dec 2008
    - added simplified .get() and .config() SOAPjr_lite API as part of the standard plugin (see code examples below)
    - added convenience response.result() method to test HEAD.result

    v1.0.0 - 29 Nov 2008
    - initial release


    Requirements
    ------------
    - http://jqueryjs.googlecode.com/files/jquery-1.2.6.min.js (see http://jquery.com/)
    - http://www.json.org/json2.js (see http://www.json.org/)
    - http://jsonschema.googlecode.com/files/jsonschema-b2.js (see http://code.google.com/p/jsonschema/)


    Support
    -------
    For support, suggestions or general discussion please visit the #SOAPjr irc
    channel on freenode.net or visit http://SOAPjr.org 


    Implementation overview
    -----------------------
    1. Load this plugin (jquery.SOAPjr.js)
    2. Configure default settings using .config() [OPTIONAL]
    3. Make a call using .get()
    6. Setup a callback to process the results or errors


    Demonstration
    -------------
    See http://SOAPjr.org/demos.html for a working example of using this plugin.

    NOTE: The MIME-like Object with ENVELOPE/HEAD/BODY is called a MOBject.



    Lite API
    --------
    // Configure the defaults
    $.SOAPjr.config({ "url" : "http://SOAPjr.org/demos/SOAPjr.pl" });               // url must be absolute including protocol (http://...)
    $.SOAPjr.config({ "callback" : "my_callback" });                           // callback can be a function_name or an anonymous function

    // Then call a SOAPjr service
    $.SOAPjr.get({ 
        "HEAD" : { "source" : HEAD_DATA_SOURCE, "fields" : HEAD_FIELD_FILTER },     // DATA_SOURCEs can be a form_name, a var_name or a data object 
        "BODY" : { "source" : BODY_DATA_SOURCE, "fields" : BODY_FIELD_FILTER }      // FIELD_FILTERs can be an array of fields or an object map e.g. { input : output }
    });             

    // Or if all the defaults are already set using config then you can just make a simple call
    $.SOAPjr.get();


    // Otherwise you can pass them all in at once 
    $.SOAPjr.get({ "url" : url, "BODY" : { "source" : "form_name", "fields" : ["first_name"] }, "callback" : "my_callback" });

    // And don't forget to setup callbacks to handle your response
    function my_callback(resp) {
        // resp is a SOAPjr_response object
        if (resp.result()) {
            var BODY = resp.get("BODY");
            // process the results here
        } else {
            var HEAD = resp.get("HEAD");
            // dump all errors
            alert((HEAD.errors.dump());
        }
    }



    JSON Schema validation
    ----------------------
    // You can also use JSON Schema (http://json-schema.org) to validate the data structures of the BODY on both send and receive.

    // First load any schema you want to re-use
    $.SOAPjr.config({ "schema" : { "SEND_SCHEMA_KEY" : SCHEMA_GOES_HERE } });       // SCHEMA_GOES_HERE can be an absolute URL or a schema object

    // Then assign these when you make your get() call
    $.SOAPjr.get({
        "schema" : { "send" : "SEND_SCHEMA_KEY", "receive" : "RECEIVE_SCHEMA_KEY" }
    });


    //NOTE: Binary content should NOT be included within the BODY - the parameter should simply provide a URL (which may require a valid SID) for that content.
    
*/

var validJSONSchemas = {};

function setValidJSONSchemas(input) {
    for (var id in input) {
        if (typeof input[id] == "object") {
            validJSONSchemas[id] = input[id];
        }
    }
}

function getValidJSONSchemas(input) {
    for (var id in input) {
        var url         = input[id];
        if (!url) {
            throw("Not even a URL or filename was supplied!");
        }
        var xhr = $.ajax({
            "url"       : url,
            "complete"  : gotValidJSONSchema
        });
        xhr.getValidJSONSchemasConfig = {
            "id"    : id,
            "url"   : input[id]
        };
    }
}

function gotValidJSONSchema(xhr, status) {
    var json = eval("("+xhr.responseText+")");
    if (xhr.getValidJSONSchemasConfig != null) {
        if (xhr.getValidJSONSchemasConfig.id) {
            //this assumes one schema per json returned
            var id = xhr.getValidJSONSchemasConfig.id;
            var tmp = {};
            tmp[id] = json;
            setValidJSONSchemas(tmp);
        }
    } else {
        throw("No config data available for this schema");
    }
}

function gotValidJSON (xhr, status) {
    var json = eval("("+xhr.responseText+")");
    if (xhr.getValidJSONConfig != null) {
        if (xhr.getValidJSONConfig.schema.receive) {
            var valid = JSONSchema.validate(json, validJSONSchemas[xhr.getValidJSONConfig.schema.receive]);
        } else {
            var valid = JSONSchema.validate(json);
        }
        eval(xhr.getValidJSONConfig.callback+"(json,valid)");
    } else {
        throw("No data object was attached to the json response so no callback could be found");
    }
}


var SOAPjr_config_data = {};

function SOAPjr_config(input_object) {
    // url must be absolute including protocol (http://...)
    if (input_object != null && input_object.url != null) {
        if (input_object.url.match("^http://.*")) {
            SOAPjr_config_data.url = input_object.url;
        } else {
            throw("URL must be absolute : "+input_object.url);
        }
    }

    // callback can be a function_name or an anonymous function
    if (input_object != null && input_object.callback != null) {
        if (typeof input_object.callback == "string" || typeof input_object.callback == "function") {
            SOAPjr_config_data.callback = input_object.callback;
        } else {
            throw("Callback must be either a function name (string) or a function reference : "+input_object.callback);
        }
    }
    
    //$.SOAPjr.config({ "schema" : { "key" : "http://schema_url" } });
    //$.SOAPjr.config({ "schema" : { "key" : schema_object } });
    if (input_object != null && input_object.schema != null) {
        if (typeof input_object.schema == "object") {
            for (var key in input_object.schema) {
                if (input_object.schema[key].match != null && typeof input_object.schema[key].match == "function" && input_object.schema[key].match("^http://.*")) {
                    // schema is a url - NOTE: url's must be absolute
                    var schema = {};
                    schema[key] = input_object.schema[key];
                    getValidJSONSchemas(schema);
                } else if (typeof input_object.schema[key] == "object") {
                    // schema is an object
                    var schema = {};
                    schema[key] = input_object.schema[key];
                    setValidJSONSchemas(schema);
                } else {
                    throw("Invalid schema definition : "+key+" - "+input_object.schema[key]);
                }
            }
        } else {
            throw("Schema definition must be an object - { key : schema } not "+input_object.schema);
        }
    }

    //$.SOAPjr.config({ "BODY" : { "source" : "abcdef", "fields" : [] });     // source can be a form_name, a var_name or a data object
    //$.SOAPjr.config({ "HEAD" : { "source" : "abcdef", "fields" : [] });     // fields can be null, an array or an object mapping { current_id -> send_id }
    if (input_object != null && input_object.HEAD != null) {
        if (typeof input_object.HEAD == "object") {
            SOAPjr_config_data.HEAD = input_object.HEAD;
        }
    }
    if (input_object != null && input_object.BODY != null) {
        if (typeof input_object.BODY == "object") {
            SOAPjr_config_data.BODY = input_object.BODY;
        }
    }
}

function SOAPjr_lite(input_object) {
    var req = $.SOAPjr.create_request();
    
    // url
    if (input_object != null && input_object.url != null) {
        req.set({ "ENVELOPE" : { "url" : input_object.url } });
    } else if (SOAPjr_config_data.url != null) {
        req.set({ "ENVELOPE" : { "url" : SOAPjr_config_data.url } });
    } else {
        throw("URL is not set");
    }

    //get data
    //HEAD
    var new_data = {};
    if (input_object != null && input_object.HEAD != null) {
        // get data
        if (input_object.HEAD.source != null) {
            new_data.HEAD = SOAPjr_get_data(input_object.HEAD);
        } else if (typeof input_object.HEAD == "object") {
            new_data.HEAD = input_object.HEAD;
        }
    } else if (SOAPjr_config_data.HEAD != null) {
        if (SOAPjr_config_data.HEAD.source != null) {
            new_data.HEAD = SOAPjr_get_data(SOAPjr_config_data.HEAD);
        } else if (typeof SOAPjr_config_data.HEAD == "object") {
            new_data.HEAD = SOAPjr_config_data.HEAD;
        }
    }

    //BODY
    if (input_object != null && input_object.BODY != null) {
        // get data
        if (input_object.BODY.source != null) {
            new_data.BODY = SOAPjr_get_data(input_object.BODY);
        } else if (typeof input_object.BODY == "object") {
            new_data.BODY = input_object.BODY;
        }
    } else if (SOAPjr_config_data.BODY != null) {
        if (SOAPjr_config_data.BODY.source != null) {
            new_data.BODY = SOAPjr_get_data(SOAPjr_config_data.BODY);
        } else if (typeof SOAPjr_config_data.BODY == "object") {
            new_data.BODY = SOAPjr_config_data.BODY;
        }
    }
    var result = req.set(new_data);

    //schema
    var schema = {};
    if (input_object != null && input_object.schema != null) {
        if (input_object.schema.send != null) {
            schema.send = input_object.schema.send;
        }
        if (input_object.schema.receive != null) {
            schema.receive = input_object.schema.receive;
        }
    } 
    if (schema.send != null) {
        if (typeof schema.send == "object") {
            var result = JSONSchema.validate(req.get("BODY"), schema.send); 
        } else {
            var result = JSONSchema.validate(req.get("BODY"), validJSONSchemas[schema.send]); 
        }
        if (!result.valid) {
            // TODO: make this return a MOBject
            var s = "";
            for (var e in result.errors) {
                s += e+" : "+result.errors[e].property+" - "+result.errors[e].message+"\n";
            }
            throw("SEND SCHEMA VALIDATION ERROR :\n"+s);
        }
    }
    if (schema.receive != null) {
        req.set({ "OPTIONS" : { "receive_schema" : schema.receive } });
    }

    //callback
    var callback = "SOAPjr_callback";
    if (input_object != null && input_object.callback != null) {
        callback = input_object.callback;
    } else if (SOAPjr_config_data.callback != null) {
        callback = SOAPjr_config_data.callback;
    }
    req.set({ "OPTIONS" : { "callback" : callback } });

    // send it
    var result = req.send();
    return result;
}

function SOAPjr_get_data(input_object) {
    var result = {};
    if (input_object != null && input_object.source != null) {
        if (typeof input_object.source == "object") {
            var fields = input_object.source;
            if (input_object.fields != null) {
                if (typeof input_object.fields.length == "number") { // fields is an array of fields
                    fields = input_object.fields;
                    for (var prop in fields) {
                        result[fields[prop]] = input_object.source[fields[prop]]
                    }
                } else { // fields is an object map (prop is field to get and obj[prop] is name to send it as)
                    for (var prop in input_object.fields) {
                        result[input_object.fields[prop]] = input_object.source[prop]
                    }
                }
            } else {
                for (var prop in fields) {
                    result[prop] = input_object.source[prop]
                }
            }
        } else if (typeof document.forms[input_object.source] == "object") {
            var fe = document.forms[input_object.source].elements;
            var fields = fe;
            if (input_object.fields != null) {
                if (typeof input_object.fields.length == "number") { // fields is an array of fields
                    fields = input_object.fields;
                } else { // fields is an object map (prop is field to get and obj[prop] is name to send it as)
                    fields = [];
                    for (var prop in input_object.fields) {
                        fields.push(prop);
                    }
                }
            }
            for (var prop in fields) {
                // element types : 'select-one','select-multiple','select','radio','text','reset','submit','image','password','hidden','checkbox','button','file','textarea'
                if (fe[prop].type != null) {
                    var name = fields[prop].name;
                    var field= fields[prop];
                    if (input_object.fields != null && typeof input_object.fields.length != "number") {
                        name = input_object.fields[field];
                    } else if (input_object.fields != null && typeof input_object.fields.length == "number") {
                        name = field;
                    }
                    if (fe[field].type.match(/select/)) {
                        var v = new Array();
                        for (var o in fe[field].options) {
                            if (fe[field].options[o].selected) {
                                v.push(fe[field].options[o].value);
                            }
                        }
                        // TODO: may want to use other types of multiple-select packing
                        result[name] = v.join(",");
                    } else if (fe[field].type == "radio") {
                        for (var v in fe[field]) {
                            if (fe[field][v].checked) {
                                result[name] = fe[field][v].value;
                            }
                        }
                    } else if (fe[field].type == "checkbox") {
                        result[name] = fe[field].value;
                    } else if (fe[field].type == "text") {
                        result[name] = fe[field].value;
                    } else if (fe[field].type == "hidden") {
                        result[name] = fe[field].value;
                    } else if (fe[field].type == "password") {
                        result[name] = fe[field].value;
                    } else if (fe[field].type == "submit") {
                        result[name] = fe[field].value;
                    } else if (fe[field].type == "textarea") {
                        result[name] = fe[field].value;
                    }
                }
            }
        } else {
            throw("Data source is NOT an object or form");
        }
    } else {
        throw("Data source is NOT defined");
    }
    return result;
}

var MOBject_structure = {
    // A MOBject is a MIME-like Object
    "ENVELOPE"    : 1,
    "HEAD"        : 1,
    "BODY"        : 1,
    "OPTIONS"     : 1
};

function SOAPjr_base(base_input) {
    var data = {};
    this.set = function(set_input) {
        if (set_input != null) {
            var count = 0;
            for (var key in set_input) {
                // TODO: this nesting is only to one layer and should be more elegant
                if (typeof set_input[key] == "object") {
                    if (data[key] == null) {
                        data[key] = {};
                    }
                    for (var key2 in set_input[key]) {
                        data[key][key2] = set_input[key][key2];
                    }
                } else {
                    data[key] = set_input[key];
                }
            }
            return count;
        } else {
            return null;
        }
    }
    this.get = function(get_input) {
        if (get_input == null) {
            var ret = {};
            for (var prop in data) {
                ret[prop] = walk_down(data[prop]);
            }
            return ret;
        } else if (data[get_input] != null) {
            return data[get_input];
        } else {
            return {};
        }
    }
    this.set_all = function(set_input) {
        if (set_input && typeof set_input == "object") {
            data    = set_input;
            return 1;
        } else {
            return null;
        }
    }
    this.remove = function(remove_input) {
        if (remove_input != null && data[remove_input] != null) {
            delete data[remove_input];
            return 1;
        } else {
            return null;
        }
    }
    this.length = function() {
        var length = 0;
        for (var prop in data) {
            length++;
        }
        return length;
    }
    this.length2 = function() {
        var length = 0;
        for (var ds in MOBject_structure) {
            for (var prop in data[ds]) {
                length += data[ds].length();
            }
        }
        return length;
    }
    this.toString = function() {
        return this.get();
    }
    return this;
}

function SOAPjr_message(message_input) {
    var self = new SOAPjr_base(message_input);    
    for (var ds in MOBject_structure) {
        var tmp = {};
        tmp[ds] = new SOAPjr_base();
    }
    self.set(tmp);
    self.import_data = function(source_object) {
        var count = 0;
        var tmp = {};
        for (var ds in MOBject_structure) {
            tmp[ds] = {};
            if ((source_object != null) && (source_object[ds] != null)) {
                for (var key in source_object[ds]) {
                    tmp[ds][key] = source_object[ds][key];
                    count++;
                }
                this.set(tmp);
            }
        }
        return count;
    }
    self.result = function() {
        return self.get("HEAD").result;
    }
    return self;
}

function SOAPjr_errors(errors_input) {
    var self = SOAPjr_message(errors_input);
    self.dump = function() {
        var msg = "";
        for (var ds in MOBject_structure) {
            var s = this.get(ds);
            if (s != null) {
                msg += "Errors in "+ds+":\n";
                for (var e in s) {
                    if (typeof s[e] != "function" && s[e].code != null && s[e].message != null) {
                        msg += e+" : "+s[e].code+" - "+s[e].message+"\n";
                    }
                }
                msg += "\n";
            }
        }
        return msg;
    }
    return self;
}

function SOAPjr_request(request_input) {
    var self = new SOAPjr_message(request_input);
    var errors = new SOAPjr_errors();
    var count= self.import_data(request_input);
    self.output      = function() {
        var output_obj        = {"HEAD":this.get("HEAD"),"BODY":this.get("BODY")};
        // Default to application/x-www-form-urlencoded for the moment 
        // TODO: Explore different encoding methods (especially multipart/form-data e.g. using a form in an iframe with an onload listener for the response)
        var data        = "json="+JSON.stringify(output_obj);
        // Replace + with \u002B for application/x-www-form-urlencoded
        data            = data.replace(new RegExp("\\+", "g"), "\\u002B");
        return data;
    }
    self.send        = function(send_input) {
        this.import_data(send_input);
        data = this.output();
        if (this.get("ENVELOPE").url == null) {
            var error = {
                "code"        : "500",
                "message"    : "URL is not set"
            };
            errors.set({
                "ENVELOPE" : {
                    "url" : error
                }
            });
        }

        if (errors.length2()) {
            var resp    = new SOAPjr_response();
            resp.set({
                "HEAD" : {
                    "result" : 0,
                    "errors" : errors
                }
            });
            return resp;
        } else {
            var method    = "POST";
            if (this.get("ENVELOPE").method != null) {
                method = this.get("ENVELOPE").method.toUpperCase();
            }
            var xhr = $.ajax({
                //contentType:  "application/json",
                processData    : false,
                type        : method, 
                url        : this.get("ENVELOPE").url,
                data        : data,
                complete:       function (XMLHttpRequest, textStatus) {
                    if (textStatus == "success") {
                        try {
                            var data = JSON.parse(XMLHttpRequest.responseText);
                        } catch(e) {
                            // data parse failed
                        }
                        if (data) {
                            var resp = new SOAPjr_response(data);
                        } else {
                            var resp = new SOAPjr_response();
                            resp.set({
                                "HEAD" : {
                                    "result" : 0
                                }
                            });
                            var errors = new SOAPjr_errors();
                            var error = {
                                "code"        : "500",
                                "message"    : "Response did not contain valid JSON"
                            };
                            errors.set({
                                "HEAD" : {
                                    "response" : error
                                }
                            });
                            resp.set({
                                "HEAD" : {
                                    "errors" : errors
                                }
                            });
                        }
                    } else {
                        var errors     = new SOAPjr_errors();
                        var error = {
                            "code"        : XMLHttpRequest.status,
                            "message"    : "HTTP request failed"
                        };
                        errors.set({
                            "HEAD" : {
                                "http" : error
                            }
                        });

                        var resp        = new SOAPjr_response();
                        resp.set({
                            "HEAD" : {
                                "result" : 0,
                                "errors" : errors
                            }
                        });
                    }
                    var callback = "SOAPjr_callback";
                    var req = XMLHttpRequest.SOAPjr_request;
                    if (req.get("OPTIONS") != null) {
                        if (req.get("OPTIONS").callback != null) {
                            callback = req.get("OPTIONS").callback;
                        }
                        if (req.get("OPTIONS").receive_schema != null) {
                            var result = JSONSchema.validate(req.get("BODY"), validJSONSchemas[req.get("OPTIONS").receive_schema]);
                            if (!result.valid) {
                                // TODO: make this return a MOBject
                                var s = "";
                                for (var e in result.errors) {
                                    s += e+" : "+result.errors[e].property+" - "+result.errors[e].message+"\n";
                                }
                                throw("RECEIVE SCHEMA VALIDATION ERROR :\n"+s);
                            }
                        }
                    }
                    eval(callback+"(resp, XMLHttpRequest)");
                }
            });
            xhr.SOAPjr_request = this;
            var resp    = new SOAPjr_response();
            resp.set({
                "HEAD" : {
                    "result" : 1
                }
            });
            return resp;
        }
    }
    return self;
}

function SOAPjr_response(response_input) {
    var self = new SOAPjr_message(response_input);
    var errors = new SOAPjr_errors();
    var count= self.import_data(response_input);
    self.remove("ENVELOPE");
    self.remove("OPTIONS");
    return self;
}

function walk_down(obj) {
    for (var prop in obj) {
        if (typeof(obj[prop]) == "object") {
            if (obj[prop].get) {
                obj[prop] = obj[prop].get();
                for (var prop2 in obj[prop]) {
                    if (typeof(obj[prop][prop2]) == "object") {
                        obj[prop][prop2] = walk_down(obj[prop][prop2]);
                    }
                }
            }
        }
    }
    return obj;
}

function SOAPjr_callback(resp, xhr) {
    //NOTE: This is a simple example of how a callback works and will be used by default
    //resp is a SOAPjr_response object
    //xhr also includes the SOAPjr_request object
    var HEAD = resp.get("HEAD");
    if (HEAD.result) {
        var BODY = resp.get("BODY");
        alert("SOAPjr_callback BODY:\n"+JSON.stringify(BODY));
        alert(BODY["fn"]);
        // process the results here
    } else {
        // dump all errors
        alert("SOAPjr_callback errors:\n"+HEAD.errors.dump());
    }
}


// Turn this into a jQuery plugin
jQuery.SOAPjr = {
    create_request : function(input_object) { return new SOAPjr_request(input_object); },
    config : function(input_object) { return new SOAPjr_config(input_object); },
    get : function(input_object) { return new SOAPjr_lite(input_object); }
};
