// vim:ts=4:sw=4:expandtab

'use strict';

function plot(elementId, seriesList) {
    let plots = [];
    for (let i = 0; i < seriesList.length; i++) {
        let y = seriesList[i];
        let x = [];
        for (let i = 0; i < y.length; i++) {
            x.push(i);
        }
        plots.push({x: x, y: y});
    }
    Plotly.plot(document.getElementById(elementId), plots);
}

function removeColours(src, dst, threshold) {
    let from = src.data32S;
    let to = dst.data32S;
    let pixels = src.rows * src.cols;
    for (let i = 0; i < pixels; i++) {
        let pixel = from[i];
        let r = pixel & 0xFF;
        let g = (pixel >> 8) & 0xFF;
        let b = (pixel >> 16) & 0xFF;
        to[i] = from[i] | (Math.max(r, g, b) - Math.min(r, g, b) > threshold ? 0xFFFFFF : 0);
    }
}

function preprocessImage(src, dst, gaussianBlurSize, adaptiveThresholdBlockSize, adaptiveThresholdMeanAdjustment, numDilations) {
    cv.GaussianBlur(src, dst, new cv.Size(gaussianBlurSize, gaussianBlurSize), 0);
    cv.adaptiveThreshold(dst, dst, 255, cv.ADAPTIVE_THRESH_GAUSSIAN_C, cv.THRESH_BINARY_INV, adaptiveThresholdBlockSize, adaptiveThresholdMeanAdjustment);
    let kernel = cv.getStructuringElement(cv.MORPH_CROSS, new cv.Size(3, 3));
    try {
        cv.dilate(dst, dst, kernel, new cv.Point(-1, -1), numDilations);
    } finally {
        kernel.delete();
    }
}

function morphOpenImage(src, dst, kernelSize, iterations) {
    let kernel = cv.getStructuringElement(cv.MORPH_RECT, kernelSize);
    try {
        cv.morphologyEx(src, dst, cv.MORPH_OPEN, kernel, new cv.Point(-1, -1), iterations);
    } finally {
        kernel.delete();
    }
}

function findBiggestContour(img) {
    let contours = new cv.MatVector();
    try {
        let hierarchy = new cv.Mat();
        try {
            cv.findContours(img, contours, hierarchy, cv.RETR_EXTERNAL, cv.CHAIN_APPROX_SIMPLE);

            let biggest = null;
            let maxArea = 0;
            for (let i = 0; i < contours.size(); i++) {
                let contour = contours.get(i);
                let area = cv.contourArea(contour);
                if (area > maxArea) {
                    maxArea = area;
                    if (biggest !== null) {
                        biggest.delete();
                    }
                    biggest = contour;
                } else {
                    contour.delete();
                }
            }
            return biggest;
        } finally {
            hierarchy.delete();
        }
    } finally {
        contours.delete();
    }
}

function erodeContour(imageSize, contour, kernelSize, iterations) {
    let contourImg = cv.Mat.zeros(imageSize.height, imageSize.width, cv.CV_8U);
    try {
        let contours = new cv.MatVector();
        try {
            contours.push_back(contour);
            cv.drawContours(contourImg, contours, 0, new cv.Scalar(255), -1);
        } finally {
            contours.delete();
        }
        morphOpenImage(contourImg, contourImg, new cv.Size(kernelSize, kernelSize), iterations);
        return findBiggestContour(contourImg);
    } finally {
        contourImg.delete();
    }
}

function getContourCorners(imageSize, contour) {
    let topLeft = new cv.Point(imageSize.width, imageSize.height);
    let topRight = new cv.Point(-1, imageSize.height);
    let bottomLeft = new cv.Point(imageSize.width, -1);
    let bottomRight = new cv.Point(-1, -1);
    for (let i = 0; i < contour.rows; i++) {
        let vertex = new cv.Point(contour.data32S[i * 2], contour.data32S[i * 2 + 1]);
        let sum = vertex.x + vertex.y;
        let diff = vertex.x - vertex.y;
        if (sum < topLeft.x + topLeft.y) {
            topLeft = vertex;
        }
        if (sum > bottomRight.x + bottomRight.y) {
            bottomRight = vertex;
        }
        if (diff < bottomLeft.x - bottomLeft.y) {
            bottomLeft = vertex;
        }
        if (diff > topRight.x - topRight.y) {
            topRight = vertex;
        }
    }
    return [topLeft, topRight, bottomRight, bottomLeft];
}

