'use strict';

var d3 = require('@plotly/d3');
var map1dArray = require('../carpet/map_1d_array');
var makepath = require('../carpet/makepath');
var Drawing = require('../../components/drawing');
var Lib = require('../../lib');

var makeCrossings = require('../contour/make_crossings');
var findAllPaths = require('../contour/find_all_paths');
var contourPlot = require('../contour/plot');
var constants = require('../contour/constants');
var convertToConstraints = require('../contour/convert_to_constraints');
var emptyPathinfo = require('../contour/empty_pathinfo');
var closeBoundaries = require('../contour/close_boundaries');
var lookupCarpet = require('../carpet/lookup_carpetid');
var axisAlignedLine = require('../carpet/axis_aligned_line');

module.exports = function plot(gd, plotinfo, cdcontours, contourcarpetLayer) {
    var xa = plotinfo.xaxis;
    var ya = plotinfo.yaxis;

    Lib.makeTraceGroups(contourcarpetLayer, cdcontours, 'contour').each(function(cd) {
        var plotGroup = d3.select(this);
        var cd0 = cd[0];
        var trace = cd0.trace;

        var carpet = trace._carpetTrace = lookupCarpet(gd, trace);
        var carpetcd = gd.calcdata[carpet.index][0];

        if(!carpet.visible || carpet.visible === 'legendonly') return;

        var a = cd0.a;
        var b = cd0.b;
        var contours = trace.contours;
        var pathinfo = emptyPathinfo(contours, plotinfo, cd0);
        var isConstraint = contours.type === 'constraint';
        var operation = contours._operation;
        var coloring = isConstraint ? (operation === '=' ? 'lines' : 'fill') : contours.coloring;

        // Map [a, b] (data) --> [i, j] (pixels)
        function ab2p(ab) {
            var pt = carpet.ab2xy(ab[0], ab[1], true);
            return [xa.c2p(pt[0]), ya.c2p(pt[1])];
        }

        // Define the perimeter in a/b coordinates:
        var perimeter = [
            [a[0], b[b.length - 1]],
            [a[a.length - 1], b[b.length - 1]],
            [a[a.length - 1], b[0]],
            [a[0], b[0]]
        ];

        // Extract the contour levels:
        makeCrossings(pathinfo);
        var atol = (a[a.length - 1] - a[0]) * 1e-8;
        var btol = (b[b.length - 1] - b[0]) * 1e-8;
        findAllPaths(pathinfo, atol, btol);

        // Constraints might need to be draw inverted, which is not something contours
        // handle by default since they're assumed fully opaque so that they can be
        // drawn overlapping. This function flips the paths as necessary so that they're
        // drawn correctly.
        //
        // TODO: Perhaps this should be generalized and *all* paths should be drawn as
        // closed regions so that translucent contour levels would be valid.
        // See: https://github.com/plotly/plotly.js/issues/1356
        var fillPathinfo = pathinfo;
        if(contours.type === 'constraint') {
            fillPathinfo = convertToConstraints(pathinfo, operation);
        }

        // Map the paths in a/b coordinates to pixel coordinates:
        mapPathinfo(pathinfo, ab2p);

        // draw everything

        // Compute the boundary path
        var seg, xp, yp, i;
        var segs = [];
        for(i = carpetcd.clipsegments.length - 1; i >= 0; i--) {
            seg = carpetcd.clipsegments[i];
            xp = map1dArray([], seg.x, xa.c2p);
            yp = map1dArray([], seg.y, ya.c2p);
            xp.reverse();
            yp.reverse();
            segs.push(makepath(xp, yp, seg.bicubic));
        }

        var boundaryPath = 'M' + segs.join('L') + 'Z';

        // Draw the baseline background fill that fills in the space behind any other
        // contour levels:
        makeBackground(plotGroup, carpetcd.clipsegments, xa, ya, isConstraint, coloring);

        // Draw the specific contour fills. As a simplification, they're assumed to be
        // fully opaque so that it's easy to draw them simply overlapping. The alternative
        // would be to flip adjacent paths and draw closed paths for each level instead.
        makeFills(trace, plotGroup, xa, ya, fillPathinfo, perimeter, ab2p, carpet, carpetcd, coloring, boundaryPath);

        // Draw contour lines:
        makeLinesAndLabels(plotGroup, pathinfo, gd, cd0, contours, plotinfo, carpet);

        // Clip the boundary of the plot
        Drawing.setClipUrl(plotGroup, carpet._clipPathId, gd);
    });
};

