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

;(function(){

    provide('BulkSearch', (function factory(queue, cache){

        "use strict";

        /**
         * @struct
         * @private
         * @const
         * @final
         */

        var defaults = {

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

            // type of information
            result: 'id',

            // type-save separator
            separator: '~', // '^'

            // 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 on of built-in functions
            // or pass custom encoding algorithm
            encode: false,

            // default chunk size
            size: 4000, // 600 -> 2500 -> 4000 -> 5000 -> 10000

            // depth of register
            depth: 0
        };

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

        var custom_matchers = [];

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

        var chunk_size = 10000;

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

        var id_counter = 0;

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

        var scores = {};

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

        function BulkSearch(options){

            // generate UID

            /** @export */
            this.id = id_counter++;

            // initialize index

            this.init(options || defaults);

            // define functional properties

            Object.defineProperty(this, 'index', {

                /**
                 * @this {BulkSearch}
                 */

                get: function(){

                    return this._marker;
                }
            });

            Object.defineProperty(this, 'length', {

                /**
                 * @this {BulkSearch}
                 */

                get: function(){

                    return Object.keys(this._marker).length;
                }
            });
        }

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

        BulkSearch.new = function(options){

            return new this(options);
        };

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

        BulkSearch.create = function(options){

            return BulkSearch.new(options);
        };

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

        BulkSearch.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
         * @this {BulkSearch}
         * @export
         */

        BulkSearch.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.separator = (

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

                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.depth = (

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

                /** @export */
                this.encoder = (

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

                chunk_size = /** @type {number} */ (options['size'] || defaults.size);
            }

            // initialize index

            this._index = [create_typed_array(this.type, chunk_size)];
            this._marker = {};
            this._fragment = {};
            this._register = {};
            this._bulk = [""];
            this._chunk = 0;
            this._status = true;
            this._fragmented = 0;
            this._cache = this.cache ?

                (new cache(30 * 1000, 50, true))
            :
                false;
        };

        /**
         * @param {!string} value
         * @returns {string}
         * @this {BulkSearch}
         * @export
         */

        BulkSearch.prototype.encode = function(value){

            if(this.encoder){

                value = this.encoder(value);
            }

            if(custom_matchers.length){

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

                    var matcher = custom_matchers[i];

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

            return value;
        };

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

        BulkSearch.prototype.add = function(id, content){

            if((typeof content === 'string') && content && (id || (id === 0))){

                // check if index ID already exist

                if(this._marker[id]){

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

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

                    content = "";

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

                        /** @type {string} */
                        var value = words[i];

                        if(value){

                            if(this.encode){

                                value = this.encode(value);
                            }

                            if(value){

                                if(!dupes[value]){

                                    dupes[value] = "1";

                                    content += value;
                                }
                            }
                        }
                    }

                    if(!content){

                        return;
                    }

                    var bulk = this._bulk;
                    var chunk_index = this._chunk;
                    var fragment = this._fragment[content.length];
                    var index = 0;
                    var marker = 0;

                    // check fragments

                    // TODO: split fragments into reusable parts
                    // if(!fragment && this._fragmented){
                    //
                    //     var fragment_keys = Object.keys(this._fragment);
                    //
                    //     for(var i = 0; i < fragment_keys.length; i++){
                    //
                    //         if(content.length < fragment_keys[i]){
                    //
                    //             fragment = this._fragment[fragment_keys[i]];
                    //             break;
                    //         }
                    //     }
                    // }

                    if(fragment && fragment.length){

                        marker = this._marker[id] = fragment.pop();
                        index = /** @type  {number} */ (marker[1]);

                        var chunk = bulk[marker[0]];

                        bulk[marker[0]] = (

                            chunk.substring(0, index) +
                            content +
                            chunk.substring(marker[2])
                        );

                        this._fragmented -= content.length;
                    }
                    else{

                        index = bulk[chunk_index].length;

                        // check chunk size limit

                        if((index + content.length) > chunk_size){

                            if(index < (chunk_size / 2)){

                                chunk_size *= 2;
                            }

                            this._chunk = ++chunk_index;

                            index = 0;
                            bulk[chunk_index] = "";
                            this._index[chunk_index] = create_typed_array(this.type, chunk_size);
                        }

                        // 32 bytes per marker --> Uint16Array = 8 bytes per marker
                        marker = this._marker[id] = [

                            chunk_index,
                            index,
                            0,
                            0
                        ];

                        bulk[chunk_index] += content + this.separator;
                    }

                    if(this._index[chunk_index].constructor === Array){

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

                            this._index[chunk_index][index++] = id;
                        }
                    }
                    else{

                        console.log(this._index[chunk_index].fill);

                        this._index[chunk_index].fill(id, index, index + content.length + 1);

                        index += content.length;
                    }

                    // assign end marker

                    marker[2] = index;

                    // push marker to the register

                    if(this.depth){

                        for(var i = this.depth; i > 1; i--){

                            var key = content.substring(0, i);

                            this._register[key] || (this._register[key] = []);
                            this._register[key].push(marker);
                        }
                    }

                    // update status

                    this._status = false;
                }
            }
        };

        /**
         * @this {BulkSearch}
         * @export
         */

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

            if((typeof content === 'string') && (id || (id === 0))){

                var marker = this._marker[id];

                if(marker){

                    if(content && this.encode){

                        content = this.encode(content);
                    }

                    var min = marker[1];
                    var max = marker[2];
                    var old_length = max - min;
                    var overlap = content.length - old_length;
                    var old_content = content;

                    if(overlap > 0){

                        // clear content (bypass)

                        content = "";
                    }

                    // right-padding invalid index

                    while(content.length < old_length){

                        content = (content + "                                                  ").substring(0, old_length);
                    }

                    var bulk = this._bulk;
                    var chunk_index = marker[0];
                    var chunk = bulk[chunk_index];

                    // submerge updated content to bulk

                    bulk[chunk_index] = chunk.substring(0, min) +
                                        content +
                                        chunk.substring(max);

                    // check if content length has enlarged

                    if(overlap > 0 || !old_content){

                        // get fragments

                        var current_fragment_length = this._fragment[old_length];

                        if(!current_fragment_length){

                            this._fragment[old_length] = [];

                            current_fragment_length = 0;
                        }
                        else{

                            current_fragment_length = current_fragment_length.length;
                        }

                        // add fragment

                        this._fragment[old_length][current_fragment_length] = marker;
                        this._fragmented += old_length;

                        // delete marker

                        this._marker[id] = null;

                        // add overlapping contents to the end

                        if(old_content){

                            this.add(id, old_content);
                        }
                    }

                    // update status

                    this._status = false;
                }
            }
        };

        /**
         * @this {BulkSearch}
         * @export
         */

        BulkSearch.prototype.remove = function(id){

            this.update(id, '');

            delete this._marker[id];
        };

        var regex_split = regex("[ -/]");

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

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

            if((typeof query !== 'string') || !query){

                return [];
            }
            else if(typeof limit === 'function'){

                callback = limit;
                limit = 1000;
            }
            else{

                limit || (limit = 1000);
            }

            var _query = query;

            // invalidate cache

            if(!this._status){

                if(this.cache){

                    this._last_empty_query = "";
                    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(this._last_empty_query && (query.indexOf(this._last_empty_query) !== -1)){

                return [];
            }

            if(callback){

                /** @type {BulkSearch} */
                var self = this;

                queue(function(){

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

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

                return null;
            }

            // 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(regex_split)
                :
                    [_query]
            );

            var length = words.length;

            // sort words by length

            if(length > 1){

                words.sort(sort_by_length_down);
            }

            var encoded = new Array(length);

            // encode query

            if(this.encode){

                words[0] = this.encode(words[0]);
            }

            // perform search

            var result = [];

            for(var z = 0; z < this._bulk.length; z++){

                var start = 0;
                var pos = 0;
                var bulk = this._bulk[z];

                while((pos = bulk.indexOf(words[0], start)) !== -1){

                    var current_id = this._index[z][pos];
                    var marker = this._marker[current_id];

                    if(marker){

                        var min = marker[1];
                        var max = marker[2];
                        var matched = true;
                        var sub_bulk;

                        // check multiple word components

                        if(length > 1){

                            sub_bulk = bulk.substring(min, max);

                            for(var i = 1; i < length; i++){

                                // encode query

                                if(this.encode && !encoded[i]){

                                    words[i] = this.encode(words[i]);
                                    encoded[i] = true;
                                }

                                if(words[i]){

                                    if(sub_bulk.indexOf(words[i]) === -1){

                                        // boolean and:

                                        if(!this.boolean){

                                            matched = false;
                                            break;
                                        }
                                        else{

                                            matched = false;
                                        }
                                    }
                                    else{

                                        // boolean or:

                                        if(this.boolean){

                                            matched = true;
                                            break;
                                        }
                                    }
                                }
                            }
                        }

                        if(matched){

                            // increase score

                            sub_bulk || (sub_bulk = bulk.substring(min, max));
                            sub_bulk = sub_bulk.substring(0, this.depth);
                            scores[sub_bulk] ? scores[sub_bulk]++ : scores[sub_bulk] = 1;

                            // enrich result
                            // result[result.length] = {
                            //
                            //     'id': current_id,
                            //     'rank': scores[sub_bulk],
                            //     'score': 1.0
                            // };

                            result[result.length] = current_id;

                            // apply limit

                            if(limit && (result.length === limit)){

                                break;
                            }
                        }

                        pos = max;
                    }

                    start = pos + 1;
                }

                if(limit && (result.length === limit)){

                    break;
                }
            }

            if(result.length){

                this._last_empty_query = "";
            }
            else{

                this._last_empty_query || (this._last_empty_query = query);
            }

            // store result to cache

            if(this.cache){

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

            return result;
        };

        /**
         * @this {BulkSearch}
         * @export
         */

        BulkSearch.prototype.optimize = function(){

            var old_bulk = this._bulk;
            var old_marker = this._marker;
            var old_cache = this._cache;
            var old_register = this._register;

            // reset index

            this.reset();

            // get ids

            // var score_keys = Object.keys(/** @type {!Object} */ (scores));
            // var register_keys = Object.keys(old_register);

            // re-add contents

            // for(var i = 0; i < score_keys.length; i++){
            //
            //     var key = score_keys[i];
            //     var score = scores[key];
            //
            //     if(score){
            //
            //         for(var a = 0; a < register_keys.length; a++){
            //
            //             if(score_keys[i] === register_keys[a]){
            //
            //
            //             }
            //         }
            //     }
            // }

            // get ids

            var marker_keys = Object.keys(old_marker);

            // re-add contents

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

                var key = marker_keys[i];
                var marker = old_marker[key];

                if(marker){

                    var bulk = old_bulk[marker[0]];
                    var current_value = bulk.substring(marker[1], marker[2]);

                    this.add(key, current_value);
                }
                else{

                    delete old_marker[key];
                }
            }

            // assign valid cache

            if(old_cache){

                this._cache = old_cache;
            }
        };

        /**
         * @this {BulkSearch}
         * @export
         */

        BulkSearch.prototype.info = function(){

            var fragmented = this._fragmented;
            var marker_length = Object.keys(this._marker).length;
            var bytes = 0;
            var length = 0;

            for(var z = 0; z < this._bulk.length; z++){

                length += this._bulk[z].length;
            }

            if(length){

                fragmented = ((100 / length * fragmented * 100) | 0) / 100;

                bytes = (

                    this._index[0].constructor === Array ?

                        32
                    :
                        this._index[0].byteLength
                );

                bytes = length * 2 + marker_length * 8 * 3 + this._bulk.length * chunk_size * bytes;
            }

            return {

                'id': this.id,
                'length': marker_length,
                'chunks': this._bulk.length,
                'register': Object.keys(this._register).length,
                'depth': this.depth,
                'size': chunk_size,
                'bytes': bytes,
                'fragments': Object.keys(this._fragment).length,
                'fragmented': fragmented,
                'status': this._status,
                'matchers': custom_matchers.length
            };
        };

        /**
         * @this {BulkSearch}
         * @export
         */

        BulkSearch.prototype.reset = function(){

            // destroy index

            this.destroy();

            // initialize index

            this.init();
        };

        /**
         * @this {BulkSearch}
         * @export
         */

        BulkSearch.prototype.destroy = function(){

            // cleanup cache

            if(this.cache){

                this._cache.reset();
            }

            // release references

            this._index = null;
            this._marker = null;
            this._fragment = null;
            this._bulk = null;
            this._register = null;
            this._cache = null;
        };

        /**
         * Phonetic Encoders
         * @enum {Function}
         * @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_split = regex('[-/]'),
                    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('ß');

                var regex_pairs = [

                    regex_a, 'a',
                    regex_e, 'e',
                    regex_i, 'i',
                    regex_o, 'o',
                    regex_u, 'u',
                    regex_y, 'y',
                    regex_n, 'n',
                    regex_c, 'c',
                    regex_s, 's',
                    regex_split, ' ',
                    regex_strip, ''
                ];

                return function(str){

                    return (

                        replace(str.toLowerCase(), regex_pairs)
                    );
                };
            }()),

            // advanced phonetic transformation (latin)

            'advanced': (function(){

                var regex_space = regex(' '),
                    regex_ae = regex('ae'),
                    regex_ai = regex('ai'),
                    regex_ay = regex('ay'),
                    regex_ey = regex('ey'),
                    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');

                var regex_pairs = [

                    regex_ae, 'a',
                    regex_ai, 'ei',
                    regex_ay, 'ei',
                    regex_ey, 'ei',
                    regex_oe, 'o',
                    regex_ue, 'u',
                    regex_ie, 'i',
                    regex_sz, 's',
                    regex_zs, 's',
                    regex_sh, 's',
                    regex_ck, 'k',
                    regex_cc, 'k',
                    //regex_th, 't',
                    regex_dt, 't',
                    regex_ph, 'f',
                    regex_ou, 'o',
                    regex_uo, 'u'
                ];

                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 = replace(string, regex_pairs)
                    }

                    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]');

                var regex_pairs = [

                    soundex_b, 'b',
                    soundex_c, 'c',
                    soundex_k, 'k',
                    soundex_i, 'i',
                    soundex_m, 'm',
                    soundex_t, 't',
                    soundex_f, 'f',
                    regex_vowel, ''
                ];

                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] + replace(current.substring(1), regex_pairs)
                            }
                        }

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

                    return str;
                };
            })()

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

        return BulkSearch;

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

        function create_typed_array(type, size){

            /** @type {Function} */

            var array_type = (

                (type === 'int') || (type === 'integer') ?

                    Uint32Array
                :(
                    type === 'short' ?

                        Uint16Array
                    :(
                        (type === 'float') || (type === 'double') ?

                            Float64Array
                        :(
                            type === 'byte' ?

                                Uint8Array
                            :
                                Array
                        )
                    )
                )

            ) || Array;

            return new array_type(size);
        }

        /**
         * @param {!string} str
         * @returns {RegExp}
         */

        function regex(str){

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

        /**
         * @param {!string} str
         * @param {RegExp|Array} regex
         * @param {string=} replacement
         * @returns {string}
         */

        function replace(str, regex, replacement){

            if(typeof replacement === 'undefined'){

                for(var i = 0; i < /** @type {Array} */ (regex).length; i += 2){

                    str = str.replace(regex[i], regex[i + 1]);
                }

                return str;
            }
            else{

                return str.replace(/** @type {!RegExp} */ (regex), replacement);
            }
        }

        /**
         * @param {!Array|string} a
         * @param {!Array|string} b
         * @returns {number}
         */

        function sort_by_length_down(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;
        }
    })(
        // Xone Async Handler Fallback

        (function(){

            var stack = {};

            return function(fn, delay, id){

                var timer = stack[id];

                if(timer){

                    clearTimeout(timer);
                }

                return (

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

        // Xone Flexi-Cache Handler Fallback

        (function(){

            /** @this {Cache} */
            function Cache(){

                this.cache = {};
            }

            /** @this {Cache} */
            Cache.prototype.reset = function(){

                this.cache = {};
            };

            /** @this {Cache} */
            Cache.prototype.set = function(id, value){

                this.cache[id] = value;
            };

            /** @this {Cache} */
            Cache.prototype.get = function(id){

                return this.cache[id];
            };

            return Cache;
        })()

    ), this);

    /** --------------------------------------------------------------------------------------
     * UMD Wrapper for Browser and Node.js
     * @param {!string} name
     * @param {!Function|Object} factory
     * @param {!Function|Object=} root
     * @suppress {checkVars}
     * @const
     */

    function provide(name, factory, root){

        root || (root = this);

        var prop;

        // AMD (RequireJS)
        if((prop = root['define']) && prop['amd']){

            prop([], function(){

                return factory;
            });
        }
        // Closure (Xone)
        else if((prop = root['modules'])){

            prop[name.toLowerCase()] = factory;
        }
        // CommonJS (Node.js)
        else if(typeof module !== 'undefined'){

            /** @export */
            module.exports = factory;
        }
        // Global (window)
        else{

            root[name] = factory;
        }
    }

}).call(this);
