/*
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
import $ from "jquery";
import * as _ from 'underscore';
import React from "react";
//@ts-ignore
import JSON5 from "json5";
import './style/global';
import { ParamType, HiPlotLoadStatus, PSTATE_COLOR_BY, PSTATE_LOAD_URI, PSTATE_PARAMS, PSTATE_FILTERS } from "./types";
import { RowsDisplayTable } from "./rowsdisplaytable";
import { infertypes, colorScheme } from "./infertypes";
import { PersistentStateInMemory } from "./lib/savedstate";
import { ParallelPlot } from "./parallel/parallel";
import { PlotXY } from "./plotxy";
import { SelectedCountProgressBar } from "./controls";
import { ErrorDisplay, HeaderBar } from "./elements";
//@ts-ignore
import LogoSVG from "../hiplot/static/logo.svg";
//@ts-ignore
import LogoSVGW from "../hiplot/static/logo-w.svg";
//@ts-ignore
import style from "./hiplot.scss";
import { ContextMenu } from "./contextmenu";
import { HiPlotDistributionPlugin } from "./distribution/plugin";
import { FilterType, apply_filters, apply_filter } from "./filters";
// Exported from HiPlot
export { PlotXY } from "./plotxy";
export { ParallelPlot } from "./parallel/parallel";
export { RowsDisplayTable } from "./rowsdisplaytable";
export { HiPlotLoadStatus } from "./types";
;
;
function detectIsDarkTheme() {
    // Hack: detect dark/light theme in Jupyter Lab
    const jupyterLabAttrLightTheme = "data-jp-theme-light";
    if (document.body.hasAttribute(jupyterLabAttrLightTheme)) {
        return document.body.getAttribute(jupyterLabAttrLightTheme) == "false";
    }
    return window.matchMedia("(prefers-color-scheme: dark)").matches;
}
export const defaultPlugins = {
    // Names correspond to values of hip.Displays
    // @ts-ignore
    "PARALLEL_PLOT": ParallelPlot,
    // @ts-ignore
    "XY": PlotXY,
    // @ts-ignore
    "DISTRIBUTION": HiPlotDistributionPlugin,
    // @ts-ignore
    "TABLE": RowsDisplayTable,
};
export class HiPlot extends React.Component {
    constructor(props) {
        super(props);
        // React refs
        this.contextMenuRef = React.createRef();
        this.comm_message_id = 0;
        this.plugins_window_state = {};
        this.plugins_ref = []; // For debugging/tests
        this.state = {
            experiment: props.experiment,
            colormap: null,
            version: 0,
            loadStatus: HiPlotLoadStatus.None,
            error: null,
            dp_lookup: {},
            rows_all_unfiltered: [],
            rows_filtered: [],
            rows_filtered_filters: [],
            rows_selected: [],
            rows_selected_filter: null,
            rows_highlighted: [],
            params_def: {},
            params_def_unfiltered: {},
            colorby: null,
            dark: this.props.dark === null ? detectIsDarkTheme() : this.props.dark,
            persistent_state: props.persistent_state !== undefined && props.persistent_state !== null ? props.persistent_state : new PersistentStateInMemory("", {}),
        };
        Object.keys(props.plugins).forEach((name, index) => {
            this.plugins_window_state[name] = {};
            this.plugins_ref[index] = React.createRef();
        });
        this.onSelectedChange_debounced = _.debounce(this.onSelectedChange.bind(this), 200);
    }
    static getDerivedStateFromError(error) {
        // Update state so the next render will show the fallback UI.
        return {
            experiment: null,
            loadStatus: HiPlotLoadStatus.Error,
            error: error.toString(),
        };
    }
    makeDatasets(experiment, dp_lookup, initial_filters) {
        if (experiment) {
            const rows_all_unfiltered = experiment.datapoints.map(function (t) {
                var obj_with_uid = $.extend({
                    "uid": t.uid,
                    "from_uid": t.from_uid,
                }, t.values);
                dp_lookup[t.uid] = obj_with_uid;
                return obj_with_uid;
            });
            var rows_filtered = rows_all_unfiltered;
            try {
                rows_filtered = apply_filters(rows_all_unfiltered, initial_filters);
                if (!rows_filtered.length) {
                    rows_filtered = rows_all_unfiltered;
                    console.log("Not reapplying filters (would filter out all rows)");
                }
            }
            catch (err) {
                console.error("Error trying to apply filters", initial_filters, ":", err);
            }
            return {
                rows_all_unfiltered: rows_all_unfiltered,
                rows_filtered: rows_filtered,
                rows_selected: rows_filtered,
                rows_highlighted: []
            };
        }
        return {
            rows_all_unfiltered: [],
            rows_filtered: [],
            rows_selected: [],
            rows_highlighted: []
        };
    }
    sendMessage(type, data) {
        if (this.props.comm !== null) {
            this.props.comm.send({
                'type': type,
                'message_id': this.comm_message_id,
                'data': data,
            });
            this.comm_message_id += 1;
        }
    }
    onSelectedChange() {
        this.sendMessage("selection", {
            'selected': this.state.rows_selected.map(row => '' + row['uid'])
        });
    }
    _loadExperiment(experiment) {
        // Generate dataset for Parallel Plot
        var dp_lookup = {};
        var initFilters = this.state.persistent_state.get(PSTATE_FILTERS, []);
        const datasets = this.makeDatasets(experiment, dp_lookup, initFilters);
        if (datasets.rows_all_unfiltered == datasets.rows_filtered) {
            initFilters = [];
        }
        const params_def = infertypes(this.state.persistent_state.children(PSTATE_PARAMS), datasets.rows_filtered, experiment.parameters_definition);
        const params_def_unfiltered = infertypes(this.state.persistent_state.children(PSTATE_PARAMS), datasets.rows_all_unfiltered, experiment.parameters_definition);
        // Color handling
        function get_default_color() {
            if (experiment.colorby && params_def[experiment.colorby]) {
                return experiment.colorby;
            }
            function select_as_coloring_score(r) {
                var pd = params_def[r];
                var score = 0;
                if (pd.colors || pd.colormap) {
                    score += 100;
                }
                if (pd.type == ParamType.CATEGORICAL) {
                    score -= 20;
                }
                if (pd.optional) {
                    score -= 40;
                }
                return score;
            }
            ;
            var possibles = Object.keys(params_def).sort((a, b) => select_as_coloring_score(b) - select_as_coloring_score(a));
            return possibles[0];
        }
        var colorby = this.state.persistent_state.get(PSTATE_COLOR_BY, get_default_color());
        if (params_def[colorby] === undefined) {
            colorby = get_default_color();
        }
        this.setState(function (state, props) {
            return {
                experiment: experiment,
                colormap: experiment.colormap,
                version: state.version + 1,
                loadStatus: HiPlotLoadStatus.Loaded,
                dp_lookup: dp_lookup,
                colorby: colorby,
                params_def: params_def,
                params_def_unfiltered: params_def_unfiltered,
                rows_filtered_filters: initFilters,
                ...datasets,
            };
        });
    }
    getColorForRow(trial, alpha) {
        return colorScheme(this.state.params_def[this.state.colorby], trial[this.state.colorby], alpha, this.state.colormap);
    }
    ;
    loadWithPromise(prom) {
        var me = this;
        me.setState({ loadStatus: HiPlotLoadStatus.Loading });
        prom.then(function (data) {
            if (data.experiment === undefined) {
                console.log("Experiment loading failed", data);
                me.setState({
                    loadStatus: HiPlotLoadStatus.Error,
                    experiment: null,
                    error: data.error !== undefined ? data.error : 'Unable to load experiment',
                });
                return;
            }
            me._loadExperiment(data.experiment);
        })
            .catch(error => {
            console.log('Error', error);
            me.setState({ loadStatus: HiPlotLoadStatus.Error, experiment: null, error: 'HTTP error, check server logs / javascript console' });
            throw error;
        });
    }
    componentWillUnmount() {
        if (this.contextMenuRef.current) {
            this.contextMenuRef.current.removeCallbacks(this);
        }
    }
    componentDidMount() {
        // Setup contextmenu when we right-click a parameter
        this.contextMenuRef.current.addCallback(this.columnContextMenu.bind(this), this);
        // Load experiment provided in constructor if any
        if (this.props.experiment !== null) {
            this.loadWithPromise(new Promise(function (resolve, reject) {
                resolve({ experiment: this.props.experiment });
            }.bind(this)));
        }
        else {
            var load_uri = this.state.persistent_state.get(PSTATE_LOAD_URI);
            if (load_uri !== undefined) {
                this.loadURI(load_uri);
            }
        }
    }
    componentDidUpdate(prevProps, prevState) {
        if (prevState.rows_selected != this.state.rows_selected) {
            this.onSelectedChange_debounced();
        }
        if (prevState.rows_filtered_filters != this.state.rows_filtered_filters) {
            this.state.persistent_state.set(PSTATE_FILTERS, this.state.rows_filtered_filters);
        }
        if (prevState.colorby != this.state.colorby && this.state.colorby) {
            this.state.persistent_state.set(PSTATE_COLOR_BY, this.state.colorby);
        }
        if (this.props.experiment !== prevProps.experiment) {
            this.loadWithPromise(new Promise(function (resolve, reject) {
                resolve({ experiment: this.props.experiment });
            }.bind(this)));
        }
    }
    columnContextMenu(column, cm) {
        const VAR_TYPE_TO_NAME = {
            [ParamType.CATEGORICAL]: 'Categorical',
            [ParamType.NUMERIC]: 'Number',
            [ParamType.NUMERICLOG]: 'Number (log-scale)',
            [ParamType.NUMERICPERCENTILE]: 'Number (percentile-scale)',
            [ParamType.TIMESTAMP]: 'Timestamp',
        };
        var contextmenu = $(cm);
        contextmenu.append($('<h6 class="dropdown-header">Data scaling</h6>'));
        this.state.params_def[column].type_options.forEach(function (possible_type) {
            var option = $('<a class="dropdown-item" href="#">').text(VAR_TYPE_TO_NAME[possible_type]);
            if (possible_type == this.state.params_def[column].type) {
                option.addClass('disabled').css('pointer-events', 'none');
            }
            option.click(function (event) {
                contextmenu.css('display', 'none');
                this.setState(function (state, props) {
                    return {
                        params_def: {
                            ...state.params_def,
                            [column]: {
                                ...state.params_def[column],
                                type: possible_type
                            }
                        }
                    };
                });
                this.state.persistent_state.children(PSTATE_PARAMS).children(column).set('type', possible_type);
                event.preventDefault();
            }.bind(this));
            contextmenu.append(option);
        }.bind(this));
        contextmenu.append($('<div class="dropdown-divider"></div>'));
        // Color by
        var link_colorize = $('<a class="dropdown-item" href="#">Use for coloring</a>');
        link_colorize.click(function (event) {
            this.setState({
                colorby: column,
            });
            event.preventDefault();
        }.bind(this));
        if (this.state.colorby == column) {
            link_colorize.addClass('disabled').css('pointer-events', 'none');
        }
        contextmenu.append(link_colorize);
    }
    onRefreshDataBtn() {
        this.loadURI(this.state.persistent_state.get(PSTATE_LOAD_URI));
    }
    loadURI(uri) {
        this.loadWithPromise(new Promise(function (resolve, reject) {
            $.get("/data?uri=" + encodeURIComponent(uri), resolve, "json").fail(function (data) {
                //console.log("Data loading failed", data);
                if (data.readyState == 4 && data.status == 200) {
                    console.log('Unable to parse JSON with JS default decoder (Maybe it contains NaNs?). Trying custom decoder');
                    var decoded = JSON5.parse(data.responseText);
                    resolve(decoded);
                }
                else if (data.status == 0) {
                    resolve({
                        'experiment': undefined,
                        'error': 'Network error'
                    });
                    return;
                }
                else {
                    reject(data);
                }
            });
        }));
    }
    onRunsTextareaSubmitted(uri) {
        this.state.persistent_state.set(PSTATE_LOAD_URI, uri);
        this.loadURI(uri);
    }
    createNewParamsDef(rows_filtered) {
        var new_pd = Object.assign({}, this.state.params_def);
        Object.assign(new_pd, infertypes(this.state.persistent_state.children(PSTATE_PARAMS), rows_filtered, this.state.params_def));
        return new_pd;
    }
    restoreAllRows() {
        /**
         * When we hit `Restore` button
         */
        this.setState(function (state, props) {
            const all_rows = state.rows_all_unfiltered;
            const new_pd = this.createNewParamsDef(all_rows);
            return {
                rows_selected: all_rows,
                rows_selected_filter: null,
                rows_filtered: all_rows,
                rows_filtered_filters: [],
                params_def: new_pd,
            };
        }.bind(this));
    }
    ;
    filterRows(keep) {
        /**
         * When we hit Keep (keep=true), or Exclude (keep=false) buttons
         */
        this.setState(function (state, props) {
            const new_filtered = keep ? state.rows_selected : _.difference(state.rows_filtered, state.rows_selected);
            var filter = state.rows_selected_filter;
            if (!keep) {
                filter = {
                    type: FilterType.Not,
                    data: filter,
                };
            }
            const new_pd = this.createNewParamsDef(new_filtered);
            return {
                rows_filtered: new_filtered,
                params_def: new_pd,
                rows_selected_filter: null,
                rows_filtered_filters: state.rows_filtered_filters.concat([filter]),
            };
        }.bind(this));
    }
    ;
    setSelected(rows, filter = null) {
        if (filter && _.isEqual(filter, this.state.rows_selected_filter)) {
            return;
        }
        if (filter && this.props.asserts) {
            const new_rows = apply_filter(this.state.rows_filtered, filter);
            if (new_rows.length != rows.length || _.difference(new_rows, rows).length) {
                console.error("Warning! Filter ", filter, " does not match given rows", rows, " Computed rows with filter:", new_rows);
            }
        }
        this.setState({
            rows_selected: rows,
            rows_selected_filter: filter
        });
    }
    setHighlighted(rows) {
        this.setState({ rows_highlighted: rows });
    }
    renderRowText(row) {
        return row.uid;
    }
    ;
    render() {
        const datasets = {
            rows_all_unfiltered: this.state.rows_all_unfiltered,
            rows_filtered: this.state.rows_filtered,
            rows_highlighted: this.state.rows_highlighted,
            rows_selected: this.state.rows_selected
        };
        const controlProps = {
            restoreAllRows: this.restoreAllRows.bind(this),
            filterRows: this.filterRows.bind(this),
            ...datasets
        };
        const createPluginProps = function (idx, name) {
            return {
                ref: this.plugins_ref[idx],
                ...(this.state.experiment._displays[name] ? this.state.experiment._displays[name] : {}),
                ...datasets,
                rows_selected_filter: this.state.rows_selected_filter,
                name: name,
                persistent_state: this.state.persistent_state.children(name),
                window_state: this.plugins_window_state[name],
                sendMessage: this.sendMessage.bind(this),
                get_color_for_row: this.getColorForRow.bind(this),
                experiment: this.state.experiment,
                params_def: this.state.params_def,
                params_def_unfiltered: this.state.params_def_unfiltered,
                dp_lookup: this.state.dp_lookup,
                colorby: this.state.colorby,
                render_row_text: this.renderRowText.bind(this),
                context_menu_ref: this.contextMenuRef,
                setSelected: this.setSelected.bind(this),
                setHighlighted: this.setHighlighted.bind(this),
                asserts: this.props.asserts,
            };
        }.bind(this);
        return (React.createElement("div", { className: `scoped_css_bootstrap--${this.state.dark ? "dark" : "light"}` },
            React.createElement("div", { className: `${style.hiplot} ${this.state.dark ? style.dark : ""}` },
                React.createElement(SelectedCountProgressBar, Object.assign({}, controlProps)),
                React.createElement(HeaderBar, Object.assign({ onRequestLoadExperiment: this.props.is_webserver ? this.onRunsTextareaSubmitted.bind(this) : null, onRequestRefreshExperiment: this.props.is_webserver ? this.onRefreshDataBtn.bind(this) : null, loadStatus: this.state.loadStatus, initialLoadUri: this.state.persistent_state.get(PSTATE_LOAD_URI, ''), dark: this.state.dark }, controlProps)),
                this.state.loadStatus == HiPlotLoadStatus.Error &&
                    React.createElement(ErrorDisplay, { error: this.state.error }),
                this.state.loadStatus != HiPlotLoadStatus.Loaded &&
                    React.createElement(DocAndCredits, { dark: this.state.dark }),
                React.createElement(ContextMenu, { ref: this.contextMenuRef }),
                this.state.loadStatus == HiPlotLoadStatus.Loaded &&
                    React.createElement("div", null, Object.entries(this.props.plugins).map((plugin, idx) => React.createElement(React.Fragment, { key: idx }, React.createElement(plugin[1], createPluginProps(idx, plugin[0]))))))));
    }
    getPlugin(cls) {
        const entries = Object.entries(this.props.plugins);
        for (var i = 0; i < entries.length; ++i) {
            if (entries[i][1] == cls) {
                return this.plugins_ref[i].current;
            }
        }
        throw new Error("Can not find plugin" + cls);
    }
}
HiPlot.defaultProps = {
    is_webserver: false,
    comm: null,
    dark: false,
    asserts: false,
    plugins: defaultPlugins,
    experiment: null,
};
;
class DocAndCredits extends React.Component {
    render() {
        return (React.createElement("div", { className: "container hide-when-loaded" },
            React.createElement("div", { className: "row" },
                React.createElement("div", { className: "col-md-3" }),
                React.createElement("div", { className: "col-md-6" },
                    React.createElement("img", { src: this.props.dark ? LogoSVGW : LogoSVG })),
                React.createElement("div", { className: "col-md-3" }),
                React.createElement("div", { className: "col-md-6" },
                    React.createElement("h3", null, "Controls"),
                    React.createElement("p", null,
                        React.createElement("strong", null, "Brush"),
                        ": Drag vertically along an axis.",
                        React.createElement("br", null),
                        React.createElement("strong", null, "Remove Brush"),
                        ": Tap the axis background.",
                        React.createElement("br", null),
                        React.createElement("strong", null, "Reorder Axes"),
                        ": Drag a label horizontally.",
                        React.createElement("br", null),
                        React.createElement("strong", null, "Invert Axis"),
                        ": Tap an axis label.",
                        React.createElement("br", null),
                        React.createElement("strong", null, "Remove Axis"),
                        ": Drag axis label to the left edge.",
                        React.createElement("br", null))),
                React.createElement("div", { className: "cold-md-6" },
                    React.createElement("h3", null, "Credits & License"),
                    React.createElement("p", null,
                        "Adapted from examples by",
                        React.createElement("br", null),
                        React.createElement("a", { href: "http://bl.ocks.org/syntagmatic/3150059" }, "Kai"),
                        ", ",
                        React.createElement("a", { href: "http://bl.ocks.org/1341021" }, "Mike Bostock"),
                        " and ",
                        React.createElement("a", { href: "http://bl.ocks.org/1341281" }, "Jason Davies"),
                        React.createElement("br", null)),
                    React.createElement("p", null,
                        "Released under the ",
                        React.createElement("strong", null, "MIT License"),
                        ".")))));
    }
}
;