function mapPathinfo(pathinfo, map) {
    var i, j, k, pi, pedgepaths, ppaths, pedgepath, ppath, path;

    for(i = 0; i < pathinfo.length; i++) {
        pi = pathinfo[i];
        pedgepaths = pi.pedgepaths = [];
        ppaths = pi.ppaths = [];
        for(j = 0; j < pi.edgepaths.length; j++) {
            path = pi.edgepaths[j];
            pedgepath = [];
            for(k = 0; k < path.length; k++) {
                pedgepath[k] = map(path[k]);
            }
            pedgepaths.push(pedgepath);
        }
        for(j = 0; j < pi.paths.length; j++) {
            path = pi.paths[j];
            ppath = [];
            for(k = 0; k < path.length; k++) {
                ppath[k] = map(path[k]);
            }
            ppaths.push(ppath);
        }
    }
}

function makeLinesAndLabels(plotgroup, pathinfo, gd, cd0, contours, plotinfo, carpet) {
    var isStatic = gd._context.staticPlot;
    var lineContainer = Lib.ensureSingle(plotgroup, 'g', 'contourlines');
    var showLines = contours.showlines !== false;
    var showLabels = contours.showlabels;
    var clipLinesForLabels = showLines && showLabels;

    // Even if we're not going to show lines, we need to create them
    // if we're showing labels, because the fill paths include the perimeter
    // so can't be used to position the labels correctly.
    // In this case we'll remove the lines after making the labels.
    var linegroup = contourPlot.createLines(lineContainer, showLines || showLabels, pathinfo, isStatic);

    var lineClip = contourPlot.createLineClip(lineContainer, clipLinesForLabels, gd, cd0.trace.uid);

    var labelGroup = plotgroup.selectAll('g.contourlabels')
        .data(showLabels ? [0] : []);

    labelGroup.exit().remove();

    labelGroup.enter().append('g')
        .classed('contourlabels', true);

    if(showLabels) {
        var xa = plotinfo.xaxis;
        var ya = plotinfo.yaxis;
        var xLen = xa._length;
        var yLen = ya._length;
        // for simplicity use the xy box for label clipping outline.
        var labelClipPathData = [[
            [0, 0],
            [xLen, 0],
            [xLen, yLen],
            [0, yLen]
        ]];


        var labelData = [];

        // invalidate the getTextLocation cache in case paths changed
        Lib.clearLocationCache();

        var contourFormat = contourPlot.labelFormatter(gd, cd0);

        var dummyText = Drawing.tester.append('text')
            .attr('data-notex', 1)
            .call(Drawing.font, contours.labelfont);

        // use `bounds` only to keep labels away from the x/y boundaries
        // `constrainToCarpet` below ensures labels don't go off the
        // carpet edges
        var bounds = {
            left: 0,
            right: xLen,
            center: xLen / 2,
            top: 0,
            bottom: yLen,
            middle: yLen / 2
        };

        var plotDiagonal = Math.sqrt(xLen * xLen + yLen * yLen);

        // the path length to use to scale the number of labels to draw:
        var normLength = constants.LABELDISTANCE * plotDiagonal /
            Math.max(1, pathinfo.length / constants.LABELINCREASE);

        linegroup.each(function(d) {
            var textOpts = contourPlot.calcTextOpts(d.level, contourFormat, dummyText, gd);

            d3.select(this).selectAll('path').each(function(pathData) {
                var path = this;
                var pathBounds = Lib.getVisibleSegment(path, bounds, textOpts.height / 2);
                if(!pathBounds) return;

                constrainToCarpet(path, pathData, d, pathBounds, carpet, textOpts.height);

                if(pathBounds.len < (textOpts.width + textOpts.height) * constants.LABELMIN) return;

                var maxLabels = Math.min(Math.ceil(pathBounds.len / normLength),
                    constants.LABELMAX);

                for(var i = 0; i < maxLabels; i++) {
                    var loc = contourPlot.findBestTextLocation(path, pathBounds, textOpts,
                        labelData, bounds);

                    if(!loc) break;

                    contourPlot.addLabelData(loc, textOpts, labelData, labelClipPathData);
                }
            });
        });

        dummyText.remove();

        contourPlot.drawLabels(labelGroup, labelData, gd, lineClip,
            clipLinesForLabels ? labelClipPathData : null);
    }

    if(showLabels && !showLines) linegroup.remove();
}