function segmentLength(p1, p2) {
    let dx = p1.x - p2.x;
    let dy = p1.y - p2.y;
    return Math.sqrt(dx ** 2 + dy ** 2);
}

function getLongestSide(corners) {
    let previous = corners[corners.length - 1];
    let max = 0;
    for (let i = 0; i < corners.length; i++) {
        let current = corners[i];
        let length = segmentLength(previous, current);
        if (length > max) {
            max = length;
        }
        previous = current;
    }
    return max;
}

function extractSquare(img, corners) {
    let longest = getLongestSide(corners);
    let end = longest - 1;
    let sourceRect = cv.matFromArray(4, 1, cv.CV_32FC2, [corners[0].x, corners[0].y, corners[1].x, corners[1].y, corners[2].x, corners[2].y, corners[3].x, corners[3].y]);
    try {
        let destRect = cv.matFromArray(4, 1, cv.CV_32FC2, [0, 0, end, 0, end, end, 0, end]);
        try {
            let m = cv.getPerspectiveTransform(sourceRect, destRect);
            try {
                let destImg = new cv.Mat();
                try {
                    cv.warpPerspective(img, destImg, m, new cv.Size(longest, longest));
                    return destImg;
                } catch (err) {
                    destImg.delete();
                    throw err;
                }
            } finally {
                m.delete();
            }
        } finally {
            destRect.delete();
        }
    } finally {
        sourceRect.delete();
    }
}

function indexOfMax(arr) {
    return arr.reduce((iMax, x, i, arr) => x > arr[iMax] ? i : iMax, 0);
}

function getFundamentalFrequency(mag) {
    mag = mag.slice(0, Math.ceil(mag.length / 2));
    mag[0] = 0;
    return indexOfMax(mag);
}

function createMatVector(length) {
    let vec = new cv.MatVector();
    try {
        let mat = new cv.Mat();
        try {
            for (let i = 0; i < length; i++) {
                vec.push_back(mat);
            }
        } finally {
            mat.delete();
        }
        return vec;
    } catch (err) {
        vec.delete();
        throw err;
    }
}

function getLineFFT(img, lineDetectorElementSize, axis) {
    let lines = new cv.Mat();
    try {
        morphOpenImage(img, lines, axis === 1 ? new cv.Size(lineDetectorElementSize, 1) : new cv.Size(1, lineDetectorElementSize), 1);
        let sums = new cv.Mat();
        try {
            cv.reduce(lines, sums, axis, cv.REDUCE_SUM, cv.CV_32FC1);
            let fft = new cv.Mat();
            try {
                cv.dft(sums, fft, cv.DFT_COMPLEX_OUTPUT, 0);
                return fft;
            } catch (err) {
                fft.delete();
                throw err;
            }
        } finally {
            sums.delete();
        }
    } finally {
        lines.delete();
    }
}

function getFFTMagnitude(fft) {
    let planes = createMatVector(2);
    try {
        cv.split(fft, planes);
        let real = planes.get(0);
        try {
            let imag = planes.get(1);
            try {
                let ret = [];
                let length = Math.max(real.cols, real.rows);
                for (let i = 0; i < length; i++) {
                    ret.push(Math.sqrt(real.data32F[i] ** 2 + imag.data32F[i] ** 2))
                }
                return ret;
            } finally {
                imag.delete();
            }
        } finally {
            real.delete();
        }
    } finally {
        planes.delete();
    }
}

function getLineFrequency(img, lineDetectorElementSize, axis) {
    let fft = getLineFFT(img, lineDetectorElementSize, axis);
    try {
        return getFundamentalFrequency(getFFTMagnitude(fft));
    } finally {
        fft.delete();
    }
}

