/**!
 * FlexSearch - Superfast lightweight full text search engine
 * ----------------------------------------------------------
 * https://github.com/nextapps-de/flexsearch
 * @version: 0.1.2
 * @author: Thomas Wilkerling
 * @license: Apache 2.0 Licence
 */

window['FlexSearch'] = (function(queue, cache){

    "use strict";

    /**
     * @enum {number|string|boolean}
     * @private
     * @const
     * @final
     */

    var defaults = {

        // bitsize of assigned IDs (data type)
        type: 'integer',

        // type of information
        result: 'id',

        // type of information
        mode: 'forward',

        // match when starts from beginning
        strict: false,

        // handle multiple words as separated queries
        multi: false,

        // boolean model of multiple words
        boolean: false,

        // matching in strict order (multiple words)
        ordered: false,

        // use flexible cache (scales automatically)
        cache: false,

        // use flexible cache (scales automatically)
        async: false,

        // use on of built-in functions
        // or pass custom encoding algorithm
        encode: false
    };

    /**
     * Phonetic Encoders
     * @enum {Function|boolean}
     * @private
     * @const
     * @final
     */

    var encoder = {

        // case insensitive search

        'icase': function(value){

            return value.toLowerCase();
        },

        // simple phonetic normalization (latin)

        'simple': (function(){

            var regex_strip = regex('[^a-z0-9 ]'),
                regex_a = regex('[àáâãäå]'),
                regex_e = regex('[èéêë]'),
                regex_i = regex('[ìíîï]'),
                regex_o = regex('[òóôõöő]'),
                regex_u = regex('[ùúûüű]'),
                regex_y = regex('[ýŷÿ]'),
                regex_n = regex('ñ'),
                regex_c = regex('ç'),
                regex_s = regex('ß');

            return function(str) {

                return (

                    str.toLowerCase()
                       .replace(regex_a, 'a')
                       .replace(regex_e, 'e')
                       .replace(regex_i, 'i')
                       .replace(regex_o, 'o')
                       .replace(regex_u, 'u')
                       .replace(regex_y, 'y')
                       .replace(regex_n, 'n')
                       .replace(regex_c, 'c')
                       .replace(regex_s, 's')
                       .replace(regex_strip, '')
                );
            };
        }()),

        // advanced phonetic transformation (latin)

        'advanced': (function(){

            var regex_space = regex(' '),
                regex_ae = regex('ae'),
                regex_oe = regex('oe'),
                regex_ue = regex('ue'),
                regex_ie = regex('ie'),
                regex_sz = regex('sz'),
                regex_zs = regex('zs'),
                regex_ck = regex('ck'),
                regex_cc = regex('cc'),
                regex_sh = regex('sh'),
                regex_th = regex('th'),
                regex_dt = regex('dt'),
                regex_ph = regex('ph'),
                regex_ou = regex('ou'),
                regex_uo = regex('uo');

            return function(string, _skip_post_processing){

                if(!string){

                    return string;
                }

                // perform simple encoding
                string = encoder['simple'](string);

                // normalize special pairs
                if(string.length > 2){

                    string = string.replace(regex_ae, 'a')
                                   .replace(regex_oe, 'o')
                                   .replace(regex_ue, 'u')
                                   .replace(regex_ie, 'i')
                                   .replace(regex_sz, 's')
                                   .replace(regex_zs, 's')
                                   .replace(regex_sh, 's')
                                   .replace(regex_ck, 'k')
                                   .replace(regex_cc, 'k')
                                   .replace(regex_th, 't')
                                   .replace(regex_dt, 't')
                                   .replace(regex_ph, 'f')
                                   .replace(regex_ou, 'o')
                                   .replace(regex_uo, 'u');
                }

                if(!_skip_post_processing){

                    // remove white spaces
                    string = string.replace(regex_space, '');

                    // delete all repeating chars
                    if(string.length > 1){

                        string = collapseRepeatingChars(string);
                    }
                }

                return string;
            };

        })(),

        'extra': (function(){

            var soundex_b = regex('p'),
                soundex_c = regex('[sz]'),
                soundex_k = regex('[gq]'),
                soundex_i = regex('[jy]'),
                soundex_m = regex('n'),
                soundex_t = regex('d'),
                soundex_f = regex('[vw]');

            var regex_vowel = regex('[aeiouy]');

            return function(str) {

                if(!str){

                    return str;
                }

                // perform advanced encoding
                str = encoder['advanced'](str, /* skip post processing? */ true);

                if(str.length > 1){

                    str = str.split(" ");

                    for(var i = 0; i < str.length; i++){

                        var current = str[i];

                        if(current.length > 1){

                            // remove all vowels after 2nd char
                            str[i] = current[0] + current.substring(1)
                                                         .replace(soundex_b, 'b')
                                                         .replace(soundex_c, 'c')
                                                         .replace(soundex_k, 'k')
                                                         .replace(soundex_i, 'i')
                                                         .replace(soundex_m, 'm')
                                                         .replace(soundex_t, 't')
                                                         .replace(soundex_f, 'f')
                                                         .replace(regex_vowel, '');
                        }
                    }

                    str = str.join("");
                    str = collapseRepeatingChars(str);
                }

                return str;
            };
        })()

        // TODO: provide some common encoder plugins
        // soundex
        // cologne
        // metaphone
        // caverphone
        // levinshtein
        // hamming
        // matchrating
        // ngram
    };

    /**
     * @type {Array<Array>}
     * @private
     */

    var custom_matchers = [];

    /**
     * @type {number}
     * @private
     */

    var id_counter = 0;

    /**
     * @type {string}
     * @private
     */

    var last_empty_query = "";

    /**
     * @type {Object<string, number>}
     * @private
     */

    var scores = {};

    /**
     * @param {Object<string, number|string|boolean>=} options
     * @constructor
     * @implements _search_struct
     * @private
     */

    function FlexSearch(options){

        // generate UID

        this.id = id_counter++;

        // initialize index

        this.init(options || defaults);
    }

    /**
     * @param {Object<string, number|string|boolean>=} options
     * @returns _search_struct
     */

    FlexSearch.create =
        FlexSearch.new = function(options){

            return new this(options);
        };

    /**
     * @param {Object<string, string>} matcher
     */

    FlexSearch.addMatcher = function(matcher){

        for(var key in matcher){

            if(matcher.hasOwnProperty(key)){

                custom_matchers[custom_matchers.length] = [

                    regex(key), matcher[key]
                ];
            }
        }
    };

    /**
     * @param {Object<string, number|string|boolean>=} options
     */

    FlexSearch.prototype.init = function(options){

        // apply options

        if(options){

            this.type = (

                options['type'] ||
                this.type ||
                defaults.type
            );

            this.result = (

                options['result'] ||
                this.result ||
                defaults.result
            );

            this.mode = (

                options['mode'] ||
                this.mode ||
                defaults.mode
            );

            this.strict = (

                options['strict'] ||
                this.strict ||
                defaults.strict
            );

            this.ordered = (

                options['ordered'] ||
                this.ordered ||
                defaults.ordered
            );

            this.multi = (

                options['multi'] ||
                this.multi ||
                defaults.multi
            );

            this.boolean = (

                options['boolean'] === 'or' ||
                this.boolean ||
                defaults.boolean
            );

            this.cache = (

                options['cache'] ||
                this.cache ||
                defaults.cache
            );

            this.async = (

                options['async'] ||
                this.async ||
                defaults.async
            );

            this.encode = (

                (options['encode'] && encoder[options['encode']]) ||
                this.encode ||
                (defaults.encode && encoder[defaults.encode])
            );
        }

        // initialize index

        this._map = {};
        this._ids = {};
        this._status = true;
        this._cache = this.cache ?

            /** @type _cache_struct */
            (new cache(30 * 1000, 50, true))
            :
            false;
    };

    /**
     * @param {!number|string} id
     * @param {!string} content
     */

    FlexSearch.prototype.set =
        FlexSearch.prototype.add = function(id, content){

            if(content){

                // check if index ID already exist

                if(this._ids[id]){

                    this.update(id, content);
                }
                else{

                    var dupes = {};
                    var words = content.split(' ');

                    for(var i = 0; i < words.length; i++){

                        var value = words[i];

                        if(value){

                            if(this.encode) {

                                value = apply_encoding(value, this.encode);
                            }

                            if(value){

                                switch(this.mode){

                                    case 'inverse':

                                        var tmp = "";

                                        for(var a = value.length - 1; a >= 1; a--){

                                            tmp = value[a] + tmp;

                                            if(!dupes[tmp]){

                                                dupes[tmp] = "1";
                                                this._map[tmp] || (this._map[tmp] = []);
                                                this._map[tmp].push(id);
                                            }
                                        }

                                    case 'forward':

                                        var tmp = "";

                                        for(var a = 0; a < value.length; a++){

                                            tmp += value[a];

                                            if(!dupes[tmp]){

                                                dupes[tmp] = "1";
                                                this._map[tmp] || (this._map[tmp] = []);
                                                this._map[tmp].push(id);
                                            }
                                        }

                                        break;

                                    case 'full':

                                        var tmp = "";

                                        for(var x = 0; x < value.length; x++){

                                            for(var y = value.length; y > x; y--){

                                                tmp = value.substring(x, y);

                                                if(!dupes[tmp]){

                                                    dupes[tmp] = "1";
                                                    this._map[tmp] || (this._map[tmp] = []);
                                                    this._map[tmp].push(id);
                                                }
                                            }
                                        }

                                        break;
                                }
                            }
                        }
                    }

                    // update status

                    this._ids[id] = "1";
                    this._status = false;
                }
            }

            if(this.async){

                return Promise.resolve(this);
            }
        };

    FlexSearch.prototype.update = function(id, content){

        var marker = this._ids[id];

        if(marker){

            this.remove(id);

            if(content){

                this.add(id, content);
            }
        }

        if(this.async){

            return Promise.resolve(this);
        }
    };

    FlexSearch.prototype.delete =
        FlexSearch.prototype.remove = function(id){

            var keys = Object.keys(this._map);
            var tmp;

            for(var i = 0; i < keys.length; i++){

                tmp = this._map[keys[i]];

                for(var a = 0; a < tmp.length; a++){

                    if(tmp[a] === id){

                        tmp.splice(a, 1);

                        break;
                    }
                }

                if(!tmp.length){

                    delete this._map[keys[i]];
                }
            }

            delete this._ids[id];

            if(this.async){

                return Promise.resolve(this);
            }
        };

    /**
     * @param {string} query
     * @param {number|Function} limit
     * @param {Function=} callback
     * @returns {Array}
     */

    FlexSearch.prototype.search =
        FlexSearch.prototype.query =
            FlexSearch.prototype.find = function(query, limit, callback){

                if(typeof limit === 'function'){

                    callback = limit;
                    limit = 0;
                }

                limit || (limit = 1000);

                if(callback){

                    var self = this;

                    queue(function(){

                        callback(self.search(query, limit));
                        self = null;

                    }, 'search-' + this.id);

                    return null;
                }

                var _query = query;

                // invalidate cache

                if(!this._status){

                    if(this.cache){

                        this._cache.reset();
                    }

                    this._status = true;
                }

                // validate cache

                else if(this.cache){

                    var cache = this._cache.get(query);

                    if(cache){

                        return cache;
                    }
                }

                // validate last input

                else if(last_empty_query && (query.indexOf(last_empty_query) !== -1)){

                    return [];
                }

                // remove trailing spaces

                var spaces = 0;

                while(query[spaces] === " "){

                    spaces++;
                }

                if(spaces){

                    _query = query.substring(spaces);
                }

                if(!query){

                    return [];
                }

                // convert words into single components

                var words = (

                    this.multi ?

                        _query.split(' ')
                        :
                        [_query]
                );

                // sort words by length

                // if((length > 1) && !this.boolean){
                //
                //     words.sort(sort_by_length);
                // }

                // var encoded = new Array(length);

                // encode query

                // if(this.encode){
                //
                //     words[0] = apply_encoding(
                //
                //         words[0], this.encode, false
                //     );
                // }

                // perform search

                var result = [];

                var map;
                var found = true;
                var value;
                var check = [];
                var check_words = {};

                for(var a = 0; a < words.length; a++){

                    value = words[a];

                    if(check_words[value]){

                        continue;
                    }

                    if(value){

                        value = this.encode(value);
                    }

                    if(value){

                        if(map = this._map[value]){

                            check[check.length] = map;
                        }
                        else{

                            found = false;
                            break;
                        }

                        check_words[words[a]] = "1";
                    }
                }

                if(found){

                    var length = check.length;
                    var count = 0;
                    var tmp = check[count];

                    while(tmp.length && (count < length)){

                        var item = check[count++];

                        tmp = intersect(tmp, item, count === length ? limit : 0);
                    }

                    if(limit && (tmp.length > limit)){

                        tmp = tmp.slice(0, limit);
                    }

                    result = tmp;
                }

                if(result.length){

                    last_empty_query = "";
                }
                else{

                    last_empty_query || (last_empty_query = query);
                }

                // store result to cache

                if(this.cache) {

                    this._cache.set(query, result);
                }

                return result;
            };

    FlexSearch.prototype.info = function(){

        var keys = Object.keys(this._map);
        var bytes = 0;

        for(var i = 0; i < keys.length; i++){

            bytes += this._map[keys[i]].length * 4 + keys[i].length * 2;
        }

        return {

            'id': this.id,
            'bytes': bytes,
            'status': this._status,
            'matchers': custom_matchers.length
        };
    };

    FlexSearch.prototype.reset =
        FlexSearch.prototype.clear = function(){

            // destroy index

            this.destroy();

            // initialize index

            this.init();
        };

    FlexSearch.prototype.destroy = function(){

        // cleanup cache

        if(this.cache){

            this._cache.reset();
        }

        // release references

        this._map = null;
        this._cache = null;
    };

    return FlexSearch;

    // ---------------------------------------------------------
    // Helpers

    function regex(str){

        return new RegExp(str, 'g');
    }

    function apply_custom_matchers(str){

        for(var i = 0; i < custom_matchers.length; i++){

            var matcher = custom_matchers[i];

            str = str.replace(matcher[0], matcher[1]);
        }

        return str;
    }

    function apply_encoding(content, encode){

        content = encode(content);

        if(custom_matchers.length){

            content = apply_custom_matchers(content);
        }

        return content;
    }

    // function sort_by_length(a, b){
    //
    //     var diff = a.length - b.length;
    //
    //     return (
    //
    //         diff < 0 ?
    //
    //             1
    //         :(
    //             diff > 0 ?
    //
    //                 -1
    //             :
    //                 0
    //         )
    //     );
    // }

    /**
     * @param {string} string
     * @returns {string}
     */

    function collapseRepeatingChars(string){

        var collapsed_string = '',
            char_prev = '',
            char_next = '';

        for(var i = 0; i < string.length; i++){

            var char = string[i];

            if(char !== char_prev){

                if(i > 0 && char === 'h'){

                    var char_prev_is_vowel = (

                        char_prev === 'a' ||
                        char_prev === 'e' ||
                        char_prev === 'i' ||
                        char_prev === 'o' ||
                        char_prev === 'u' ||
                        char_prev === 'y'
                    );

                    var char_next_is_vowel = (

                        char_next === 'a' ||
                        char_next === 'e' ||
                        char_next === 'i' ||
                        char_next === 'o' ||
                        char_next === 'u' ||
                        char_next === 'y'
                    );

                    if(char_prev_is_vowel && char_next_is_vowel){

                        collapsed_string += char;
                    }
                }
                else{

                    collapsed_string += char;
                }
            }

            char_next = (

                (i === string.length - 1) ?

                    ''
                    :
                    string[i + 1]
            );

            char_prev = char;
        }

        return collapsed_string;
    }

    function intersect(a, b, limit){

        var length_a = a.length,
            length_b = b.length;

        var tmp;

        if(length_b < length_a){

            tmp = b;
            b = a;
            a = tmp;
        }

        var result = [],
            count = 0;

        for(var y = 0; y < length_a; y++){

            var current_a = a[y];

            for(var x = 0; x < length_b; x++){

                if(b[x] === current_a){

                    result[count++] = current_a;

                    if(limit && (count === limit)){

                        return result;
                    }

                    break;
                }
            }
        }

        return result;
    }
})(
    // Xone Async Handler Fallback

    (function(){

        var stack = {};

        return function(fn, id){

            var timer = stack[id];

            if(timer) {

                clearTimeout(timer);
            }

            return (

                stack[id] = setTimeout(fn)
            );
        };
    })(),

    // Xone Flexi-Cache Handler Fallback

    (function(){

        function Cache(){

            this.cache = {};
        }

        Cache.prototype.reset = function(){

            this.cache = {};
        };

        Cache.prototype.set = function(id, value){

            this.cache[id] = value;
        };

        Cache.prototype.get = function(id){

            return this.cache[id];
        };

        return Cache;
    })()
);

/**
 * Search Interface
 * @interface
 * @this {_search_struct}
 * @const
 */

function _search_struct(options) {}
/** @type {Function} */
_search_struct.register;
/** @type {Function} */
_search_struct.prototype.add;
/** @type {Function} */
_search_struct.prototype.update;
/** @type {Function} */
_search_struct.prototype.remove;
/** @type {Function} */
_search_struct.prototype.clear;
/** @type {Function} */
_search_struct.prototype.destroy;
/** @type {Function} */
_search_struct.prototype.search;

/**
 * Cache Interface
 * @interface
 * @this {_cache_struct}
 * @param {number=} expiration
 * @const
 */

function _cache_struct(expiration) {}
/** @type {function(string, *, boolean=)} */
_cache_struct.prototype.set;
/** @type {function(string, boolean=):*} */
_cache_struct.prototype.get;
/** @type {function(string):*} */
_cache_struct.prototype.remove;
/** @type {function()} */
_cache_struct.prototype.clear;
