# Source: ext/lz-widget-addons.js

    /**
     * Optional LocusZoom extension: must be included separately, and after LocusZoom has been loaded
     *
     * This contains (reusable) code to power some (rarely used) demo features:
     *  - The "covariates model" demo, in which an LZ toolbar widget is populated
     *    with information by selecting points on the plot (see "covariates model" demo)
     *  - The "data layers" button, which allows fine control over multiple data layers shown in the same panel
     *    (show/hide, fade, change order, etc). This is powerful, but rarely used because showing many datasets in a small
     *    space makes data hard to see. (see "multiple phenotypes layered" demo)
     *
     * ### Loading and usage
     * The page must incorporate and load all libraries before this file can be used, including:
     * - LocusZoom
     *
     * To use in an environment without special JS build tooling, simply load the extension file as JS from a CDN (after any dependencies):
     *
     * ```javascript
     * <script src="https://cdn.jsdelivr.net/npm/locuszoom@INSERT_VERSION_HERE/dist/ext/lz-widget-addons.min.js" type="application/javascript"></script>
     * ```
     *
     * To use with ES6 modules, the plugin must be loaded and registered explicitly before use:
     * ```javascript
     * import LocusZoom from 'locuszoom';
     * import WidgetAddons from 'locuszoom/esm/ext/lz-widget-addons';
     * LocusZoom.use(WidgetAddons);
     * ```
     *
     * Then use the features made available by this extension. (see demos and documentation for guidance)
     *
     * @module
     */
    import {deepCopy} from '../helpers/layouts';

    // In order to work in a UMD context, this module imports the top-level LocusZoom symbol

    const STATUS_VERBS = ['highlight', 'select', 'fade', 'hide'];
    const STATUS_ADJECTIVES = ['highlighted', 'selected', 'faded', 'hidden'];
    const STATUS_ANTIVERBS = ['unhighlight', 'deselect', 'unfade', 'show'];

    // LocusZoom plugins work by exporting a function that receives the `LocusZoom` object
    // This allows them to work in many contexts (including script tags and ES6 imports)
    function install(LocusZoom) {
        const _Button = LocusZoom.Widgets.get('_Button');
        const _BaseWidget = LocusZoom.Widgets.get('BaseWidget');

        /**
         * Special button/menu to allow model building by tracking individual covariants. Will track a list of covariate
         *   objects and store them in the special `model.covariates` field of plot `state`.
         *
         * This is a prototype widget for building a conditional analysis model, but it performs no calculation
         *  functionality beyond building a list of items.
         * @alias module:ext/lz-widget-addons~covariates_model
         * @see module:LocusZoom_Widgets~BaseWidget
         * @param {object} layout
         * @param {string} layout.button_html The HTML to render inside the button
         * @param {string} layout.button_title Text to display as a tooltip when hovering over the button
         */
        class CovariatesModel extends _BaseWidget {
            initialize() {
                // Initialize state.model.covariates
                this.parent_plot.state.model = this.parent_plot.state.model || {};
                this.parent_plot.state.model.covariates = this.parent_plot.state.model.covariates || [];
                // Create an object at the plot level for easy access to interface methods in custom client-side JS
                /**
                 * When a covariates model toolbar element is present, create (one) object at the plot level that exposes
                 *   widget data and state for custom interactions with other plot elements.
                 */
                this.parent_plot.CovariatesModel = {
                    /** @member {Button} */
                    button: this,
                    /**
                     * Add an element to the model and show a representation of it in the toolbar widget menu. If the
                     *   element is already part of the model, do nothing (to avoid adding duplicates).
                     * When plot state is changed, this will automatically trigger requests for new data accordingly.
                     * @param {string|object} element_reference Can be any value that can be put through JSON.stringify()
                     *   to create a serialized representation of itself.
                     */
                    add: (element_reference) => {
                        const plot = this.parent_plot;
                        const element = deepCopy(element_reference);
                        if (typeof element_reference == 'object' && typeof element.html != 'string') {
                            element.html = ( (typeof element_reference.toHTML == 'function') ? element_reference.toHTML() : element_reference.toString());
                        }
                        // Check if the element is already in the model covariates array and return if it is.
                        for (let i = 0; i < plot.state.model.covariates.length; i++) {
                            if (JSON.stringify(plot.state.model.covariates[i]) === JSON.stringify(element)) {
                                return plot;
                            }
                        }
                        plot.state.model.covariates.push(element);
                        plot.applyState();
                        plot.CovariatesModel.updateWidget();
                        return plot;
                    },
                    /**
                     * Remove an element from `state.model.covariates` (and from the toolbar widget menu's
                     *  representation of the state model). When plot state is changed, this will automatically trigger
                     *  requests for new data accordingly.
                     * @param {number} idx Array index of the element, in the `state.model.covariates array`.
                     */
                    removeByIdx: (idx) => {
                        const plot = this.parent_plot;
                        if (typeof plot.state.model.covariates[idx] == 'undefined') {
                            throw new Error(`Unable to remove model covariate, invalid index: ${idx.toString()}`);
                        }
                        plot.state.model.covariates.splice(idx, 1);
                        plot.applyState();
                        plot.CovariatesModel.updateWidget();
                        return plot;
                    },
                    /**
                     * Empty the `state.model.covariates` array (and toolbar widget menu representation thereof) of all
                     *  elements. When plot state is changed, this will automatically trigger requests for new data accordingly
                     */
                    removeAll: () => {
                        const plot = this.parent_plot;
                        plot.state.model.covariates = [];
                        plot.applyState();
                        plot.CovariatesModel.updateWidget();
                        return plot;
                    },
                    /**
                     * Manually trigger the update methods on the toolbar widget's button and menu elements to force
                     *   display of most up-to-date content. Can be used to force the toolbar to reflect changes made, eg if
                     *   modifying `state.model.covariates` directly instead of via `plot.CovariatesModel`
                     */
                    updateWidget: () => {
                        this.button.update();
                        this.button.menu.update();
                    },
                };
            }

            update() {

                if (this.button) {
                    return this;
                }

                this.button = new _Button(this)
                    .setColor(this.layout.color)
                    .setHtml(this.layout.button_html)
                    .setTitle(this.layout.button_title)
                    .setOnclick(() => {
                        this.button.menu.populate();
                    });

                this.button.menu.setPopulate(() => {
                    const selector = this.button.menu.inner_selector;
                    selector.html('');
                    // General model HTML representation
                    if (typeof this.parent_plot.state.model.html != 'undefined') {
                        selector.append('div').html(this.parent_plot.state.model.html);
                    }
                    // Model covariates table
                    if (!this.parent_plot.state.model.covariates.length) {
                        selector.append('i').html('no covariates in model');
                    } else {
                        selector.append('h5').html(`Model Covariates (${this.parent_plot.state.model.covariates.length})`);
                        const table = selector.append('table');
                        this.parent_plot.state.model.covariates.forEach((covariate, idx) => {
                            const html = ((typeof covariate == 'object' && typeof covariate.html == 'string') ? covariate.html : covariate.toString());
                            const row = table.append('tr');
                            row.append('td').append('button')
                                .attr('class', `lz-toolbar-button lz-toolbar-button-${this.layout.color}`)
                                .style('margin-left', '0em')
                                .on('click', () => this.parent_plot.CovariatesModel.removeByIdx(idx))
                                .html('×');
                            row.append('td')
                                .html(html);
                        });
                        selector.append('button')
                            .attr('class', `lz-toolbar-button lz-toolbar-button-${this.layout.color}`)
                            .style('margin-left', '4px')
                            .html('× Remove All Covariates')
                            .on('click', () => this.parent_plot.CovariatesModel.removeAll());
                    }
                });

                this.button.preUpdate = () => {
                    let html = 'Model';
                    const count = this.parent_plot.state.model.covariates.length;
                    if (count) {
                        const noun = count > 1 ? 'covariates' : 'covariate';
                        html += ` (${count} ${noun})`;
                    }
                    this.button.setHtml(html).disable(false);
                };

                this.button.show();

                return this;
            }
        }

        /**
         * Menu for manipulating multiple data layers in a single panel: show/hide, change order, etc.
         * @alias module:ext/lz-widget-addons~data_layers
         * @see module:LocusZoom_Widgets~BaseWidget
         */
        class DataLayersWidget extends _BaseWidget {
            update() {

                if (typeof this.layout.button_html != 'string') {
                    this.layout.button_html = 'Data Layers';
                }
                if (typeof this.layout.button_title != 'string') {
                    this.layout.button_title = 'Manipulate Data Layers (sort, dim, show/hide, etc.)';
                }

                if (this.button) {
                    return this;
                }

                this.button = new _Button(this)
                    .setColor(this.layout.color)
                    .setHtml(this.layout.button_html)
                    .setTitle(this.layout.button_title)
                    .setOnclick(() => {
                        this.button.menu.populate();
                    });

                this.button.menu.setPopulate(() => {
                    this.button.menu.inner_selector.html('');
                    const table = this.button.menu.inner_selector.append('table');
                    this.parent_panel._data_layer_ids_by_z_index.slice().reverse().forEach((id, idx) => {
                        const data_layer = this.parent_panel.data_layers[id];
                        const name = (typeof data_layer.layout.name != 'string') ? data_layer.id : data_layer.layout.name;
                        const row = table.append('tr');
                        // Layer name
                        row.append('td').html(name);
                        // Status toggle buttons
                        this.layout.statuses.forEach((status_adj) => {
                            const status_idx = STATUS_ADJECTIVES.indexOf(status_adj);
                            const status_verb = STATUS_VERBS[status_idx];
                            let html, onclick, highlight;
                            if (data_layer._global_statuses[status_adj]) {
                                html = STATUS_ANTIVERBS[status_idx];
                                onclick = `un${status_verb}AllElements`;
                                highlight = '-highlighted';
                            } else {
                                html = STATUS_VERBS[status_idx];
                                onclick = `${status_verb}AllElements`;
                                highlight = '';
                            }
                            row.append('td').append('a')
                                .attr('class', `lz-toolbar-button lz-toolbar-button-${this.layout.color}${highlight}`)
                                .style('margin-left', '0em')
                                .on('click', () => {
                                    data_layer[onclick]();
                                    this.button.menu.populate();
                                })
                                .html(html);
                        });
                        // Sort layer buttons
                        const at_top = (idx === 0);
                        const at_bottom = (idx === (this.parent_panel._data_layer_ids_by_z_index.length - 1));
                        const td = row.append('td');
                        td.append('a')
                            .attr('class', `lz-toolbar-button lz-toolbar-button-group-start lz-toolbar-button-${this.layout.color}${at_bottom ? '-disabled' : ''}`)
                            .style('margin-left', '0em')
                            .on('click', () => {
                                data_layer.moveBack(); this.button.menu.populate();
                            })
                            .html('▾')
                            .attr('title', 'Move layer down (further back)');
                        td.append('a')
                            .attr('class', `lz-toolbar-button lz-toolbar-button-group-middle lz-toolbar-button-${this.layout.color}${at_top ? '-disabled' : ''}`)
                            .style('margin-left', '0em')
                            .on('click', () => {
                                data_layer.moveForward(); this.button.menu.populate();
                            })
                            .html('▴')
                            .attr('title', 'Move layer up (further front)');
                        td.append('a')
                            .attr('class', 'lz-toolbar-button lz-toolbar-button-group-end lz-toolbar-button-red')
                            .style('margin-left', '0em')
                            .on('click', () => {
                                if (confirm(`Are you sure you want to remove the ${name} layer? This cannot be undone.`)) {
                                    data_layer.parent.removeDataLayer(id);
                                }
                                return this.button.menu.populate();
                            })
                            .html('×')
                            .attr('title', 'Remove layer');
                    });
                    return this;
                });

                this.button.show();

                return this;
            }
        }

        const covariates_model_tooltip = function () {
            const covariates_model_association = LocusZoom.Layouts.get('tooltip', 'standard_association');
            covariates_model_association.html += '<a href="javascript:void(0);" onclick="LocusZoom.getToolTipPlot(this).CovariatesModel.add(LocusZoom.getToolTipData(this));">Condition on Variant</a><br>';
            return covariates_model_association;
        }();

        const covariates_model_plot = function () {
            const covariates_model_plot_toolbar = LocusZoom.Layouts.get('toolbar', 'standard_association');
            covariates_model_plot_toolbar.widgets.push({
                type: 'covariates_model',
                button_html: 'Model',
                button_title: 'Show and edit covariates currently in model',
                position: 'left',
            });
            return covariates_model_plot_toolbar;
        }();

        LocusZoom.Widgets.add('covariates_model', CovariatesModel);
        LocusZoom.Widgets.add('data_layers', DataLayersWidget);

        LocusZoom.Layouts.add('tooltip', 'covariates_model_association', covariates_model_tooltip);
        LocusZoom.Layouts.add('toolbar', 'covariates_model_plot', covariates_model_plot);
    }

    if (typeof LocusZoom !== 'undefined') {
        // Auto-register the plugin when included as a script tag. ES6 module users must register via LocusZoom.use()
        // eslint-disable-next-line no-undef
        LocusZoom.use(install);
    }

    export default install;