'use strict';
module.exports = (function () {
    //defiant.js relies on navigator and DOMParser
    global.navigator = {};
    global.DOMParser = require('xmldom').DOMParser;
    //TODO find a way not to use defiant for node module version (lib/rest-json) but keep it for browser version (dist/rest-json).
    // Currently defiant is used only to prepare xml from json
    require('defiant.js');
    var xpath = require('xpath');
    //Defian relies on XNode.selectNodes method and it is not defined with xmldom
    Defiant.node.selectNodes = function(XNode, XPath) {
        return xpath.select(XPath, XNode);
    };
//TODO JSON.stringify does not handle order, need to guarantee this works
// Properties of non-array objects are not guaranteed to be stringified in any particular order. Do not rely on ordering of properties within the same object within the stringification.

/**
 * @param data {Object} JSON data object
 * @constructor
 */
function RestJson(data){
    this.data = data;
}

/**
 * GET (retrieve) data from JSON data object
 * @param definitions {Object}
 *  {
 *      xpath: {string} //required
 *      type: {string}  //type of data, expected at xpath location 'array', 'object', or anything else.
 *                        Default value depends on data found with xpath expression:
 *                             - if only one element found - return that element
 *                             - if multiple elements found - return 'array'
 *  }
 * @returns {*}
 */
RestJson.prototype.get = function(definitions){
    var result = JSON.search(this.data, definitions.xpath);
    if(typeof definitions.type === 'undefined'){
        definitions.type = result.length>1 ? 'array' : 'object';
    }
    if(definitions.type !== 'array'){
        result = result.length>0?result[0]:null;
    }
    return result;
};

/**
 * POST (create) data at JSON data object
 * @param definitions {Object}
 *  {
 *      xpath: {string} //required
 *      type: {string}  //type of data, expected at xpath location 'array', 'object' (default), or anything else
 *      operation: {string} //how to concatenate new data with existing:
 *                              'merge' (default) - just add to existing data (see 'unique' for data uniqueness comparison)
 *                              'replace' - data found with xpath expression will be replaced with new data
 *                              'as-is' (works for types that are not 'array') - the data will just be returned as provided
 *      unique: {string|Array} //fields used for uniqueness comparison; see RestJson.checkUnique()
 *
 *  }
 * @param data {*} data to be POSTed
 * @returns {*}
 */
RestJson.prototype.post = function(definitions, data){
    if(!data) {
        return null;
    }

    var resultObjects = this.cutResultObjects(definitions),
        result = resultObjects.result,
        parent = resultObjects.parent,
        property = resultObjects.property;

    //Replace (PUT) operation should return null if object to be updated is not found
    if((definitions.operation === 'replace' || definitions.operation === 'merge-replace') && (result === null || result.length === 0)){
        return null;
    }
    if(typeof definitions.operation === 'undefined'){
        definitions.operation = 'merge';
    }
    switch(definitions.operation){
        case 'replace':
            result = data;
            break;
        case 'merge-replace':
            if((data instanceof Object) && (result[0] instanceof Object)){
                result = RestJson.merge(result[0], data);
            }
            else {
                result = data;
            }
            break;
        case 'as-is':
            if(definitions.type !== 'array'){
                result = result.length>0?result[0]:null;
            }
            break;
        case 'merge':
            if(definitions.type === 'array'){
                if(data instanceof Array) {
                    result = data.concat(result);
                }
                else {
                    //add element to the beginning of the array to allow RestJson.checkUnique() to keep this new element and remove other
                    result.unshift(data);
                }
            }
            else {
                result = result.length>0?result[0]:null;
                if(data instanceof Object){
                    if(result instanceof Object){
                        result = RestJson.merge(result, data);
                    }
                    else {
                        result = data;
                    }
                }
                else {
                    result = data;
                }
            }
            break;
    }

    if(!parent[property]){
        parent[property] = result;
    }
    else if(parent[property] instanceof Array){
        if(result instanceof Array) {
            parent[property] = result.concat(parent[property]);
        }
        else {
            //add element to the beginning of the array to allow RestJson.checkUnique() to keep this new element and remove other
            parent[property].unshift(result);
        }
    }
    else {
        parent[property] = RestJson.merge(parent[property], result);
    }

    parent[property] = RestJson.checkUnique(parent[property], definitions.unique);
    return result;
};

/**
 * PUT (update) data at JSON data object
 * @param definitions {Object}
 *  {
 *      xpath: {string} //required
 *      type: {string}  //type of data, expected at xpath location 'array', 'object' (default), or anything else
 *      operation: {string} //forced to 'replace'
 *      unique: {string|Array} //fields used for uniqueness comparison; see RestJson.checkUnique()
 *  }
 * @param data {*} data to be PUT
 * @returns {*}
 */
RestJson.prototype.put = function(definitions, data){
    definitions.operation = 'merge-replace';
    return this.post(definitions, data);
};

/**
 * DELETE data from JSON data object
 * @param definitions {Object}
 *  {
 *      xpath: {string} //required
 *      type: {string}  //type of data, expected at xpath location 'array', 'object' (default), or anything else
 *  }
 * @returns {*}
 */
RestJson.prototype.remove = function(definitions){
    var resultObjects = this.cutResultObjects(definitions),
        result = resultObjects.result;
    //Remove (DELETE) operation should return null if object to be deleted is not found
    if(result === null || result.length === 0){
        return null;
    }
    if(definitions.type !== 'array'){
        result = result[0];
    }
    return result;
};

/**
 * Cut (remove and return) data from JSON data object
 * @param definitions {Object}
 * @returns {Object} contains three properties:
 *                   - result {Array} elements that were removed
 *                   - parent {Object} parent object from which the elements were removed
 *                   - property {string} property name under which the removed elements were located in parent object
 */
RestJson.prototype.cutResultObjects = function(definitions){

    if(typeof definitions.type === 'undefined'){
        definitions.type = 'object';
    }

    var result = JSON.search(this.data, definitions.xpath),
        parent = null,
        property = null,
        re = /^(\/\/\w+.*)(\/(\w+)(\[[ \(\)\w<>\=\'\',]{1,}\])?).*$/,
        parentXpath, match;

    //clean all undefined results
    for(var i=0; i<result.length; i++){
        if(typeof result[i] === 'undefined') {
            result.splice(i,1);
        }
    }

    match = definitions.xpath.match(re);
    if(match){
        parentXpath = definitions.xpath.replace(re, '$1');
        property = definitions.xpath.replace(re, '$3');
        parent = JSON.search(this.data, parentXpath);
        parent = parent[0];
        if(result.length>0){
            if(match.length === 4){
                parent[property] = (definitions.type==='array')?[]:null;
            }
            else{
                parent[property] = RestJson.removeElementsFromData(parent[property], result);
            }
        }
    }
    else {
        re = /^\/\/(\w+)(\[[ \(\)\w<>\=\'\',]{1,}\])?.*$/;
        match = definitions.xpath.match(re);
        if(match){
            property = definitions.xpath.replace(re, '$1');
            parent = this.data;
            if(result.length>0){
                if(match.length === 2){
                    parent[property] = [];
                }
                else{
                    parent[property] = RestJson.removeElementsFromData(parent[property], result);
                }
            }
        }
    }
    if(!parent) {
        throw new Error('Could not detect parent object');
    }

    return {
        result: result,
        parent: parent,
        property: property
    };
};

/**
 * Merge two objects
 * @param object1 {Object}
 * @param object2 {Object}
 */
RestJson.merge = function(object1, object2){
    object1 = (object1 instanceof Object) ? object1 : {};
    object2 = (object2 instanceof Object) ? object2 : {};
    return JSON.parse((JSON.stringify(object1) + JSON.stringify(object2)).replace(/\}\{/g,',').replace(/\,\}$/, '}').replace(/^\{\,/, '{'));
};

/**
 * Remove elements from data provided. If data is an Object and equals element to be removed then null the data.
 * @param data {Array|Object}
 * @param toBeRemoved {Array} elements to be removed from 'data'
 * @returns {Array|Object}
 */
RestJson.removeElementsFromData = function(data, toBeRemoved){
    if(data instanceof Array){
        data = data.filter(function(el){
            for(var i=0; i<toBeRemoved.length; i++){
                if(JSON.stringify(toBeRemoved[i]) === JSON.stringify(el)) {
                    return false;
                }
            }
            return true;
        });
    }
    else if(JSON.stringify(toBeRemoved[0]) === JSON.stringify(data)) {
        data = null;
    }
    return data;
};

/**
 * Keep only unique records in data.
 * Note that order of elements in dara array matters: first found element will be kept, other similar elements will be removed
 *
 * @param data {Array}
 * @param unique {string|Array} fields used for uniqueness comparison
 * @returns {Array}
 * @example
 *  var data = [
 *      {id: 111, category: 'book', title: 'book 111'},
 *      {id: 111, category: 'bike', title: 'bike 111'}
 *     ];
 *  //consider 'id' to be unique
 *  RestJson.checkUnique(data, 'id'); returns:
 *     [
 *      id: 111, category: 'book', title: 'book 111'}
 *     ]
 *  //consider combination of 'id' and 'category' to be unique
 *  RestJson.checkUnique(data, ['id', 'category']); returns:
 *     [
 *      {id: 111, category: 'book', title: 'book 111'},
 *      {id: 111, category: 'bike', title: 'bike 111'}
 *     ];
 */
RestJson.checkUnique = function(data, unique){
    if(typeof unique === 'undefined' || (!(data instanceof Array))){
        return data;
    }
    if(typeof unique === 'string'){
        unique = [unique];
    }
    var found = [], foundEl, i;
    data = data.filter(function(el){
        if(el instanceof Object){
            foundEl = '';
            for(i=0; i<unique.length; i++){
                foundEl += (''+JSON.stringify(el[unique[i]])).toLowerCase();
            }
            for(i=0; i<found.length; i++){
                if(foundEl === found[i]) {
                    return false;
                }
            }
            found.push(foundEl);
        }
        return true;
    });
    return data;
};

/**
 * Prepare xpath string depending on parameters.
 * @param definitions {Object}
 *  {
 *      xpath: {string} //required, e.g. "//Product"
 *      xpaths: {Array} array of objects with conditions:
 *                 {
 *                    "if":["category"], //Condition, apply if all parameters of the array (e.g. "category") are defined
 *                    "xpath": "[contains(category,\"{category}\")]" //xpath to be added
 *                 }
 *      type: {string}  //type of data, expected at xpath location 'array', 'object' (default), or anything else
 *      operation: {string} //forced to 'replace'
 *      unique: {string|Array} //fields used for uniqueness comparison; see RestJson.checkUnique()
 *  }
 * @param params {Object} key-value parameters, e.g. {"category":"books"}
 * @example
 *   var definitions = {
 *        xpath: "//Product",
 *        xpaths: [ //order matters for negative conditions
 *            {
 *               "if":["category"],
 *               //xpath to be added if "category" parameter is provided
 *               "xpath": "[contains(category,\"{category}\")]"
 *             },
 *             {
 *               //xpath to be added if "category" and "flag" parameter is provided
 *               "if":["category","flag"],
 *               "xpath": "[flag=\"{flag}\"][contains(category,\"{category}\")]"
 *             },
 *             {
 *               //NEGATIVE conditions are possible using "!" in the beginning
 *               //xpath to be added if "group" is provided and "category" parameter is missing
 *               "if":["group","!category"],
 *               "xpath": "[group=\"{group}\"]"
 *             }
 *        ]
 *      }
 *   RestJson.prepareXpath(definitions, {category: "books"});
 *   // returns definitions: {xpath:"//Product[contains(category,\"{category}\")]", ...}
 *
 *   RestJson.prepareXpath(definitions, {flag:"x",category: "books"});
 *   // returns definitions: {xpath:"//Product[flag=\"{flag}\"][contains(category,\"{category}\")]", ...}
 *
 *   RestJson.prepareXpath(definitions, {category: "books", group: "main"});
 *   // returns definitions: {xpath:"//Product[contains(category,\"{category}\")]", ...} ; does not add group
 *
 *   RestJson.prepareXpath(definitions, {group: "main"});
 *   // returns definitions: {xpath:"//Product[group=\"{group}\"]", ...} ; add group when category missing
 *
 * @returns {Object} definitions
 */
RestJson.prepareXpath = function(definitions, params){
    if(definitions.xpaths instanceof Array){
        var xpath = (typeof definitions.xpath !== 'undefined') ? definitions.xpath : '', conditions, c;
        for (var i=0; i< definitions.xpaths.length; i++){
            if(typeof definitions.xpaths[i].xpath === 'undefined') {
                continue;
            }
            conditions = definitions.xpaths[i].if;
            if(conditions){
                if(typeof conditions === 'string') {
                    conditions = [conditions];
                }
                var apply = true;
                var condition;
                for (c=0; c<conditions.length; c++){
                    condition = conditions[c];
                    if (condition.charAt(0) === '!') {
                        condition = condition.substring(1);
                        if(params[condition]) {
                            apply = false;
                            break;
                        }
                    }
                    else {
                        if(!params[condition]) {
                            apply = false;
                            break;
                        }
                    }
                }
                if(!apply) {
                    continue;
                }
            }
            xpath += definitions.xpaths[i].xpath;
        }
        definitions.xpath = xpath;
    }
    return definitions;
};

/**
 * Replace tokens (e.g. "{category}" ) in xpath with parameters' values
 * @param definitions {Object}
 * @param params {Object}
 * @example
 *   RestJson.applyXpathParams(
 *           {xpath: "//Product[contains(category,'{category}')]"},
 *           {category: "books"}
 *        ); //returns definitions: {xpath:"//Product[contains(category,'books')]", ...}
 *
 * @returns {Object} definitions
 */
RestJson.applyXpathParams = function(definitions, params){
    for (var key in params) {
        if(params.hasOwnProperty(key)){
            definitions.xpath = definitions.xpath.replace('{'+key+'}', params[key]);
        }
    }
    return definitions.xpath;
};
return RestJson;
})();