/*
   Initializes a zoomable scatter plot in the element "node"
   node = , for example, "#chart_div"
*/
function ColorScatter(node) {
    this.node = $(node).get(0);
    this.firstPlot = true;
    this.resize = _.debounce(this._resize, 300);
    this.title = "";
    this.proj_keyX = ["", ""];
    this.proj_keyY = ["", ""];
    this.initialZoom = null;
    this.currentZoom = null;
    this.n_points = null;

    /*
     * This one is weird.  So when you double-click, plotly also
     * fires the click event, but fires it after the double-click on
     * some browsers (Chrome).  This causes the new point to be
     * selected where you double-clicked.  Apparently a known issue in Github.
     * To work around, we disable single-click events for a small
     * interval after a doubleclick using this variable
     */
    this.clickMask = false;

    this.isfactor = true;
    this.diverging_colormap = true;
    this.full_color_range = true;
}

/*
 * points: array of {'x', 'y', 'value', 'label', 'selected'}
 * tree_points: array of [x, y]
 * tree_adj: array of [point1, points2] indices into tree_points
 *
 */
ColorScatter.prototype.setData = function (object) {
    var points = object["points"];
    var isFactor =
        object["isFactor"] === undefined ? false : object["isFactor"];
    var full_color_range =
        object["full_color_range"] === undefined
            ? false
            : object["full_color_range"];
    var diverging_colormap =
        object["diverging_colormap"] === undefined
            ? true
            : object["diverging_colormap"];
    var autoZoom = object["autozoom"];

    this.title = object["title"];
    this.proj_keyX = object["proj_keyX"];
    this.proj_keyY = object["proj_keyY"];
    this.n_points = points.length;

    this.isfactor = isFactor;
    this.full_color_range = full_color_range;
    this.diverging_colormap = diverging_colormap;

    var x = _.map(points, (p) => p["x"]);
    var xmin = _.min(x);
    var xmax = _.max(x);
    var xrange = xmax - xmin;
    xmin = xmin - xrange * 0.05;
    xmax = xmax + xrange * 0.05;

    var y = _.map(points, (p) => p["y"]);
    var ymin = _.min(y);
    var ymax = _.max(y);
    var yrange = ymax - ymin;
    ymin = ymin - yrange * 0.05;
    ymax = ymax + yrange * 0.05;

    this.initialZoom = {
        xmin: xmin,
        xmax: xmax,
        ymin: ymin,
        ymax: ymax,
    };

    if (autoZoom || this.currentZoom === null) {
        this.currentZoom = {
            xmin: xmin,
            xmax: xmax,
            ymin: ymin,
            ymax: ymax,
        };
    }

    var circle_radius = this.pointsToRadius();

    var data = []; // Holds plotly traces

    var showlegend = false;
    if (isFactor) {
        var c = _.map(points, (p) => p["value"]);
        var unique = d3.set(c).values().sort();

        // Need to check if any is selected.  If no, then all selected_points is null
        var anySelected = _(points)
            .map("selected")
            .reduce((a, i) => a || i, false);

        _.forEach(unique, (level, i) => {
            var subset = _.filter(points, (p) => p["value"] == level);
            var x_sub = _.map(subset, (p) => p["x"]);
            var y_sub = _.map(subset, (p) => p["y"]);
            var id_sub = _.map(subset, (p) => p["label"]);

            var selected_points;
            if (anySelected) {
                selected_points = _(subset)
                    .map((p, i) => {
                        return { selected: p.selected, index: i };
                    })
                    .filter((p) => p.selected)
                    .map((p) => p.index)
                    .value();
            } else {
                selected_points = null;
            }

            var marker = {
                size: circle_radius,
            };
            if (unique.length > 10) {
                // Use husl if more than 10 categories
                var step = 8;
                var L = Math.ceil(unique.length / step) * step;
                var hue = Math.round(
                    (((((i * L) / step) % L) + Math.floor(i / step)) / L) * 360, // Helps scatter values
                );
                marker["color"] = hsluv.hsluvToHex([hue, 90, 55]);
            }

            var trace = {
                x: x_sub,
                y: y_sub,
                mode: "markers",
                type: "scattergl",
                text: id_sub,
                name: level.toString(),
                marker: marker,
                selectedpoints: selected_points,
                hoverinfo: "text+name",
            };
            data.push(trace);
        });

        showlegend = true;
    } else {
        var x = _.map(points, (p) => p["x"]);
        var y = _.map(points, (p) => p["y"]);
        var c = _.map(points, (p) => p["value"]);
        var id = _.map(points, (p) => p["label"]);

        var cvals = c.filter((cv) => cv !== "NA");
        cvals.sort(d3.ascending); // Needed for quantile
        var low, high;
        if (full_color_range) {
            low = d3.min(cvals);
            high = d3.max(cvals);
        } else {
            low = d3.quantile(cvals, 0.02);
            high = d3.quantile(cvals, 0.98);
        }

        var colorscale;
        if (diverging_colormap) {
            colorscale = "Viridis";
        } else {
            colorscale = [
                [0, "#d8d8d8"],
                [1, "#952E25"],
            ];
        }

        var selected_points = _(points)
            .map((p, i) => {
                return { selected: p.selected, index: i };
            })
            .filter((p) => p.selected)
            .map((p) => p.index)
            .value();

        if (selected_points.length == 0) selected_points = null;

        var trace1 = {
            x: x,
            y: y,
            mode: "markers",
            type: "scattergl",
            text: id,
            marker: {
                size: circle_radius,
                color: c,
                cmin: low,
                cmax: high,
                showscale: true,
                colorbar: {
                    titleside: "right", // default is 'top'
                    thickness: 15, // pixels
                    len: 0.5, //fraction of plot
                },
                colorscale: colorscale,
            },
            selectedpoints: selected_points,
            hoverinfo: "text",
        };

        data.push(trace1);
    }

    var shapes = [];

    if ("tree_points" in object) {
        _.forEach(object["tree_points"], (tp) => {
            var x = tp[0];
            var y = tp[1];
            var r = 5;
            shapes.push({
                type: "circle",
                xref: "x",
                yref: "y",
                xsizemode: "pixel",
                ysizemode: "pixel",
                xanchor: x,
                yanchor: y,
                x0: -1 * r,
                x1: r,
                y0: -1 * r,
                y1: r,
                fillcolor: "#05ff65",
                opacity: 0.5,
                line: {
                    width: 1,
                },
            });
        });

        _.forEach(object["tree_adj"], (tp) => {
            var x0 = object["tree_points"][tp[0]][0];
            var y0 = object["tree_points"][tp[0]][1];
            var x1 = object["tree_points"][tp[1]][0];
            var y1 = object["tree_points"][tp[1]][1];
            shapes.push({
                type: "line",
                xref: "x",
                yref: "y",
                x0: x0,
                x1: x1,
                y0: y0,
                y1: y1,
                line: {
                    color: "#05ff65",
                    width: 2,
                },
                opacity: 0.5,
            });
        });
    }

    var layout = this.getLayout();

    layout["shapes"] = shapes;
    layout["showlegend"] = showlegend;

    var options = this.getOptions();

    if (this.firstPlot || this.plotlyBug(data)) {
        Plotly.newPlot(this.node, data, layout, options);
        this.createListeners();
    } else {
        Plotly.react(this.node, data, layout, options);
    }

    this.firstPlot = false;
};