// figure out if this path goes off the edge of the carpet
// and shorten the part we call visible to keep labels away from the edge
function constrainToCarpet(path, pathData, levelData, pathBounds, carpet, textHeight) {
    var pathABData;
    for(var i = 0; i < levelData.pedgepaths.length; i++) {
        if(pathData === levelData.pedgepaths[i]) {
            pathABData = levelData.edgepaths[i];
        }
    }
    if(!pathABData) return;

    var aMin = carpet.a[0];
    var aMax = carpet.a[carpet.a.length - 1];
    var bMin = carpet.b[0];
    var bMax = carpet.b[carpet.b.length - 1];

    function getOffset(abPt, pathVector) {
        var offset = 0;
        var edgeVector;
        var dAB = 0.1;
        if(Math.abs(abPt[0] - aMin) < dAB || Math.abs(abPt[0] - aMax) < dAB) {
            edgeVector = normalizeVector(carpet.dxydb_rough(abPt[0], abPt[1], dAB));
            offset = Math.max(offset, textHeight * vectorTan(pathVector, edgeVector) / 2);
        }

        if(Math.abs(abPt[1] - bMin) < dAB || Math.abs(abPt[1] - bMax) < dAB) {
            edgeVector = normalizeVector(carpet.dxyda_rough(abPt[0], abPt[1], dAB));
            offset = Math.max(offset, textHeight * vectorTan(pathVector, edgeVector) / 2);
        }
        return offset;
    }

    var startVector = getUnitVector(path, 0, 1);
    var endVector = getUnitVector(path, pathBounds.total, pathBounds.total - 1);
    var minStart = getOffset(pathABData[0], startVector);
    var maxEnd = pathBounds.total - getOffset(pathABData[pathABData.length - 1], endVector);

    if(pathBounds.min < minStart) pathBounds.min = minStart;
    if(pathBounds.max > maxEnd) pathBounds.max = maxEnd;

    pathBounds.len = pathBounds.max - pathBounds.min;
}

function getUnitVector(path, p0, p1) {
    var pt0 = path.getPointAtLength(p0);
    var pt1 = path.getPointAtLength(p1);
    var dx = pt1.x - pt0.x;
    var dy = pt1.y - pt0.y;
    var len = Math.sqrt(dx * dx + dy * dy);
    return [dx / len, dy / len];
}

function normalizeVector(v) {
    var len = Math.sqrt(v[0] * v[0] + v[1] * v[1]);
    return [v[0] / len, v[1] / len];
}

function vectorTan(v0, v1) {
    var cos = Math.abs(v0[0] * v1[0] + v0[1] * v1[1]);
    var sin = Math.sqrt(1 - cos * cos);
    return sin / cos;
}

function makeBackground(plotgroup, clipsegments, xaxis, yaxis, isConstraint, coloring) {
    var seg, xp, yp, i;
    var bggroup = Lib.ensureSingle(plotgroup, 'g', 'contourbg');

    var bgfill = bggroup.selectAll('path')
        .data((coloring === 'fill' && !isConstraint) ? [0] : []);
    bgfill.enter().append('path');
    bgfill.exit().remove();

    var segs = [];
    for(i = 0; i < clipsegments.length; i++) {
        seg = clipsegments[i];
        xp = map1dArray([], seg.x, xaxis.c2p);
        yp = map1dArray([], seg.y, yaxis.c2p);
        segs.push(makepath(xp, yp, seg.bicubic));
    }

    bgfill
        .attr('d', 'M' + segs.join('L') + 'Z')
        .style('stroke', 'none');
}

function makeFills(trace, plotgroup, xa, ya, pathinfo, perimeter, ab2p, carpet, carpetcd, coloring, boundaryPath) {
    var hasFills = coloring === 'fill';

    // fills prefixBoundary in pathinfo items
    if(hasFills) {
        closeBoundaries(pathinfo, trace.contours);
    }

    var fillgroup = Lib.ensureSingle(plotgroup, 'g', 'contourfill');
    var fillitems = fillgroup.selectAll('path').data(hasFills ? pathinfo : []);
    fillitems.enter().append('path');
    fillitems.exit().remove();
    fillitems.each(function(pi) {
        // join all paths for this level together into a single path
        // first follow clockwise around the perimeter to close any open paths
        // if the whole perimeter is above this level, start with a path
        // enclosing the whole thing. With all that, the parity should mean
        // that we always fill everything above the contour, nothing below
        var fullpath = (pi.prefixBoundary ? boundaryPath : '') +
            joinAllPaths(trace, pi, perimeter, ab2p, carpet, carpetcd, xa, ya);

        if(!fullpath) {
            d3.select(this).remove();
        } else {
            d3.select(this)
                .attr('d', fullpath)
                .style('stroke', 'none');
        }
    });
}

