'use strict';

const Stream = require('stream');

const Hoek = require('@hapi/hoek');
const { Vise } = require('@hapi/vise');


const internals = {};


exports.compile = function (needle) {

    Hoek.assert(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 = class extends Stream.Writable {

    constructor(needle) {

        super();

        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 = this._haystack.chunks();
            for (let i = 0; i < chunks.length; ++i) {
                this.emit('haystack', chunks[i]);
            }
        });
    }

    needle(needle) {

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

    _write(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();
    }

    _flush(pos) {

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

    flush() {

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