ColorScatter.prototype.createListeners = function () {
    var self = this;

    this.node.on("plotly_click", function (data) {
        if (self.clickMask) {
            return;
        }

        var point = data.points[0];
        var cellId = point.text;

        var event = new CustomEvent("select-cells", {
            detail: { cells: [cellId] },
        });
        window.dispatchEvent(event);
    });

    this.node.on("plotly_doubleclick", function () {
        self.clickMask = true;
        window.setTimeout(function () {
            self.clickMask = false;
        }, 500);

        var newLayout = {
            "xaxis.range[0]": self.initialZoom.xmin,
            "xaxis.range[1]": self.initialZoom.xmax,
            "yaxis.range[0]": self.initialZoom.ymin,
            "yaxis.range[1]": self.initialZoom.ymax,
        };

        self.relayout(newLayout);

        var event = new CustomEvent("select-cells", {
            detail: { cells: [] },
        });
        window.dispatchEvent(event);
    });

    this.node.on("plotly_selected", function (eventData) {
        var cellIds;
        if (eventData === undefined) {
            cellIds = [];
        } else {
            cellIds = _.map(eventData.points, (d) => {
                return d.data.text[d.pointNumber];
            });
        }
        var event = new CustomEvent("select-cells", {
            detail: { cells: cellIds },
        });
        window.dispatchEvent(event);
    });

    this.node.on("plotly_legendclick", function (data) {
        var selectedPoints = data.fullData[data.curveNumber].text;
        var name = data.fullData[data.curveNumber].name;

        var event = new CustomEvent("select-cells", {
            detail: { cells: selectedPoints, name: name },
        });
        window.dispatchEvent(event);
        return false;
    });

    this.node.on("plotly_legenddoubleclick", function () {
        return false;
    });

    this.node.on("plotly_relayout", function (newLayout) {
        if (_.isEmpty(newLayout)) {
            return;
        }

        var fireEvent = false;
        var needsMarkerScale = false;

        if (_.isEqual(newLayout, { autosize: true })) needsMarkerScale = true;

        if ("xaxis.range[0]" in newLayout) {
            self.currentZoom.xmin = newLayout["xaxis.range[0]"];
            fireEvent = true;
            needsMarkerScale = true;
        }

        if ("xaxis.range[1]" in newLayout) {
            self.currentZoom.xmax = newLayout["xaxis.range[1]"];
            fireEvent = true;
            needsMarkerScale = true;
        }

        if ("yaxis.range[0]" in newLayout) {
            self.currentZoom.ymin = newLayout["yaxis.range[0]"];
            fireEvent = true;
            needsMarkerScale = true;
        }

        if ("yaxis.range[1]" in newLayout) {
            self.currentZoom.ymax = newLayout["yaxis.range[1]"];
            fireEvent = true;
            needsMarkerScale = true;
        }

        if (needsMarkerScale) {
            var circle_radius = self.pointsToRadius();
            var dataUpdate = {
                "marker.size": circle_radius,
            };
            Plotly.restyle(self.node, dataUpdate);
        }

        if (fireEvent) {
            var event = new CustomEvent("scatter_relayout", {
                bubbles: true,
                detail: {
                    newLayout: newLayout,
                    projKeyX: self.proj_keyX,
                    projKeyY: self.proj_keyY,
                    origin: self,
                },
            });
            self.node.dispatchEvent(event);
        }
    });
};

