'use strict';

var d3 = require('@plotly/d3');
var tinycolor = require('tinycolor2');

var Registry = require('../../registry');
var Drawing = require('../../components/drawing');
var Axes = require('../../plots/cartesian/axes');
var Lib = require('../../lib');
var svgTextUtils = require('../../lib/svg_text_utils');
var formatLabels = require('../scatter/format_labels');
var Color = require('../../components/color');
var extractOpts = require('../../components/colorscale').extractOpts;
var makeColorScaleFuncFromTrace = require('../../components/colorscale').makeColorScaleFuncFromTrace;
var xmlnsNamespaces = require('../../constants/xmlns_namespaces');
var alignmentConstants = require('../../constants/alignment');
var LINE_SPACING = alignmentConstants.LINE_SPACING;
var supportsPixelatedImage = require('../../lib/supports_pixelated_image');
var PIXELATED_IMAGE_STYLE = require('../../constants/pixelated_image').STYLE;

var labelClass = 'heatmap-label';

function selectLabels(plotGroup) {
    return plotGroup.selectAll('g.' + labelClass);
}

function removeLabels(plotGroup) {
    selectLabels(plotGroup).remove();
}

module.exports = function(gd, plotinfo, cdheatmaps, heatmapLayer) {
    var xa = plotinfo.xaxis;
    var ya = plotinfo.yaxis;

    Lib.makeTraceGroups(heatmapLayer, cdheatmaps, 'hm').each(function(cd) {
        var plotGroup = d3.select(this);
        var cd0 = cd[0];
        var trace = cd0.trace;
        var xGap = trace.xgap || 0;
        var yGap = trace.ygap || 0;

        var z = cd0.z;
        var x = cd0.x;
        var y = cd0.y;
        var xc = cd0.xCenter;
        var yc = cd0.yCenter;
        var isContour = Registry.traceIs(trace, 'contour');
        var zsmooth = isContour ? 'best' : trace.zsmooth;

        // get z dims
        var m = z.length;
        var n = Lib.maxRowLength(z);
        var xrev = false;
        var yrev = false;

        var left, right, temp, top, bottom, i, j, k;

        // TODO: if there are multiple overlapping categorical heatmaps,
        // or if we allow category sorting, then the categories may not be
        // sequential... may need to reorder and/or expand z

        // Get edges of png in pixels (xa.c2p() maps axes coordinates to pixel coordinates)
        // figure out if either axis is reversed (y is usually reversed, in pixel coords)
        // also clip the image to maximum 50% outside the visible plot area
        // bigger image lets you pan more naturally, but slows performance.
        // TODO: use low-resolution images outside the visible plot for panning
        // these while loops find the first and last brick bounds that are defined
        // (in case of log of a negative)
        i = 0;
        while(left === undefined && i < x.length - 1) {
            left = xa.c2p(x[i]);
            i++;
        }
        i = x.length - 1;
        while(right === undefined && i > 0) {
            right = xa.c2p(x[i]);
            i--;
        }

        if(right < left) {
            temp = right;
            right = left;
            left = temp;
            xrev = true;
        }

        i = 0;
        while(top === undefined && i < y.length - 1) {
            top = ya.c2p(y[i]);
            i++;
        }
        i = y.length - 1;
        while(bottom === undefined && i > 0) {
            bottom = ya.c2p(y[i]);
            i--;
        }

        if(bottom < top) {
            temp = top;
            top = bottom;
            bottom = temp;
            yrev = true;
        }

        // for contours with heatmap fill, we generate the boundaries based on
        // brick centers but then use the brick edges for drawing the bricks
        if(isContour) {
            xc = x;
            yc = y;
            x = cd0.xfill;
            y = cd0.yfill;
        }

        var drawingMethod = 'default';
        if(zsmooth) {
            drawingMethod = zsmooth === 'best' ? 'smooth' : 'fast';
        } else if(trace._islinear && xGap === 0 && yGap === 0 && supportsPixelatedImage()) {
            drawingMethod = 'fast';
        }

        // make an image that goes at most half a screen off either side, to keep
        // time reasonable when you zoom in. if drawingMethod is fast, don't worry
        // about this, because zooming doesn't increase number of pixels
        // if zsmooth is best, don't include anything off screen because it takes too long
        if(drawingMethod !== 'fast') {
            var extra = zsmooth === 'best' ? 0 : 0.5;
            left = Math.max(-extra * xa._length, left);
            right = Math.min((1 + extra) * xa._length, right);
            top = Math.max(-extra * ya._length, top);
            bottom = Math.min((1 + extra) * ya._length, bottom);
        }

        var imageWidth = Math.round(right - left);
        var imageHeight = Math.round(bottom - top);

        // setup image nodes

        // if image is entirely off-screen, don't even draw it
        var isOffScreen = (
            left >= xa._length || right <= 0 || top >= ya._length || bottom <= 0
        );

        if(isOffScreen) {
            var noImage = plotGroup.selectAll('image').data([]);
            noImage.exit().remove();

            removeLabels(plotGroup);
            return;
        }

        // generate image data

        var canvasW, canvasH;
        if(drawingMethod === 'fast') {
            canvasW = n;
            canvasH = m;
        } else {
            canvasW = imageWidth;
            canvasH = imageHeight;
        }

        var canvas = document.createElement('canvas');
        canvas.width = canvasW;
        canvas.height = canvasH;
        var context = canvas.getContext('2d', {willReadFrequently: true});

        var sclFunc = makeColorScaleFuncFromTrace(trace, {noNumericCheck: true, returnArray: true});

        // map brick boundaries to image pixels
        var xpx,
            ypx;
        if(drawingMethod === 'fast') {
            xpx = xrev ?
                function(index) { return n - 1 - index; } :
                Lib.identity;
            ypx = yrev ?
                function(index) { return m - 1 - index; } :
                Lib.identity;
        } else {
            xpx = function(index) {
                return Lib.constrain(Math.round(xa.c2p(x[index]) - left),
                    0, imageWidth);
            };
            ypx = function(index) {
                return Lib.constrain(Math.round(ya.c2p(y[index]) - top),
                    0, imageHeight);
            };
        }

        // build the pixel map brick-by-brick
        // cruise through z-matrix row-by-row
        // build a brick at each z-matrix value
        var yi = ypx(0);
        var yb = [yi, yi];
        var xbi = xrev ? 0 : 1;
        var ybi = yrev ? 0 : 1;
        // for collecting an average luminosity of the heatmap
        var pixcount = 0;
        var rcount = 0;
        var gcount = 0;
        var bcount = 0;

        var xb, xi, v, row, c;

        function setColor(v, pixsize) {
            if(v !== undefined) {
                var c = sclFunc(v);
                c[0] = Math.round(c[0]);
                c[1] = Math.round(c[1]);
                c[2] = Math.round(c[2]);

                pixcount += pixsize;
                rcount += c[0] * pixsize;
                gcount += c[1] * pixsize;
                bcount += c[2] * pixsize;
                return c;
            }
            return [0, 0, 0, 0];
        }

        function interpColor(r0, r1, xinterp, yinterp) {
            var z00 = r0[xinterp.bin0];
            if(z00 === undefined) return setColor(undefined, 1);

            var z01 = r0[xinterp.bin1];
            var z10 = r1[xinterp.bin0];
            var z11 = r1[xinterp.bin1];
            var dx = (z01 - z00) || 0;
            var dy = (z10 - z00) || 0;
            var dxy;

            // the bilinear interpolation term needs different calculations
            // for all the different permutations of missing data
            // among the neighbors of the main point, to ensure
            // continuity across brick boundaries.
            if(z01 === undefined) {
                if(z11 === undefined) dxy = 0;
                else if(z10 === undefined) dxy = 2 * (z11 - z00);
                else dxy = (2 * z11 - z10 - z00) * 2 / 3;
            } else if(z11 === undefined) {
                if(z10 === undefined) dxy = 0;
                else dxy = (2 * z00 - z01 - z10) * 2 / 3;
            } else if(z10 === undefined) dxy = (2 * z11 - z01 - z00) * 2 / 3;
            else dxy = (z11 + z00 - z01 - z10);

            return setColor(z00 + xinterp.frac * dx + yinterp.frac * (dy + xinterp.frac * dxy));
        }

        if(drawingMethod !== 'default') { // works fastest with imageData
            var pxIndex = 0;
            var pixels;

            try {
                pixels = new Uint8Array(canvasW * canvasH * 4);
            } catch(e) {
                pixels = new Array(canvasW * canvasH * 4);
            }

            if(drawingMethod === 'smooth') { // zsmooth="best"
                var xForPx = xc || x;
                var yForPx = yc || y;
                var xPixArray = new Array(xForPx.length);
                var yPixArray = new Array(yForPx.length);
                var xinterpArray = new Array(imageWidth);
                var findInterpX = xc ? findInterpFromCenters : findInterp;
                var findInterpY = yc ? findInterpFromCenters : findInterp;
                var yinterp, r0, r1;

                // first make arrays of x and y pixel locations of brick boundaries
                for(i = 0; i < xForPx.length; i++) xPixArray[i] = Math.round(xa.c2p(xForPx[i]) - left);
                for(i = 0; i < yForPx.length; i++) yPixArray[i] = Math.round(ya.c2p(yForPx[i]) - top);

                // then make arrays of interpolations
                // (bin0=closest, bin1=next, frac=fractional dist.)
                for(i = 0; i < imageWidth; i++) xinterpArray[i] = findInterpX(i, xPixArray);

                // now do the interpolations and fill the png
                for(j = 0; j < imageHeight; j++) {
                    yinterp = findInterpY(j, yPixArray);
                    r0 = z[yinterp.bin0];
                    r1 = z[yinterp.bin1];
                    for(i = 0; i < imageWidth; i++, pxIndex += 4) {
                        c = interpColor(r0, r1, xinterpArray[i], yinterp);
                        putColor(pixels, pxIndex, c);
                    }
                }
            } else { // drawingMethod = "fast" (zsmooth = "fast"|false)
                for(j = 0; j < m; j++) {
                    row = z[j];
                    yb = ypx(j);
                    for(i = 0; i < n; i++) {
                        c = setColor(row[i], 1);
                        pxIndex = (yb * n + xpx(i)) * 4;
                        putColor(pixels, pxIndex, c);
                    }
                }
            }

            var imageData = context.createImageData(canvasW, canvasH);
            try {
                imageData.data.set(pixels);
            } catch(e) {
                var pxArray = imageData.data;
                var dlen = pxArray.length;
                for(j = 0; j < dlen; j ++) {
                    pxArray[j] = pixels[j];
                }
            }

            context.putImageData(imageData, 0, 0);
        } else { // rawingMethod = "default" (zsmooth = false)
            // filling potentially large bricks works fastest with fillRect
            // gaps do not need to be exact integers, but if they *are* we will get
            // cleaner edges by rounding at least one edge
            var xGapLeft = Math.floor(xGap / 2);
            var yGapTop = Math.floor(yGap / 2);

            for(j = 0; j < m; j++) {
                row = z[j];
                yb.reverse();
                yb[ybi] = ypx(j + 1);
                if(yb[0] === yb[1] || yb[0] === undefined || yb[1] === undefined) {
                    continue;
                }
                xi = xpx(0);
                xb = [xi, xi];
                for(i = 0; i < n; i++) {
                    // build one color brick!
                    xb.reverse();
                    xb[xbi] = xpx(i + 1);
                    if(xb[0] === xb[1] || xb[0] === undefined || xb[1] === undefined) {
                        continue;
                    }
                    v = row[i];
                    c = setColor(v, (xb[1] - xb[0]) * (yb[1] - yb[0]));
                    context.fillStyle = 'rgba(' + c.join(',') + ')';

                    context.fillRect(xb[0] + xGapLeft, yb[0] + yGapTop,
                        xb[1] - xb[0] - xGap, yb[1] - yb[0] - yGap);
                }
            }
        }

        rcount = Math.round(rcount / pixcount);
        gcount = Math.round(gcount / pixcount);
        bcount = Math.round(bcount / pixcount);
        var avgColor = tinycolor('rgb(' + rcount + ',' + gcount + ',' + bcount + ')');

        gd._hmpixcount = (gd._hmpixcount||0) + pixcount;
        gd._hmlumcount = (gd._hmlumcount||0) + pixcount * avgColor.getLuminance();

        var image3 = plotGroup.selectAll('image')
            .data(cd);

        image3.enter().append('svg:image').attr({
            xmlns: xmlnsNamespaces.svg,
            preserveAspectRatio: 'none'
        });

        image3.attr({
            height: imageHeight,
            width: imageWidth,
            x: left,
            y: top,
            'xlink:href': canvas.toDataURL('image/png')
        });

        if(drawingMethod === 'fast' && !zsmooth) {
            image3.attr('style', PIXELATED_IMAGE_STYLE);
        }

        removeLabels(plotGroup);

        var texttemplate = trace.texttemplate;
        if(texttemplate) {
            // dummy axis for formatting the z value
            var cOpts = extractOpts(trace);
            var dummyAx = {
                type: 'linear',
                range: [cOpts.min, cOpts.max],
                _separators: xa._separators,
                _numFormat: xa._numFormat
            };

            var aHistogram2dContour = trace.type === 'histogram2dcontour';
            var aContour = trace.type === 'contour';
            var iStart = aContour ? 1 : 0;
            var iStop = aContour ? m - 1 : m;
            var jStart = aContour ? 1 : 0;
            var jStop = aContour ? n - 1 : n;

            var textData = [];
            for(i = iStart; i < iStop; i++) {
                var yVal;
                if(aContour) {
                    yVal = cd0.y[i];
                } else if(aHistogram2dContour) {
                    if(i === 0 || i === m - 1) continue;
                    yVal = cd0.y[i];
                } else if(cd0.yCenter) {
                    yVal = cd0.yCenter[i];
                } else {
                    if(i + 1 === m && cd0.y[i + 1] === undefined) continue;
                    yVal = (cd0.y[i] + cd0.y[i + 1]) / 2;
                }

                var _y = Math.round(ya.c2p(yVal));
                if(0 > _y || _y > ya._length) continue;

                for(j = jStart; j < jStop; j++) {
                    var xVal;
                    if(aContour) {
                        xVal = cd0.x[j];
                    } else if(aHistogram2dContour) {
                        if(j === 0 || j === n - 1) continue;
                        xVal = cd0.x[j];
                    } else if(cd0.xCenter) {
                        xVal = cd0.xCenter[j];
                    } else {
                        if(j + 1 === n && cd0.x[j + 1] === undefined) continue;
                        xVal = (cd0.x[j] + cd0.x[j + 1]) / 2;
                    }

                    var _x = Math.round(xa.c2p(xVal));
                    if(0 > _x || _x > xa._length) continue;

                    var obj = formatLabels({
                        x: xVal,
                        y: yVal
                    }, trace, gd._fullLayout);

                    obj.x = xVal;
                    obj.y = yVal;

                    var zVal = cd0.z[i][j];
                    if(zVal === undefined) {
                        obj.z = '';
                        obj.zLabel = '';
                    } else {
                        obj.z = zVal;
                        obj.zLabel = Axes.tickText(dummyAx, zVal, 'hover').text;
                    }

                    var theText = cd0.text && cd0.text[i] && cd0.text[i][j];
                    if(theText === undefined || theText === false) theText = '';
                    obj.text = theText;

                    var _t = Lib.texttemplateString(texttemplate, obj, gd._fullLayout._d3locale, obj, trace._meta || {});
                    if(!_t) continue;

                    var lines = _t.split('<br>');
                    var nL = lines.length;
                    var nC = 0;
                    for(k = 0; k < nL; k++) {
                        nC = Math.max(nC, lines[k].length);
                    }

                    textData.push({
                        l: nL, // number of lines
                        c: nC, // maximum number of chars in a line
                        t: _t, // text
                        x: _x,
                        y: _y,
                        z: zVal
                    });
                }
            }

            var font = trace.textfont;
            var fontSize = font.size;
            var globalFontSize = gd._fullLayout.font.size;

            if(!fontSize || fontSize === 'auto') {
                var minW = Infinity;
                var minH = Infinity;
                var maxL = 0;
                var maxC = 0;

                for(k = 0; k < textData.length; k++) {
                    var d = textData[k];
                    maxL = Math.max(maxL, d.l);
                    maxC = Math.max(maxC, d.c);

                    if(k < textData.length - 1) {
                        var nextD = textData[k + 1];
                        var dx = Math.abs(nextD.x - d.x);
                        var dy = Math.abs(nextD.y - d.y);

                        if(dx) minW = Math.min(minW, dx);
                        if(dy) minH = Math.min(minH, dy);
                    }
                }

                if(
                    !isFinite(minW) ||
                    !isFinite(minH)
                ) {
                    fontSize = globalFontSize;
                } else {
                    minW -= xGap;
                    minH -= yGap;

                    minW /= maxC;
                    minH /= maxL;

                    minW /= LINE_SPACING / 2;
                    minH /= LINE_SPACING;

                    fontSize = Math.min(
                        Math.floor(minW),
                        Math.floor(minH),
                        globalFontSize
                    );
                }
            }
            if(fontSize <= 0 || !isFinite(fontSize)) return;

            var xFn = function(d) { return d.x; };
            var yFn = function(d) {
                return d.y - fontSize * ((d.l * LINE_SPACING) / 2 - 1);
            };

            var labels = selectLabels(plotGroup).data(textData);

            labels
                .enter()
                .append('g')
                .classed(labelClass, 1)
                .append('text')
                .attr('text-anchor', 'middle')
                .each(function(d) {
                    var thisLabel = d3.select(this);

                    var fontColor = font.color;
                    if(!fontColor || fontColor === 'auto') {
                        fontColor = Color.contrast(
                            d.z === undefined ? gd._fullLayout.plot_bgcolor :
                            'rgba(' +
                                sclFunc(d.z).join() +
                            ')'
                        );
                    }

                    thisLabel
                        .attr('data-notex', 1)
                        .call(svgTextUtils.positionText, xFn(d), yFn(d))
                        .call(Drawing.font, {
                            family: font.family,
                            size: fontSize,
                            color: fontColor,
                            weight: font.weight,
                            style: font.style,
                            variant: font.variant,
                            textcase: font.textcase,
                            lineposition: font.lineposition,
                            shadow: font.shadow,
                        })
                        .text(d.t)
                        .call(svgTextUtils.convertToTspans, gd);
                });
        }
    });
};

