//
// Shinobi - fork of pam-diff
// Copyright (C) 2018 Kevin Godell
// Author : Kevin Godell, https://github.com/kevinGodell
// npmjs : https://www.npmjs.com/package/pam-diff
// Github : https://github.com/kevinGodell/pam-diff
//
'use strict';

const { Transform } = require('stream');

const PP = require('polygon-points');
/**
 *
 * @param chunk
 * @private
 */
 var _getMatrixFromPoints = function(thisRegion) {
     var coordinates = [
         thisRegion.topLeft,
         {"x" : thisRegion.bottomRight.x, "y" : thisRegion.topLeft.y},
         thisRegion.bottomRight
     ]
     var width = Math.sqrt( Math.pow(coordinates[1].x - coordinates[0].x, 2) + Math.pow(coordinates[1].y - coordinates[0].y, 2));
     var height = Math.sqrt( Math.pow(coordinates[2].x - coordinates[1].x, 2) + Math.pow(coordinates[2].y - coordinates[1].y, 2))
     return {
         x: coordinates[0].x,
         y: coordinates[0].y,
         width: width,
         height: height,
         tag: thisRegion.name
     }
 }
class PamDiff extends Transform {

    /**
     *
     * @param [options] {Object}
     * @param [callback] {Function}
     */
    constructor(options, callback) {
        super(options);
        Transform.call(this, {objectMode: true});
        this.difference = PamDiff._parseOptions('difference', options);//global option, can be overridden per region
        this.percent = PamDiff._parseOptions('percent', options);//global option, can be overridden per region
        this.regions = PamDiff._parseOptions('regions', options);//can be no regions or a single region or multiple regions. if no regions, all pixels will be compared.
        this.drawMatrix = PamDiff._parseOptions('drawMatrix', options);//can be no regions or a single region or multiple regions. if no regions, all pixels will be compared.
        this.callback = callback;//callback function to be called when pixel difference is detected
        this._parseChunk = this._parseFirstChunk;//first parsing will be reading settings and configuring internal pixel reading
    }

    /**
     *
     * @param option {String}
     * @param options {Object}
     * @return {*}
     * @private
     */
    static _parseOptions(option, options) {
        if (options && options.hasOwnProperty(option)) {
            return options[option];
        }
        return null;
    }

    /**
     *
     * @param number {Number}
     * @param def {Number}
     * @param low {Number}
     * @param high {Number}
     * @return {Number}
     * @private
     */
    static _validateNumber(number, def, low, high) {
        if (isNaN(number)) {
            return def;
        } else if (number < low) {
            return low;
        } else if (number > high) {
            return high;
        } else {
            return number;
        }
    }

    /**
     *
     * @deprecated
     * @param string {String}
     */
    setGrayscale(string) {
        console.warn('grayscale option has been removed, "average" has proven to most accurate and is the default');
    }

    /**
     *
     * @param number {Number}
     */
    set difference(number) {
        this._difference = PamDiff._validateNumber(parseInt(number), 5, 1, 255);
    }

    /**
     *
     * @return {Number}
     */
    get difference() {
        return this._difference;
    }

    /**
     *
     * @param number {Number}
     * @return {PamDiff}
     */
    setDifference(number) {
        this.difference = number;
        return this;
    }

    /**
     *
     * @param number {Number}
     */
    set percent(number) {
        this._percent = PamDiff._validateNumber(parseInt(number), 5, 1, 100);
    }

    /**
     *
     * @return {Number}
     */
    get percent() {
        return this._percent;
    }

    /**
     *
     * @param number {Number}
     * @return {PamDiff}
     */
    setPercent(number) {
        this.percent = number;
        return this;
    }