ColorScatter.prototype.getLayout = function () {
    var width = $(this.node).width();

    // Some options might depend on the plot size

    var titleOpt;
    if (width < 800) {
        titleOpt = {
            text: this.title,
            x: 0,
            xref: "paper",
            xanchor: "left",
            yanchor: "top",
            font: {
                size: 14,
            },
        };
    } else {
        titleOpt = {
            text: this.title,
            x: 0,
            xref: "paper",
            xanchor: "left",
            yanchor: "top",
            font: {
                size: 18,
            },
        };
    }

    var xlabel = this.proj_keyX[0];
    var ylabel = this.proj_keyY[0];

    if (this.proj_keyX[1].length > 0) {
        xlabel = xlabel + ": " + this.proj_keyX[1];
    }
    if (this.proj_keyY[1].length > 0) {
        ylabel = ylabel + ": " + this.proj_keyY[1];
    }

    var layout = {
        title: titleOpt,
        hovermode: "closest",
        paper_bgcolor: "rgba(255, 255, 255, 0)",
        plot_bgcolor: "#ffffff",
        dragmode: "pan",
        legend: {
            xanchor: "right",
            yanchor: "right",
            x: 1.08,
            y: 1,
            bgcolor: "rgba(255, 255, 255, .8)",
            bordercolor: "rgba(0, 87, 82, .5)",
            borderwidth: 1,
        },
        margin: {
            l: 40,
            r: 90,
            t: 50,
            b: 40,
            autoexpand: false,
        },
        xaxis: {
            zeroline: false,
            range: [this.currentZoom.xmin, this.currentZoom.xmax],
            title: {
                text: xlabel,
            },
            showgrid: false,
            showline: true,
        },
        yaxis: {
            zeroline: false,
            range: [this.currentZoom.ymin, this.currentZoom.ymax],
            title: {
                text: ylabel,
            },
            showgrid: false,
            showline: true,
        },
        modebar: {
            bgcolor: "rgba(255, 255, 255, 0)",
        },
    };

    return layout;
};

