/*
    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
    ----------------
    - 29 Nov 2008: initial release


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


    Implementation overview
    -----------------------
    1. Create request message
    2. Configure request message
    3. Send request message
    4. Receive response message
    5. Validate result
    6. Process result 


    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.

    Code usage examples
    -------------------
    1. Create request message
    //This returns a js object with get() and set() accessors, etc. wrapped around ENVELOPE, HEAD, BODY and OPTIONS data structures
    //ENVELOPE, HEAD and BODY relate to the SOAPjr request message itself
    //OPTIONS relates to the SOAPjr client and how it handles the response

    var SOAPjr_request_object        = $.SOAPjr.new(MOBject);


    2. Configure request message
    //This returns a boolean result when you set values
    //The object you supply is iterated over with it's contents copied onto the SOAPjr_request_object's internal data structures
    var result          = SOAPjr_request_object.set({ "HEAD" : { "key1":"value", "key2","value" }, "BODY" : { "key3":"value" } });

    //This returns the value of the requested key from the specified SOAPjr_request_object data structure
    var value           = SOAPjr_request_object.get("ENVELOPE")["key1"];
    var value           = SOAPjr_request_object.get("HEAD")["key1"];
    var value           = SOAPjr_request_object.get("BODY")["key1"];
    var value           = SOAPjr_request_object.get("OPTIONS")["key1"];

    //This returns the specified SOAPjr_request_object data structure as a JS object
    var object          = SOAPjr_request_object.get().ENVELOPE;
    var object          = SOAPjr_request_object.get().HEAD;
    var object          = SOAPjr_request_object.get().BODY;
    var object          = SOAPjr_request_object.get().OPTIONS;


    3. Send request message
    //Values supplied will override any values already configured within the SOAPjr_request_object's internal data structures

    var result          = SOAPjr_request_object.send({
                                "ENVELOPE"          : {
                                    "url"               : "http://SOAPjr.org/demo/simple_test",
                                    "content_type"      : "form_data",
                                    "method"            : "post"
                                },
                                "OPTIONS"           : {
                                    "callback"          : "my_callback"
                                },
                                "HEAD"              : { }, // All of these data structures are options at either
                                "BODY"              : { }  // the constructor, iteratively/individually or upon send
                            });


    4. Receive response message
    //This callback function is defined by the creator of the original SOAPjr_response_object via the OPTIONS setters
    //If it is not configured via OPTIONS then the default SOAPjr_callback() function will be used
    //So you can either overload it via configuration or at the function level
    //If the request fails at the HTTP level a valid SOAPjr_response_object with HEAD.result = 0 and a valid HEAD.errors will be created

    function my_callback(SOAPjr_response_object, SOAPjr_request_object, xhr) {
        ...
    }


    5. Validate result
    //Check if the response succeeded
    var results         = SOAPjr_response_object.result();

    //Retrieve the error information
    var errors_object   = SOAPjr_response_object.get("HEAD").errors; //Will be an empty JSON object if result() was 1

    //This errors object should contain all the errors that occured for this request
    var error_code      = errors_object.get("HEAD").[param_name].code
    var error_message   = errors_object.get("HEAD").[param_name].message
    var error_code      = errors_object.get("BODY").[param_name].code
    var error_message   = errors_object.get("BODY").[param_name].message

    //NOTE: Any errors when the ENVELOPE or OPTIONS params are set should be thrown at that time
    //e.g. the setter will return a 0 and the SOAPjr_request_object.get("HEAD").errors[data_structure][param_name] object will be populated with code and message

    //The list of error codes should be defined in the "dmd" (data model definition) specified in the SOAPjr_request_object HEAD
    //If none are explicitly defined then OPTIONS will be checked and if "version" has been set the default "dmd" for that "version" will be used (see http://SOAPjr.org/dmds)
    //Otherwise the default "dmd" for the default "version" defined in this plugin will be used


    6. Process result 
    var payload         = SOAPjr_response_object.BODY.get()

    var first_name      = payload.first_name   //or
    var first_name      = SOAPjr_response_object.BODY.get("first_name")

    //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.
    //NOTE: HEAD.sid is treated like any other parameter and is not cached or stored by the plugin...authentication implementation is left up to the developer.
*/

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) {
                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 null;
        }
    }
    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;
    }
    return self;
}

function SOAPjr_errors(errors_input) {
    var self = SOAPjr_message(errors_input);
    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.send        = function(send_input) {
        this.import_data(send_input);
        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");

        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 (XMLHttpRequest.status == 200) {
                        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 && req.get("OPTIONS").callback != null) {
                        callback = req.get("OPTIONS").callback;
                    }
                    eval(callback+"(resp, req, 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;
}



// This is the default callback - you should create your own callback 
// and configure that in your SOAPjr_request object using OPTIONS.callback
function SOAPjr_callback(SOAPjr_response, SOAPjr_request, xhr) {
    // SOAPjr_response is a MOBject_response object populated with the data from the server
    // SOAPjr_request is the MOBject_request object that was used to .send() the request
    // xhr is the XMLHttpRequest object that was used
    
    /*
        Common CALLBACK design pattern:
        -------------------------------
        - get the response object's HEAD (for convenience)
        - if the result was successful process the BODY
        - else get the errors object and process any errors you find

        NOTE:   Now all transport, application and validation errors can be handled in a common way.
        Also, multiple errors can easily be returned and common or domain specific error codes can also be standardised.
    */

    // Get the HEAD of the response object
    var resp_head = SOAPjr_response.get("HEAD");

    // If the request was successful then get the BODY and process it
    if (resp_head.result == 1) {
        var body = SOAPjr_response.get("BODY");
        // do whatever you need to here with the data you successfully got back

    // Else see what errors we had
    } else {
        // Get the HEAD errors from the errors object
        var eh = resp_head.errors.get("HEAD");
        // Let's see if the request HEAD generated any errors
        for (prop in eh) {
            var code = eh[prop].code;
            var msg  = eh[prop].message;
            // do whatever you need to here to handle the errors
        }

        // Get the BODY errors from the errors object
        var eb = resp_head.errors.get("BODY");
        // Let's see if the request BODY generated any errors
        for (prop in eb) {
            var code = eb[prop].code;
            var msg  = eb[prop].message;
            // do whatever you need to here to handle the errors
        }

        // You could repeat this for ENVELOPE and OPTIONS too
    }
    alert(
        "Here's the request object you created:\n"+
        JSON.stringify(SOAPjr_request.get())
    );
    alert(
        "Here's the response object you got back:\n"+
        JSON.stringify(SOAPjr_response.get())
    );
}

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