    /**
     *
     * @param array {Array}
     */
    set regions(array) {
        if (!array) {
            if (this._regions) {
                delete this._regions;
                delete this._regionsLength;
                delete this._minDiff;
            }
            this._diffs = 0;
        } else if (!Array.isArray(array) || array.length < 1) {
            throw new Error(`Regions must be an array of at least 1 region object {name: 'region1', difference: 10, percent: 10, polygon: [[0, 0], [0, 50], [50, 50], [50, 0]]}`);
        } else {
            this._regions = [];
            this._minDiff = 255;
            for (const region of array) {
                if (!region.hasOwnProperty('name') || !region.hasOwnProperty('polygon')) {
                    throw new Error('Region must include a name and a polygon property');
                }
                const polygonPoints = new PP(region.polygon);
                const difference = PamDiff._validateNumber(parseInt(region.difference), this._difference, 1, 255);
                const percent = PamDiff._validateNumber(parseInt(region.percent), this._percent, 1, 100);
                this._minDiff = Math.min(this._minDiff, difference);
                this._regions.push(
                    {
                        name: region.name,
                        polygon: polygonPoints,
                        difference: difference,
                        percent: percent,
                        diffs: 0
                    }
                );
            }
            this._regionsLength = this._regions.length;
            this._createPointsInPolygons(this._regions, this._width, this._height);
        }
    }

    /**
     *
     * @return {Array}
     */
    get regions() {
        return this._regions;
    }

    /**
     *
     * @param array {Array}
     * @return {PamDiff}
     */
    setRegions(array) {
        this.regions = array;
        return this;
    }

    /**
     *
     * @param func {Function}
     */
    set callback(func) {
        if (!func) {
            delete this._callback;
        } else if (typeof func === 'function' && func.length === 1) {
            this._callback = func;
        } else {
            throw new Error('Callback must be a function that accepts 1 argument.');
        }
    }

    /**
     *
     * @return {Function}
     */
    get callback() {
        return this._callback;
    }

    /**
     *
     * @param func {Function}
     * @return {PamDiff}
     */
    setCallback(func) {
        this.callback = func;
        return this;
    }

    /**
     *
     * @return {PamDiff}
     */
    resetCache() {
        //delete this._oldPix;
        //delete this._newPix;
        //delete this._width;
        //delete this._length;
        this._parseChunk = this._parseFirstChunk;
        return this;
    }

    /**
     *
     * @param regions {Array}
     * @param width {Number}
     * @param height {Number}
     * @private
     */
    _createPointsInPolygons(regions, width, height) {
        if (regions && width && height) {
            this._pointsInPolygons = [];
            for (const region of regions) {
                const bitset = region.polygon.getBitset(this._width, this._height);
                region.pointsLength = bitset.count;
                this._pointsInPolygons.push(bitset.buffer);
            }
        }
    }

    /**
     *
     * @param chunk
     * @private
     */
    _blackAndWhitePixelDiff(chunk) {
        this._newPix = chunk.pixels;
        for (let y = 0, i = 0; y < this._height; y++) {
            for (let x = 0; x < this._width; x++, i++) {
                const diff = this._oldPix[i] !== this._newPix[i];
                if (this._regions && diff === true) {
                    for (let j = 0; j < this._regionsLength; j++) {
                        if (this._pointsInPolygons[j][i]) {
                            this._regions[j].diffs++;
                        }
                    }
                } else if (diff === true) {
                    this._diffs++;
                }
            }
        }
        if (this._regions) {
            const regionDiffArray = [];
            for (let i = 0; i < this._regionsLength; i++) {
                const percent = Math.floor(100 * this._regions[i].diffs / this._regions[i].pointsLength);
                if (percent >= this._regions[i].percent) {
                    regionDiffArray.push({name: this._regions[i].name, percent: percent});
                }
                this._regions[i].diffs = 0;
            }
            if (regionDiffArray.length > 0) {
                const data = {trigger: regionDiffArray, pam: chunk.pam};
                if (this._callback) {
                    this._callback(data);
                }
                if (this._readableState.pipesCount > 0) {
                    this.push(data);
                }
                if (this.listenerCount('diff') > 0) {
                    this.emit('diff', data);
                }
            }
        } else {
            const percent = Math.floor(100 * this._diffs / this._length);
            if (percent >= this._percent) {
                const data = {trigger: [{name: 'percent', percent: percent}], pam: chunk.pam};
                if (this._callback) {
                    this._callback(data);
                }
                if (this._readableState.pipesCount > 0) {
                    this.push(data);
                }
                if (this.listenerCount('diff') > 0) {
                    this.emit('diff', data);
                }
            }
            this._diffs = 0;
        }
        this._oldPix = this._newPix;
    }