function extractGridColours(img, numRows, numCols, samplingBlockSizeRatio) {
    let imageSize = img.size();
    let rowOffset = Math.floor(imageSize.height * samplingBlockSizeRatio / numRows / 2);
    let rowHeight = 2 * rowOffset + 1;
    let colOffset = Math.floor(imageSize.width * samplingBlockSizeRatio / numCols / 2);
    let colWidth = 2 * colOffset + 1;
    let gridColours = [];
    for (let row = 0; row < numRows; row++) {
        let line = [];
        let y = Math.floor((row + 0.5) / numRows * imageSize.height) - rowOffset;
        for (let col = 0; col < numCols; col++) {
            let x = Math.floor((col + 0.5) / numCols * imageSize.width) - colOffset;
            let roi = img.roi(new cv.Rect(x, y, colWidth, rowHeight));
            try {
                line.push(cv.mean(roi)[0]);
            } finally {
                roi.delete();
            }
        }
        gridColours.push(line);
    }
    return gridColours;
}

function getGridColourThreshold(gridColours) {
    let colours = gridColours.reduce(function(acc, x) { return acc.concat(x); }, []).sort(function (a, b) { return a - b; });
    let deltaMax = 0;
    let iMax;
    for (let i = 0; i < colours.length; i++) {
        let delta = colours[i] - colours[i - 1];
        if (delta > deltaMax) {
            deltaMax = delta;
            iMax = i;
        }
    }
    return (colours[iMax] + colours[iMax - 1]) / 2;
}

function gridColoursToBlocks(gridColours, numRows, numCols, samplingThreshold) {
    let blocks = JSON.parse(JSON.stringify(gridColours));
    let warning = false;
    let midpoint = Math.floor(numRows / 2) + (numRows % 2 > 0 ? 1 : 0);
    for (let row = 0; row < midpoint; row++) {
        for (let col = 0; col < numCols; col++) {
            // If there is an odd number of rows then row and row2 will point to
            // the same row when we reach the middle. Doesn't seem worth adding a
            // special case.
            let row2 = numRows - row - 1;
            let col2 = numCols - col - 1;
            let delta1 = gridColours[row][col] - samplingThreshold;
            let delta2 = gridColours[row2][col2] - samplingThreshold;
            let filled;
            if ((delta1 > 0) && (delta2 > 0)) {
                filled = false;
            } else if ((delta1 < 0) && (delta2 < 0)) {
                filled = true;
            } else {
                warning = true;
                if (Math.abs(delta1) > Math.abs(delta2)) {
                    filled = delta1 < 0;
                } else {
                    filled = delta2 < 0;
                }
            }
            blocks[row][col] = {filled: filled}
            blocks[row2][col2] = {filled: filled}
        }
    }

    let number = 1
    for (let row = 0; row < numRows; row++) {
        for (let col = 0; col < numCols; col++) {
            if (! blocks[row][col].filled && (
                (((col == 0) || blocks[row][col - 1].filled) && (col < numCols - 1) && ! blocks[row][col + 1].filled) ||
                (((row == 0) || blocks[row - 1][col].filled) && (row < numRows - 1) && ! blocks[row + 1][col].filled)
            )) {
                blocks[row][col].number = number
                number += 1
            }
        }
    }

    return {warning: warning, blocks: blocks};
}

function drawGrid(canvas, blocks, numRows, numCols, gridLineThickness, gridSquareSize, gridBorderSize) {
    let step = gridSquareSize + gridLineThickness;
    let gridHeight = numRows * step + gridLineThickness;
    let gridWidth = numCols * step + gridLineThickness;
    canvas.width = 2 * gridBorderSize + gridWidth;
    canvas.height = 2 * gridBorderSize + gridHeight;
    let context = canvas.getContext('2d');
    context.fillStyle = 'black';
    context.fillRect(gridBorderSize, gridBorderSize, gridWidth, gridHeight);
    context.fillStyle = 'white';
    for (let row = 0; row < numRows; row++) {
        let y = row * step + gridLineThickness + gridBorderSize;
        for (let col = 0; col < numCols; col++) {
            if (! blocks[row][col].filled) {
                let x = col * step + gridLineThickness + gridBorderSize;
                context.fillRect(x, y, gridSquareSize, gridSquareSize);
            }
        }
    }
}
