// (C) Copyright 2020 Hewlett-Packard Enterprise Company, L.P.
define(['hp/services/IndexTime', 'hp/services/Log', 'jquery'],
function (indexTime, log) {
"use strict";

    var IndexFilterParser = (function () {
      
        var NOT_DOUBLE_QUOTE_USER_TERM_REGEXP = /(^|[\s])NOT "(?:[^"\\]|\\.)+"/g;
        var DOUBLE_QUOTE_USER_TERM_REGEXP = /(^|[\s])"(?:[^"\\]|\\.)+"/g;
        var NOT_SINGLE_QUOTE_USER_TERM = /(^|[\s])NOT '(?:[^'\\]|\\.)+'/g;
        var SINGLE_QUOTE_USER_TERM_REGEXP = /(^|[\s])'(?:[^'\\]|\\.)+'/g;
        var NOT_PROPERTY_REGEXP = /NOT [\w\-\.]+:(("[^"]+")|('[^']+')|([\S]+))/g;
        var PROPERTY_REGEXP = /[\w\-\.]+:(("[^"]+")|('[^']+')|([\S]+))/g;
        var NOT_USER_TERM_REGEXP = /(^|[\s])NOT ([\S]+)/g;
        
        // we special case MAC addresses since they contain colons
        var MAC_REGEXP = /(([0-9,a-f,A-F]){2}(:([0-9,a-f,A-F]){2}){3,11})/g;

        /**
         * arg is either a location or an IndexFilter to start from
         */
        function IndexFilterParser(arg) {

            // trim white space and remove surrounding quotes from all entries in the array
            function trimEntries(terms, removeNot) {
                if (terms) {
                    $.each(terms, function(index,term) {
                        terms[index] = $.trim(terms[index]);
                        if (removeNot) {
                            terms[index] = terms[index].replace(/^NOT /, '');
                        }
                        terms[index] = terms[index].replace(/^"|"$/g, '');
                        terms[index] = terms[index].replace(/:"/g, ':');
                        terms[index] = terms[index].replace(/^'|'$/g, '');
                        terms[index] = terms[index].replace(/:'/g, ':');
                        terms[index] = $.trim(terms[index]);
                    });
                }
            }

            // extract all the terms from query. Expect query to be a non null string.
            // 'property terms' are terms separated by ":"; they are returned in "properties"
            // e.g. roles:"system administrator"
            //      firmware:"need update"
            //      status:ok
            // 'user terms' are terms that are not property; they are returned in "userTerms"
            // e.g. "John Doe"
            //      fc_switch
            //      rack
            // Property terms may have a preceding 'NOT', these will be returned in separate
            // 'notProperties' object
            function extractQueryTerms(query) {
                var result = {
                    properties: {},
                    notProperties: {},
                    userTerms: [],
                    notUserTerms: []
                };
                
                // Javascript does not support look-behind. Otherwise this will work:
                //     (?<!:)("(?:[^"\\]|\\.)+"). 
                // As a workaround, separates all side by side quoted terms
                // "a bc\"""e f" -> ["a bc\"", "e f"]
                query = query.replace('\\"""', '\\" ""').replace("\\'''", "\\' ''");
                // "abc""def" 'xyz''tuv' -> ["abc", "def", 'xyz', 'tuv']
                query = query.replace('""', '" "').replace("''", "' '");
                
                // extract all NOT double quoted terms, e.g. NOT "rack 3" NOT "(created GE '2013-03-01T00:00:00.000Z')".
                // allow escaping quotes inside, e.g. "rack\"s 3"
                var terms = query.match(NOT_DOUBLE_QUOTE_USER_TERM_REGEXP);
                if (terms) {
                    trimEntries(terms, true);
                    result.notUserTerms = result.notUserTerms.concat(terms);
                    query = query.replace(NOT_DOUBLE_QUOTE_USER_TERM_REGEXP, '');
                }

                // extract all double quoted terms, e.g. "rack 3" "(created GE '2013-03-01T00:00:00.000Z')".
                // allow escaping quotes inside, e.g. "rack\"s 3"
                terms = query.match(DOUBLE_QUOTE_USER_TERM_REGEXP);
                if (terms) {
                    trimEntries(terms);
                    result.userTerms = result.userTerms.concat(terms);
                    query = query.replace(DOUBLE_QUOTE_USER_TERM_REGEXP, '');
                }
                
                // extract all NOT single quoted terms, e.g. NOT 'rack 3'.
                terms = query.match(NOT_SINGLE_QUOTE_USER_TERM);
                if (terms) {
                    trimEntries(terms, true);
                    result.notUserTerms = result.notUserTerms.concat(terms);
                    query = query.replace(NOT_SINGLE_QUOTE_USER_TERM, '');
                }

                // extract all single quoted terms, e.g. 'rack 3'.
                terms = query.match(SINGLE_QUOTE_USER_TERM_REGEXP);
                if (terms) {
                    trimEntries(terms);
                    result.userTerms = result.userTerms.concat(terms);
                    query = query.replace(SINGLE_QUOTE_USER_TERM_REGEXP, '');
                }
                
                // extract all MAC address terms, e.g. '1a:2b:3c:4d:5e:6f'.
                terms = query.match(MAC_REGEXP);
                if (terms) {
                    trimEntries(terms);
                    result.userTerms = result.userTerms.concat(terms);
                    query = query.replace(MAC_REGEXP, '');
                }
                
                // extract NOT properties e.g. NOT roles:"System Administrator" NOT category:switch
                // doesn't handle escaped quotes in value
                var properties = query.match(NOT_PROPERTY_REGEXP);
                if (properties) {
                    trimEntries(properties);
                    result.notProperties = properties;
                    query = query.replace(NOT_PROPERTY_REGEXP, '');
                }

                // extract properties e.g. roles:"System Administrator" category:switch
                // doesn't handle escaped quotes in value
                properties = query.match(PROPERTY_REGEXP);
                if (properties) {
                    trimEntries(properties);
                    result.properties = properties;
                    query = query.replace(PROPERTY_REGEXP, '');
                }
                
                // extract all NOT terms, e.g. NOT 3.
                terms = query.match(NOT_USER_TERM_REGEXP);
                if (terms) {
                    trimEntries(terms, true);
                    result.notUserTerms = result.notUserTerms.concat(terms);
                    query = query.replace(NOT_USER_TERM_REGEXP, '');
                }

                // extract the rest of the non-quoted terms
                if ($.trim(query).length > 0) {
                    terms = query.match(/[\S]+/g);
                    if (terms) {
                        result.userTerms = result.userTerms.concat(terms);
                    }
                }

                return result; 
            }

            // parse user search terms from the query string
            this.parseQueryTerms = function (data) {
                if (data.userQuery) {
                    var queryTerms = extractQueryTerms(data.userQuery);
                    data.terms = queryTerms.userTerms;
                    data.notTerms = queryTerms.notUserTerms;
                }
            };

            // formalize any parameters in the query
            this.parseQuery = function (data) {
                if (!data.userQuery) {
                    return;
                }
                
                var queryTerms = extractQueryTerms(data.userQuery);
                
                if (queryTerms.properties) {
                    $.each(queryTerms.properties, function (index, param) {
                        // example param: 'name:value'
                        var fields = param.split(':');
                        var name = fields[0];
                        var value = fields.slice(1).join(':');
                        if (! data.properties) {
                            data.properties = {};
                        }
                        if (data.properties[name]) {
                            // already have, make an array and add
                            if ('array' !== $.type(data.properties[name])) {
                                data.properties[name] = [data.properties[name]];
                            }
                            data.properties[name].push(value);
                        } else {
                            data.properties[name] = value;
                        }
                    });
                }
                
                if (queryTerms.notProperties) {
                    $.each(queryTerms.notProperties, function (index, param) {
                        // example param: 'NOT name:value'
                        // strip 'NOT '
                        var fields = param.split('NOT ')[1].split(':');
                        var name = fields[0];
                        var value = fields.slice(1).join(':');
                        if (! data.notProperties) {
                            data.notProperties = {};
                        }
                        if (data.notProperties[name]) {
                            // already have, make an array and add
                            if ('array' !== $.type(data.notProperties[name])) {
                                data.notProperties[name] = [data.notProperties[name]];
                            }
                            data.notProperties[name].push(value);
                        } else {
                            data.notProperties[name] = value;
                        }
                    });
                }

                data.terms = queryTerms.userTerms;
                data.notTerms = queryTerms.notUserTerms;
            };

            // add double quotes to val if it contains white space, colon, or an embedded quote
            // unless it is a MAC address
            function addQuote(val) {
                return (/\s|:|"|'/g.test(val) && ! MAC_REGEXP.test(val)) ? '"' + val + '"' : val;
            }

            // generate query string from formalized parameters
            this.generateQuery = function (data) {
                var terms = [];

                if (data.terms) {
                    $.each(data.terms, function (idx, value) {
                        terms.push(addQuote(value));
                    });
                }
                
                if (data.notTerms) {
                    $.each(data.notTerms, function (idx, value) {
                        terms.push('NOT ' + addQuote(value));
                    });
                }

                if (data.properties) {
                    $.each(data.properties, function (name, value) {
                        if ($.isArray(value)) {
                            $.each(value, function (index, val) {
                                terms.push(name + ':' + addQuote(val));
                            });
                        } else {
                            terms.push(name + ':' + addQuote(value));
                        }
                    });
                }
                
                if (data.notProperties) {
                    $.each(data.notProperties, function (name, value) {
                        if ($.isArray(value)) {
                            $.each(value, function (index, val) {
                                terms.push('NOT ' + name + ':' + addQuote(val));
                            });
                        } else {
                            terms.push('NOT ' + name + ':' + addQuote(value));
                        }
                    });
                }

                if (terms.length > 0) {
                    data.userQuery = terms.join(' ');
                    // 2000 limit based on:
                    // http://stackoverflow.com/questions/417142/what-is-the-maximum-length-of-a-url
                    // Even though the overall query would be even longer, this is to catch
                    // problems with the just the query portion.
                    if (data.userQuery.length > 2000) {
                        throw ("query too long");
                    }
                } else {
                    delete data.userQuery;
                }
            }
            
            function propertyToRESTQuery(name, value, restContext) {
                if ('array' === $.type(value)) {
                    var values = [];
                    $.each(value, function (index, val) {
                        values.push(name + ':\'' + val + '\'');
                    });
                    if (restContext.version >= 2) {
                        restContext.queries.push('(' + values.join(' OR ') + ')');
                    } else if (values.length === 1) {
                        restContext.filters.push(name + ':' + values[0]);
                    } else {
                        log.warn('Cannot use OR filters with version 1, for: ', name, value);
                    }
                } else {
                    if (restContext.version >= 2) {
                        restContext.queries.push(name + ':\'' + value + '\'');
                    } else {
                        restContext.filters.push(name + ':' + value);
                    }
                }
            }
            
            function notPropertyToRESTQuery(name, value, restContext) {
                if ('array' === $.type(value)) {
                    var values = [];
                    $.each(value, function (index, val) {
                        values.push('NOT ' + name + ':\'' + val + '\'');
                    });
                    if (restContext.version >= 2) {
                        restContext.queries.push('(' + values.join(' OR ') + ')');
                    } else if (values.length === 1) {
                        restContext.filters.push(values[0]);
                    } else {
                        log.warn('Cannot use OR filters with version 1, for: ', name, value);
                    }
                } else {
                    if (restContext.version >= 2) {
                        restContext.queries.push('NOT ' + name + ':\'' +
                            value + '\'');
                    } else {
                        log.warn('Cannot use NOT filter with version 1, for: ', name, value);
                    }
                }
            }
            
            function addProperty(name, value, restContext) {
                // Don't bother if value is '*'.
                // We use this to override hidden filters.
                if ('*' !== value) {
                    if ('created' === name || 'modified' === name) {
                        indexTime.toRESTFilters(name, value, restContext.filters);
                    } else if ('category' !== name) {
                        propertyToRESTQuery(name, value, restContext);
                    }
                }
            }
            
            function addNotProperty(name, value, restContext) {
                if ('created' === name || 'modified' === name) {
                    log.warn('Cannot use NOT filter with dates, for: ', name, value);
                } else if ('category' !== name) {
                    notPropertyToRESTQuery(name, value, restContext);
                }
            }
            
            function addProperties(properties, restContext) {
                $.each(properties, function (name, value) {
                    addProperty(name, value, restContext);
                });
            }
            
            function addNotProperties(properties, restContext) {
                $.each(properties, function (name, value) {
                    addNotProperty(name, value, restContext);
                });
            }
            
            function addHiddenProperties(hiddenProperties, restContext) {
                var data = restContext.data;
                $.each(hiddenProperties, function (name, value) {
                    // allow overriding hidden filters
                    if ((! data.properties || ! data.properties.hasOwnProperty(name)) &&
                        (! data.notProperties || ! data.notProperties.hasOwnProperty(name))) {
                        addProperty(name, value, restContext);
                    }
                });
            }
            
            function addHiddenNotProperties(hiddenProperties, restContext) {
                var data = restContext.data;
                $.each(hiddenProperties, function (name, value) {
                    // allow overriding hidden filters
                    if ((! data.properties || ! data.properties.hasOwnProperty(name)) &&
                        (! data.notProperties || ! data.notProperties.hasOwnProperty(name))) {
                        addNotProperty(name, value, restContext);
                    }
                });
            }
            
            function addTerms(terms, restContext) {
                if (terms.length > 0) {
                    var tval=[];
                    // if the value contains white space, add quotes
                    $.each(terms, function (index, val) {
                        tval.push((/\s/g.test(val)) ? "'" + val + "'" : val);
                    });
                    restContext.params.push('userQuery="' + encodeURIComponent(tval.join(' ')) + '"');
                }
            }
            
            function addNotTerms(terms, restContext) {
                if (terms.length > 0) {
                    var tval=[];
                    // if the value contains white space, add double quotes
                    $.each(terms, function (index, val) {
                        tval.push('NOT ' + ((/\s/g.test(val)) ? "'" + val + "'" : val));
                    });
                    log.warn('Cannot use NOT filter with bare terms yet, for: ', tval.join(' '));
                }
            }
            
            function addFixedParam(name, value, restContext) {
                if ('array' === $.type(value)) {
                    $.each(value, function (index, val) {
                        restContext.params.push(name + '=' + encodeURIComponent(val));
                    });
                } else {
                    if ((restContext.version > 1) && restContext.data.hasOwnProperty('associationName')) {
                        if (name === 'associationName') {
                            name = 'name';
                        } else if (name === 'startObjUri') {
                            name = 'parentUri';
                        } else if (name === 'endObjUri') {
                            name = 'childUri';
                        }
                    }
                    restContext.params.push(name + '=' + encodeURIComponent(value));
                }
            }
            
            // Convert to IndexService REST query string parameters
            this.toRESTParameters = function (data, version) {
                var restContext = {params: [], filters: [], queries: [], version: version,
                    data: data};
                $.each(data, function (name, value) {
                    if ('properties' === name) {
                        addProperties(value, restContext);
                    } else if ('notProperties' === name) {
                        addNotProperties(value, restContext);
                    } else if ('hiddenProperties' === name) {
                        addHiddenProperties(value, restContext);
                    } else if ('hiddenNotProperties' === name) {
                        addHiddenNotProperties(value, restContext);
                    } else if ('terms' === name) {
                        addTerms(value, restContext);
                    } else if ('notTerms' === name) {
                        addNotTerms(value, restContext);
                    } else if ('userQuery' !== name) {
                        addFixedParam(name, value, restContext);
                    }
                });
                
                for (var i=0; i<restContext.filters.length; i++) {
                    restContext.params.push('filter=' + restContext.filters[i]);
                }
                if (restContext.queries.length > 0) {
                    restContext.params.push('query="' +
                        encodeURIComponent(restContext.queries.join(' AND ')) + '"');
                }
                return restContext.params;
            };

            this.toString = function (data, defaults, PROP_MAP) {
                var string = '';
                
                // add formal properties
                $.each(PROP_MAP, function (queryParam, dataProp) {
                    if (data.hasOwnProperty(dataProp)) {
                        if ($.isArray(data[dataProp])) {
                            string += queryParam + "[]=" + data[dataProp].join(',') + ' ';
                        } else {
                            string += queryParam + '=' + data[dataProp] + ' ';
                        }
                    }
                });
                
                // add variable properties
                if (data.properties) {
                    $.each(data.properties, function (name, value) {
                        if ($.isArray(value)) {
                            value = $.map(value, function (v) { return addQuote(v);});
                            string += 'fp_' + name + "[]=" + value.join(',') + ' ';
                        } else {
                            string += 'fp_' + name + '=' + addQuote(value) + ' ';
                        }
                    });
                }
                
                if (data.notProperties) {
                    $.each(data.notProperties, function (name, value) {
                        if ($.isArray(value)) {
                            value = $.map(value, function (v) { return addQuote(v);});
                            string += 'fpn_' + name + "[]=" + value.join(',') + ' ';
                        } else {
                            string += 'fpn_' + name + '=' + addQuote(value) + ' ';
                        }
                    });
                }
                
                // add hidden properties
                if (data.hiddenProperties) {
                    $.each(data.hiddenProperties, function (name, value) {
                        if ($.isArray(value)) {
                            value = $.map(value, function (v) { return addQuote(v);});
                            string += 'fph_' + name + "[]=" + value.join(',') + ' ';
                        } else {
                            string += 'fph_' + name + '=' + addQuote(value) + ' ';
                        }
                    });
                }
                
                if (data.hiddenNotProperties) {
                    $.each(data.hiddenNotProperties, function (name, value) {
                        if ($.isArray(value)) {
                            value = $.map(value, function (v) { return addQuote(v);});
                            string += 'fpnh_' + name + "[]=" + value.join(',') + ' ';
                        } else {
                            string += 'fpnh_' + name + '=' + addQuote(value) + ' ';
                        }
                    });
                }
                
                // terms
                
                // defaults
                $.each(defaults, function (n, v) {
                    string += ' D:' + n + '=' + v;
                });
                return string;
            };
        }

        return new IndexFilterParser();
    }());

    return IndexFilterParser;
});