    /**
     *
     * @param chunk
     * @private
     */
    _grayScalePixelDiffWithMatrices(chunk) {
        this._newPix = chunk.pixels;
        for (let j = 0; j < this._regionsLength; j++) {
            this._regions[j].topLeft = {
                x: this._width,
                y: this._height
            }
            this._regions[j].bottomRight = {
                x: 0,
                y: 0
            }
        }
        this.topLeft = {
            x: this._width,
            y: this._height
        }
        this.bottomRight = {
            x: 0,
            y: 0
        }
        for (let y = 0, i = 0; y < this._height; y++) {
            for (let x = 0; x < this._width; x++, i++) {
                if (this._oldPix[i] !== this._newPix[i]) {
                    const diff = Math.abs(this._oldPix[i] - this._newPix[i]);
                    if (this._regions && diff >= this._minDiff) {
                        for (let j = 0; j < this._regionsLength; j++) {
                            if (this._pointsInPolygons[j][i] && diff >= this._regions[j].difference) {
                                var theRegion = this._regions[j]
                                theRegion.diffs++;
                                if(theRegion.topLeft.x > x && theRegion.topLeft.y > y){
                                    theRegion.topLeft.x = x
                                    theRegion.topLeft.y = y
                                }
                                if(theRegion.bottomRight.x < x && theRegion.bottomRight.y < y){
                                    theRegion.bottomRight.x = x
                                    theRegion.bottomRight.y = y
                                }
                            }
                        }
                    } else if (diff >= this._difference) {
                        this._diffs++;
                        if(this.topLeft.x > x && this.topLeft.y > y){
                            this.topLeft.x = x
                            this.topLeft.y = y
                        }
                        if(this.bottomRight.x < x && this.bottomRight.y < y){
                            this.bottomRight.x = x
                            this.bottomRight.y = y
                        }
                    }
                }
            }
        }
        if (this._regions) {
            const regionDiffArray = [];
            for (let i = 0; i < this._regionsLength; i++) {
                var thisRegion = this._regions[i]
                const percent = Math.floor(100 * thisRegion.diffs / thisRegion.pointsLength);
                if (percent >= thisRegion.percent) {
                    // create matrix from points >>
                    thisRegion._matrix = _getMatrixFromPoints(thisRegion)
                    // create matrix from points />>
                    regionDiffArray.push({name: thisRegion.name, percent: percent, matrix: thisRegion._matrix});
                }
                thisRegion.diffs = 0;
            }
            if (regionDiffArray.length > 0) {
                this._matrix = _getMatrixFromPoints(this)
                const data = {trigger: regionDiffArray, pam: chunk.pam, matrix: this._matrix};
                if (this._callback) {
                    this._callback(data);
                }
                if (this._readableState.pipesCount > 0) {
                    this.push(data);
                }
                if (this.listenerCount('diff') > 0) {
                    this.emit('diff', data);
                }
            }
        } else {
            const percent = Math.floor(100 * this._diffs / this._length);
            if (percent >= this._percent) {
                this._matrix = _getMatrixFromPoints(this)
                const data = {trigger: [{name: 'percent', percent: percent, matrix: this._matrix}], pam: chunk.pam};
                if (this._callback) {
                    this._callback(data);
                }
                if (this._readableState.pipesCount > 0) {
                    this.push(data);
                }
                if (this.listenerCount('diff') > 0) {
                    this.emit('diff', data);
                }
            }
            this._diffs = 0;
        }
        this._oldPix = this._newPix;
    }