function joinAllPaths(trace, pi, perimeter, ab2p, carpet, carpetcd, xa, ya) {
    var i;
    var fullpath = '';

    var startsleft = pi.edgepaths.map(function(v, i) { return i; });
    var newloop = true;
    var endpt, newendpt, cnt, nexti, possiblei, addpath;

    var atol = Math.abs(perimeter[0][0] - perimeter[2][0]) * 1e-4;
    var btol = Math.abs(perimeter[0][1] - perimeter[2][1]) * 1e-4;

    function istop(pt) { return Math.abs(pt[1] - perimeter[0][1]) < btol; }
    function isbottom(pt) { return Math.abs(pt[1] - perimeter[2][1]) < btol; }
    function isleft(pt) { return Math.abs(pt[0] - perimeter[0][0]) < atol; }
    function isright(pt) { return Math.abs(pt[0] - perimeter[2][0]) < atol; }

    function pathto(pt0, pt1) {
        var i, j, segments, axis;
        var path = '';

        if((istop(pt0) && !isright(pt0)) || (isbottom(pt0) && !isleft(pt0))) {
            axis = carpet.aaxis;
            segments = axisAlignedLine(carpet, carpetcd, [pt0[0], pt1[0]], 0.5 * (pt0[1] + pt1[1]));
        } else {
            axis = carpet.baxis;
            segments = axisAlignedLine(carpet, carpetcd, 0.5 * (pt0[0] + pt1[0]), [pt0[1], pt1[1]]);
        }

        for(i = 1; i < segments.length; i++) {
            path += axis.smoothing ? 'C' : 'L';
            for(j = 0; j < segments[i].length; j++) {
                var pt = segments[i][j];
                path += [xa.c2p(pt[0]), ya.c2p(pt[1])] + ' ';
            }
        }

        return path;
    }

    i = 0;
    endpt = null;
    while(startsleft.length) {
        var startpt = pi.edgepaths[i][0];

        if(endpt) {
            fullpath += pathto(endpt, startpt);
        }

        addpath = Drawing.smoothopen(pi.edgepaths[i].map(ab2p), pi.smoothing);
        fullpath += newloop ? addpath : addpath.replace(/^M/, 'L');
        startsleft.splice(startsleft.indexOf(i), 1);
        endpt = pi.edgepaths[i][pi.edgepaths[i].length - 1];
        nexti = -1;

        // now loop through sides, moving our endpoint until we find a new start
        for(cnt = 0; cnt < 4; cnt++) { // just to prevent infinite loops
            if(!endpt) {
                Lib.log('Missing end?', i, pi);
                break;
            }

            if(istop(endpt) && !isright(endpt)) {
                newendpt = perimeter[1]; // left top ---> right top
            } else if(isleft(endpt)) {
                newendpt = perimeter[0]; // left bottom ---> left top
            } else if(isbottom(endpt)) {
                newendpt = perimeter[3]; // right bottom
            } else if(isright(endpt)) {
                newendpt = perimeter[2]; // left bottom
            }

            for(possiblei = 0; possiblei < pi.edgepaths.length; possiblei++) {
                var ptNew = pi.edgepaths[possiblei][0];
                // is ptNew on the (horz. or vert.) segment from endpt to newendpt?
                if(Math.abs(endpt[0] - newendpt[0]) < atol) {
                    if(Math.abs(endpt[0] - ptNew[0]) < atol &&
                            (ptNew[1] - endpt[1]) * (newendpt[1] - ptNew[1]) >= 0) {
                        newendpt = ptNew;
                        nexti = possiblei;
                    }
                } else if(Math.abs(endpt[1] - newendpt[1]) < btol) {
                    if(Math.abs(endpt[1] - ptNew[1]) < btol &&
                            (ptNew[0] - endpt[0]) * (newendpt[0] - ptNew[0]) >= 0) {
                        newendpt = ptNew;
                        nexti = possiblei;
                    }
                } else {
                    Lib.log('endpt to newendpt is not vert. or horz.', endpt, newendpt, ptNew);
                }
            }

            if(nexti >= 0) break;
            fullpath += pathto(endpt, newendpt);
            endpt = newendpt;
        }

        if(nexti === pi.edgepaths.length) {
            Lib.log('unclosed perimeter path');
            break;
        }

        i = nexti;

        // if we closed back on a loop we already included,
        // close it and start a new loop
        newloop = (startsleft.indexOf(i) === -1);
        if(newloop) {
            i = startsleft[0];
            fullpath += pathto(endpt, newendpt) + 'Z';
            endpt = null;
        }
    }

    // finally add the interior paths
    for(i = 0; i < pi.paths.length; i++) {
        fullpath += Drawing.smoothclosed(pi.paths[i].map(ab2p), pi.smoothing);
    }

    return fullpath;
}