// get interpolated bin value. Returns {bin0:closest bin, frac:fractional dist to next, bin1:next bin}
function findInterp(pixel, pixArray) {
    var maxBin = pixArray.length - 2;
    var bin = Lib.constrain(Lib.findBin(pixel, pixArray), 0, maxBin);
    var pix0 = pixArray[bin];
    var pix1 = pixArray[bin + 1];
    var interp = Lib.constrain(bin + (pixel - pix0) / (pix1 - pix0) - 0.5, 0, maxBin);
    var bin0 = Math.round(interp);
    var frac = Math.abs(interp - bin0);

    if(!interp || interp === maxBin || !frac) {
        return {
            bin0: bin0,
            bin1: bin0,
            frac: 0
        };
    }
    return {
        bin0: bin0,
        frac: frac,
        bin1: Math.round(bin0 + frac / (interp - bin0))
    };
}

function findInterpFromCenters(pixel, centerPixArray) {
    var maxBin = centerPixArray.length - 1;
    var bin = Lib.constrain(Lib.findBin(pixel, centerPixArray), 0, maxBin);
    var pix0 = centerPixArray[bin];
    var pix1 = centerPixArray[bin + 1];
    var frac = ((pixel - pix0) / (pix1 - pix0)) || 0;
    if(frac <= 0) {
        return {
            bin0: bin,
            bin1: bin,
            frac: 0
        };
    }
    if(frac < 0.5) {
        return {
            bin0: bin,
            bin1: bin + 1,
            frac: frac
        };
    }
    return {
        bin0: bin + 1,
        bin1: bin,
        frac: 1 - frac
    };
}

function putColor(pixels, pxIndex, c) {
    pixels[pxIndex] = c[0];
    pixels[pxIndex + 1] = c[1];
    pixels[pxIndex + 2] = c[2];
    pixels[pxIndex + 3] = Math.round(c[3] * 255);
}