    /**
     *
     * @param chunk
     * @private
     */
     _grayScalePixelDiff(chunk) {
         this._newPix = chunk.pixels;
         for (let y = 0, i = 0; y < this._height; y++) {
             for (let x = 0; x < this._width; x++, i++) {
                 if (this._oldPix[i] !== this._newPix[i]) {
                     const diff = Math.abs(this._oldPix[i] - this._newPix[i]);
                     if (this._regions && diff >= this._minDiff) {
                         for (let j = 0; j < this._regionsLength; j++) {
                             if (this._pointsInPolygons[j][i] && diff >= this._regions[j].difference) {
                                 this._regions[j].diffs++;
                             }
                         }
                     } else {
                         if (diff >= this._difference) {
                             this._diffs++;
                         }
                     }
                 }
             }
         }
         if (this._regions) {
             const regionDiffArray = [];
             for (let i = 0; i < this._regionsLength; i++) {
                 const percent = Math.floor(100 * this._regions[i].diffs / this._regions[i].pointsLength);
                 if (percent >= this._regions[i].percent) {
                     regionDiffArray.push({name: this._regions[i].name, percent: percent});
                 }
                 this._regions[i].diffs = 0;
             }
             if (regionDiffArray.length > 0) {
                 const data = {trigger: regionDiffArray, pam: chunk.pam};
                 if (this._callback) {
                     this._callback(data);
                 }
                 if (this._readableState.pipesCount > 0) {
                     this.push(data);
                 }
                 if (this.listenerCount('diff') > 0) {
                     this.emit('diff', data);
                 }
             }
         } else {
             const percent = Math.floor(100 * this._diffs / this._length);
             if (percent >= this._percent) {
                 const data = {trigger: [{name: 'percent', percent: percent}], pam: chunk.pam};
                 if (this._callback) {
                     this._callback(data);
                 }
                 if (this._readableState.pipesCount > 0) {
                     this.push(data);
                 }
                 if (this.listenerCount('diff') > 0) {
                     this.emit('diff', data);
                 }
             }
             this._diffs = 0;
         }
         this._oldPix = this._newPix;
     }

    /**
     *
     * @param chunk
     * @private
     */
    _rgbPixelDiff(chunk) {
        this._newPix = chunk.pixels;
        for (let y = 0, i = 0, p = 0; y < this._height; y++) {
            for (let x = 0; x < this._width; x++, i += 3, p++) {
                if (this._oldPix[i] !== this._newPix[i] || this._oldPix[i + 1] !== this._newPix[i + 1] || this._oldPix[i + 2] !== this._newPix[i + 2]) {
                    const diff = Math.abs(this._oldPix[i] + this._oldPix[i + 1] + this._oldPix[i + 2] - this._newPix[i] - this._newPix[i + 1] - this._newPix[i + 2])/3;
                    if (this._regions && diff >= this._minDiff) {
                        for (let j = 0; j < this._regionsLength; j++) {
                            if (this._pointsInPolygons[j][p] && diff >= this._regions[j].difference) {
                                this._regions[j].diffs++;
                            }
                        }
                    } else {
                        if (diff >= this._difference) {
                            this._diffs++;
                        }
                    }
                }
            }
        }
        if (this._regions) {
            const regionDiffArray = [];
            for (let i = 0; i < this._regionsLength; i++) {
                const percent = Math.floor(100 * this._regions[i].diffs / this._regions[i].pointsLength);
                if (percent >= this._regions[i].percent) {
                    regionDiffArray.push({name: this._regions[i].name, percent: percent});
                }
                this._regions[i].diffs = 0;
            }
            if (regionDiffArray.length > 0) {
                const data = {trigger: regionDiffArray, pam: chunk.pam};
                if (this._callback) {
                    this._callback(data);
                }
                if (this._readableState.pipesCount > 0) {
                    this.push(data);
                }
                if (this.listenerCount('diff') > 0) {
                    this.emit('diff', data);
                }
            }
        } else {
            const percent = Math.floor(100 * this._diffs / this._length);
            if (percent >= this._percent) {
                const data = {trigger: [{name: 'percent', percent: percent}], pam: chunk.pam};
                if (this._callback) {
                    this._callback(data);
                }
                if (this._readableState.pipesCount > 0) {
                    this.push(data);
                }
                if (this.listenerCount('diff') > 0) {
                    this.emit('diff', data);
                }
            }
            this._diffs = 0;
        }
        this._oldPix = this._newPix;
    }

