'use strict';

// Load modules

const Stream = require('stream');

const Hoek = require('hoek');
const Vise = require('vise');


// Declare internals

const internals = {};


exports.compile = function (needle) {

    Hoek.assert(needle && needle.length, 'Missing needle');
    Hoek.assert(Buffer.isBuffer(needle), 'Needle must be a buffer');

    const profile = {
        value: needle,
        lastPos: needle.length - 1,
        last: needle[needle.length - 1],
        length: needle.length,
        badCharShift: Buffer.alloc(256)                  // Lookup table of how many characters can be skipped for each match
    };

    for (let i = 0; i < 256; ++i) {
        profile.badCharShift[i] = profile.length;       // Defaults to the full length of the needle
    }

    const last = profile.length - 1;
    for (let i = 0; i < last; ++i) {                    // For each character in the needle (skip last since its position is already the default)
        profile.badCharShift[profile.value[i]] = last - i;
    }

    return profile;
};


exports.horspool = function (haystack, needle, start) {

    Hoek.assert(haystack, 'Missing haystack');

    needle = (needle.badCharShift ? needle : exports.compile(needle));
    start = start || 0;

    for (let i = start; i <= haystack.length - needle.length;) {       // Has enough room to fit the entire needle
        const lastChar = haystack.readUInt8(i + needle.lastPos);
        if (lastChar === needle.last &&
            internals.startsWith(haystack, needle, i)) {

            return i;
        }

        i += needle.badCharShift[lastChar];           // Jump to the next possible position based on last character location in needle
    }

    return -1;
};


internals.startsWith = function (haystack, needle, pos) {

    if (haystack.startsWith) {
        return haystack.startsWith(needle.value, pos, needle.lastPos);
    }

    for (let i = 0; i < needle.lastPos; ++i) {
        if (needle.value[i] !== haystack.readUInt8(pos + i)) {
            return false;
        }
    }

    return true;
};


exports.all = function (haystack, needle, start) {

    needle = exports.compile(needle);
    start = start || 0;

    const matches = [];
    for (let i = start; i !== -1 && i < haystack.length;) {

        i = exports.horspool(haystack, needle, i);
        if (i !== -1) {
            matches.push(i);
            i += needle.length;
        }
    }

    return matches;
};


internals._indexOf = function (haystack, needle) {

    Hoek.assert(haystack, 'Missing haystack');

    for (let i = 0; i <= haystack.length - needle.length; ++i) {       // Has enough room to fit the entire needle
        if (haystack.startsWith(needle.value, i)) {
            return i;
        }
    }

    return -1;
};


exports.Stream = internals.Stream = function (needle) {

    const self = this;

    Stream.Writable.call(this);

    this.needle(needle);
    this._haystack = new Vise();
    this._indexOf = this._needle.length > 2 ? exports.horspool : internals._indexOf;

    this.on('finish', () => {

        // Flush out the remainder

        const chunks = self._haystack.chunks();
        for (let i = 0; i < chunks.length; ++i) {
            self.emit('haystack', chunks[i]);
        }

        setImmediate(() => {                  // Give pending events a chance to fire

            self.emit('close');
        });
    });
};

Hoek.inherits(internals.Stream, Stream.Writable);


internals.Stream.prototype.needle = function (needle) {

    this._needle = exports.compile(needle);
};


internals.Stream.prototype._write = function (chunk, encoding, next) {

    this._haystack.push(chunk);

    let match = this._indexOf(this._haystack, this._needle);
    if (match === -1 &&
        chunk.length >= this._needle.length) {

        this._flush(this._haystack.length - chunk.length);
    }

    while (match !== -1) {
        this._flush(match);
        this._haystack.shift(this._needle.length);
        this.emit('needle');

        match = this._indexOf(this._haystack, this._needle);
    }

    if (this._haystack.length) {
        const notChecked = this._haystack.length - this._needle.length + 1;       // Not enough space for Horspool
        let i = notChecked;
        for (; i < this._haystack.length; ++i) {
            if (this._haystack.startsWith(this._needle.value, i, this._haystack.length - i)) {
                break;
            }
        }

        this._flush(i);
    }

    return next();
};


internals.Stream.prototype._flush = function (pos) {

    const chunks = this._haystack.shift(pos);
    for (let i = 0; i < chunks.length; ++i) {
        this.emit('haystack', chunks[i]);
    }
};


internals.Stream.prototype.flush = function () {

    const chunks = this._haystack.shift(this._haystack.length);
    for (let i = 0; i < chunks.length; ++i) {
        this.emit('haystack', chunks[i]);
    }
};
