
/**
 * A simple peak detector
 *
 * Usage: Successively add values via add() and query the indices of the peaks with peaks
 *
 * A peak is detected if the current point is local maximum (no filtering!) and the current
 * value is larger than (threshold + minimum_since_last_peak)
 */
class PeakDetectorSimple {
    constructor(threshold) {
        this._threshold = threshold;
        this._queue = [];
        this._last_min = 0;
        this._counter = 0;
    }

    getThreshold() { 
        return this._threshold;
    }
    
    addVector(vec) {
        let result = [];
        
        for (let i = 0; i < vec.length; ++i) {
            const res = this.add(vec[i]);
            if(res !== null)
                result.push(res);
        }
        return result;
    }

    add(value) {
        let result = null;
        this._queue.push(value);
        if (this._queue.length > 3) {
            this._queue.shift();
        }
        if (this._queue.length !== 3) {
            return null;
        }
        const [last, current, next] = this._queue;
        const is_maximum = current > next && current > last;
        if (is_maximum && (current - this._last_min) > this._threshold) {
            result = this._counter + 1;
            this._last_min = current;
        }
        this._last_min = Math.min(this._last_min, current);
        this._counter += 1;
        return result;
    }
}


/**
 * Implementation of Z-Score peak detection according to
 * https://stackoverflow.com/questions/22583391/peak-signal-detection-in-realtime-timeseries-data
 */
class PeakDetectorZScore {
    constructor(lag, threshold, influence, handleNewPeaks) {
        this.peaks = [];
        this._filter = ZScoreFilter(lag, threshold, influence);
        this._counter = 0;
        this._previous_signal = 0;
        this._max = null;
        this._max_index = null;
        this._handleNewPeaks = handleNewPeaks;
    }

    addVector(vec) {
        const callbackBackup = this._handleNewPeaks;
        const numPeaksBefore = this.peaks.length;
        this._handleNewPeaks = null;
        for (let i = 0; i < vec.length; ++i) {
            this.add(vec[i]);
        }
        this._handleNewPeaks = callbackBackup;
        if (numPeaksBefore != this.peaks.length) {
            this._handleNewPeaks(this.peaks);
        }
    }

    add(value) {
        const signal = this._filter.add(value);
        if (signal != null) {
            const rising_flank = this._previous_signal !== 1 && signal === 1;
            const falling_flank = this._previous_signal === 1 && signal !== 1;
            if (rising_flank)
                this._max = -1;
            if (signal === 1 && this._max != null && value > this._max) {
                this._max = value;
                this._max_index = this._counter;
            }
            if (falling_flank) {
                this.peaks.push(this._max_index);
                if (this._handleNewPeaks) {
                    this._handleNewPeaks(this.peaks);
                }
            }

            this._previous_signal = signal;
        }
        this._counter += 1;
    }
}


export { PeakDetectorSimple, PeakDetectorZScore };


// --------------------------------------- Helper classes -------------------------------------------------------------

class StatisticsQueue {
    constructor(size) {
        this._size = size;

        this._queue = [];
        this._queue_sum = 0;  // running sum over all elements currently in _queue
        this._queue_sum_sq = 0; // sum of squared elements in _queue
    }

    add(value) {
        this._queue.push(value);

        this._queue_sum += value;
        this._queue_sum_sq += value * value;

        if (this._queue.length > this._size) {
            const removed = this._queue[0];
            this._queue.shift();
            this._queue_sum -= removed;
            this._queue_sum_sq -= removed * removed;
        }
    }

    get avg() {
        return this._queue_sum / self._queue.length;
    }

    get variance() {
        const exp_sq = this._queue_sum_sq / self._queue.length;
        const my_avg = self.avg;
        return exp_sq - (my_avg * my_avg);
    }

    get std_deviation() {
        return Math.sqrt(this.variance);
    }

    get filled() {
        return this._queue.length === this._size;
    }
}

class ZScoreFilter {

    constructor(lag, threshold, influence) {
        this._threshold = threshold;
        this._influence = influence;

        this._last_value = null;
        this._stat_queue = StatisticsQueue(lag);
    }

    add(value) {
        let sq = this._stat_queue;
        if (!sq.filled) {
            sq.add(value);
            this._last_value = value;
            return null;
        } else {
            const avg = sq.avg;
            if (Math.abs(value - avg) > this._threshold * sq.std_deviation) {
                const signal = value > avg ? 1 : -1;
                const filtered = this._influence * value + (1 - this._influence) * this._last_value;
                sq.add(filtered);
                this._last_value = filtered;
                return signal;
            } else {
                sq.add(value);
                this._last_value = value;
            }
        }
    }
}