ColorScatter.prototype.getOptions = function () {
    var width = $(this.node).width();
    var displayModeBar;
    if (width < 800) {
        displayModeBar = "hover";
    } else {
        displayModeBar = true;
    }

    var options = {
        scrollZoom: true,
        displaylogo: false,
        displayModeBar: displayModeBar,
        modeBarButtonsToRemove: [
            "sendDataToCloud",
            "hoverCompareCartesian",
            "toggleSpikelines",
        ],
        doubleClick: false,
    };

    return options;
};

ColorScatter.prototype.updateSelection = function () {
    var selected_cells = get_global_status("selected_cell");
    var selectedpoints;

    if (selected_cells.length <= 1) {
        selectedpoints = [null];
    } else {
        var selected_cells_map = _.keyBy(selected_cells, (x) => x);
        selectedpoints = _(this.node.data)
            .map((trace) => trace.text)
            .map((trace_ids) =>
                _(trace_ids)
                    .map((x) => x in selected_cells_map)
                    .map((e, i) => {
                        return { selected: e, index: i };
                    }) // add indices
                    .filter((x) => x.selected)
                    .map((x) => x.index)
                    .value(),
            )
            .value();
    }

    // Unselect everything
    Plotly.restyle(this.node, {
        selectedpoints: selectedpoints,
    });
};

ColorScatter.prototype.pointsToRadius = function () {
    // Pick a point size based on the number of scatter
    // plot points

    // Using a polynomial fit for this
    // Data taken with width: 1397, height: 790
    // Points      Low        High
    // 1052        8          12
    // 2884        7          9
    // 6857        5          7
    // 67171       2.5        3.5

    var width = $(this.node).width();
    var height = $(this.node).height();
    var scale_factor = Math.min(width, height) / 790;

    var A = -1.3026e7;
    var B = 1.9784e4;
    var C = 2.9423;

    var x = 1 / this.n_points;
    if (x > (-1 * B) / (2 * A)) {
        // Things get weird past the inflection point
        x = (-1 * B) / (2 * A);
    }

    var circle_radius =
        A * Math.pow(x, 2) + B * Math.pow(x, 1) + C * Math.pow(x, 0);

    circle_radius = Math.min(circle_radius, 12); // Looks silly if it's too big

    circle_radius = circle_radius * scale_factor;

    // Change markersize based on zoom factor
    var initialArea =
        (this.initialZoom.xmax - this.initialZoom.xmin) *
        (this.initialZoom.ymax - this.initialZoom.ymin);

    var currentArea =
        (this.currentZoom.xmax - this.currentZoom.xmin) *
        (this.currentZoom.ymax - this.currentZoom.ymin);

    circle_radius = circle_radius * Math.pow(initialArea / currentArea, 0.5);

    return circle_radius;
};

/*
 * Automatically adjusts the scale if the points are too wide
 * or too narrow
 */
ColorScatter.prototype.autoZoom = function () {};

ColorScatter.prototype._resize = function () {
    var layout = this.getLayout();
    Plotly.relayout(this.node, layout);

    Plotly.Plots.resize(this.node);
};

ColorScatter.prototype.relayout = function (newLayout) {
    return Plotly.relayout(this.node, newLayout);
};

// Need to purge plots that aren't visible
// Or else there are too many WebGL contexts
ColorScatter.prototype.clear = function () {
    Plotly.purge(this.node);
    this.firstPlot = true;
};

/*
 * Addresses Plotly Issue:
 *   - https://github.com/plotly/plotly.js/issues/3405
 */
ColorScatter.prototype.plotlyBug = function (newData) {
    var oldData = this.node.data;

    var oldSizes = _.map(oldData, (trace) => trace.x.length);
    var newSizes = _.map(newData, (trace) => trace.x.length);

    var plotBug = false;
    for (var i = 0; i < oldSizes.length; i++) {
        if (oldSizes[i] > 100000 && newSizes[i] <= 100000) {
            plotBug = true;
        }
    }

    return plotBug;
};