    /**
     *
     * @param chunk
     * @private
     */
    _rgbAlphaPixelDiff(chunk) {
        this._newPix = chunk.pixels;
        for (let y = 0, i = 0, p = 0; y < this._height; y++) {
            for (let x = 0; x < this._width; x++, i += 4, p++) {
                if (this._oldPix[i] !== this._newPix[i] || this._oldPix[i + 1] !== this._newPix[i + 1] || this._oldPix[i + 2] !== this._newPix[i + 2]) {
                    const diff = Math.abs(this._oldPix[i] + this._oldPix[i + 1] + this._oldPix[i + 2] - this._newPix[i] - this._newPix[i + 1] - this._newPix[i + 2])/3;
                    if (this._regions && diff >= this._minDiff) {
                        for (let j = 0; j < this._regionsLength; j++) {
                            if (this._pointsInPolygons[j][p] && diff >= this._regions[j].difference) {
                                this._regions[j].diffs++;
                            }
                        }
                    } else {
                        if (diff >= this._difference) {
                            this._diffs++;
                        }
                    }
                }
            }
        }
        if (this._regions) {
            const regionDiffArray = [];
            for (let i = 0; i < this._regionsLength; i++) {
                const percent = Math.floor(100 * this._regions[i].diffs / this._regions[i].pointsLength);
                if (percent >= this._regions[i].percent) {
                    regionDiffArray.push({name: this._regions[i].name, percent: percent});
                }
                this._regions[i].diffs = 0;
            }
            if (regionDiffArray.length > 0) {
                const data = {trigger: regionDiffArray, pam: chunk.pam};
                if (this._callback) {
                    this._callback(data);
                }
                if (this._readableState.pipesCount > 0) {
                    this.push(data);
                }
                if (this.listenerCount('diff') > 0) {
                    this.emit('diff', data);
                }
            }
        } else {
            const percent = Math.floor(100 * this._diffs / this._length);
            if (percent >= this._percent) {
                const data = {trigger: [{name: 'percent', percent: percent}], pam: chunk.pam};
                if (this._callback) {
                    this._callback(data);
                }
                if (this._readableState.pipesCount > 0) {
                    this.push(data);
                }
                if (this.listenerCount('diff') > 0) {
                    this.emit('diff', data);
                }
            }
            this._diffs = 0;
        }
        this._oldPix = this._newPix;
    }

    /**
     *
     * @param chunk
     * @private
     */
    _parseFirstChunk(chunk) {
        this._width = parseInt(chunk.width);
        this._height = parseInt(chunk.height);
        this._oldPix = chunk.pixels;
        this._length = this._width * this._height;
        this._createPointsInPolygons(this._regions, this._width, this._height);
        switch (chunk.tupltype) {
            case 'blackandwhite' :
                this._parseChunk = this._blackAndWhitePixelDiff;
                break;
            case 'grayscale' :
                if(this.drawMatrix === "1"){
                    this._parseChunk = this._grayScalePixelDiffWithMatrices;
                }else{
                    this._parseChunk = this._grayScalePixelDiff;
                }
                break;
            case 'rgb' :
                this._parseChunk = this._rgbPixelDiff;
                //this._increment = 3;//future use
                break;
            case 'rgb_alpha' :
                this._parseChunk = this._rgbAlphaPixelDiff;
                //this._increment = 4;//future use
                break;
            default :
                throw Error(`Unsupported tupltype: ${chunk.tupltype}. Supported tupltypes include grayscale(gray), blackandwhite(monob), rgb(rgb24), and rgb_alpha(rgba).`);
        }
    }

    /**
     *
     * @param chunk
     * @param encoding
     * @param callback
     * @private
     */
    _transform(chunk, encoding, callback) {
        this._parseChunk(chunk);
        callback();
    }

    /**
     *
     * @param callback
     * @private
     */
    _flush(callback) {
        this.resetCache();
        callback();
    }
}

/**
 *
 * @type {PamDiff}
 */
module.exports = PamDiff;
//todo get bounding box of all regions combined to exclude some pixels before checking if they exist inside specific regions
