# Source: data/requester.js

    /**
     * @module
     * @private
     */
    import {getLinkedData} from './undercomplicate';

    import { DATA_OPS } from '../registry';

    class DataOperation {
        /**
         * Perform a data operation (such as a join)
         * @param {String} join_type
         * @param initiator The entity that initiated the request for data. Usually, this is the data layer. This argument exists so that a data_operation could do things like auto-define axis labels/ color scheme in response to dynamic data. It has potential for side effects if misused, so use sparingly!
         * @param params Optional user/layout parameters to be passed to the data function
         */
        constructor(join_type, initiator, params) {
            this._callable = DATA_OPS.get(join_type);
            this._initiator = initiator;
            this._params = params || [];
        }

        getData(plot_state, ...dependent_recordsets) {
            // Most operations are joins: they receive two pieces of data (eg left + right)
            //   Other ops are possible, like consolidating just one set of records to best value per key
            // Hence all dependencies are passed as first arg: [dep1, dep2, dep3...]

            // Every data operation receives plot_state, reference to the data layer that called it, the input data, & any additional options
            const context = {plot_state, data_layer: this._initiator};
            return Promise.resolve(this._callable(context, dependent_recordsets, ...this._params));
        }
    }

    /**
     * The Requester manages fetching of data across multiple data sources. It is used internally by LocusZoom data layers.
     *   It passes plot.state information to each adapter, and ensures that a series of requests can be performed in a
     *   designated order.
     *
     * Each data layer calls the requester object directly, and as such, each data layer has a private view of data: it can
     *   perform its own calculations, filter results, and apply transforms without influencing other layers.
     *  (while still respecting a shared cache where appropriate)
     *
     * This object is not part of the public interface. It should almost **never** be replaced or modified directly.
     *
     * @param {DataSources} sources A set of data sources used specifically by this plot instance
     * @private
     */
    class Requester {
        constructor(sources) {
            this._sources = sources;
        }

        /**
         * Parse the data layer configuration when a layer is first created.
         *  Validate config, and return entities and dependencies in a format usable for data retrieval.
         *  This is used by data layers, and also other data-retrieval functions (like subscribeToDate).
         *
         *  Inherent assumptions:
         *  1. A data layer will always know its data up front, and layout mutations will only affect what is displayed.
         *  2. People will be able to add new data adapters (tracks), but if they are removed, the accompanying layers will be
         *      removed at the same time. Otherwise, the pre-parsed data fetching logic could could preserve a reference to the
         *      removed adapter.
         * @param {Object} namespace_options
         * @param {Array} data_operations
         * @param {Object|null} initiator The entity that initiated the request (the data layer). Passed to data operations,
         *   but not adapters. By baking this reference into each data operation, functions can do things like autogenerate
         *   axis tick marks or color schemes based on dyanmic data. This is an advanced usage and should be handled with care!
         * @returns {Array} Map of entities and list of dependencies
         */
        config_to_sources(namespace_options = {}, data_operations = [], initiator) {
            const entities = new Map();
            const namespace_local_names = Object.keys(namespace_options);

            // 1. Specify how to coordinate data. Precedence:
            //   a) EXPLICIT fetch logic,
            //   b) IMPLICIT auto-generate fetch order if there is only one NS,
            //   c) Throw "spec required" error if > 1, because 2 adapters may need to be fetched in a sequence
            let dependency_order = data_operations.find((item) => item.type === 'fetch');  // explicit spec: {fetch, from}
            if (!dependency_order) {
                dependency_order = { type: 'fetch', from: namespace_local_names };
                data_operations.unshift(dependency_order);
            }

            // Validate that all NS items are available to the root requester in DataSources. All layers recognize a
            //  default value, eg people copying the examples tend to have defined a datasource called "assoc"
            const ns_pattern = /^\w+$/;
            for (let [local_name, global_name] of Object.entries(namespace_options)) {
                if (!ns_pattern.test(local_name)) {
                    throw new Error(`Invalid namespace name: '${local_name}'. Must contain only alphanumeric characters`);
                }

                const source = this._sources.get(global_name);
                if (!source) {
                    throw new Error(`A data layer has requested an item not found in DataSources: data type '${local_name}' from ${global_name}`);
                }
                entities.set(local_name, source);

                // Note: Dependency spec checker will consider "ld(assoc)" to match a namespace called "ld"
                if (!dependency_order.from.find((dep_spec) => dep_spec.split('(')[0] === local_name)) {
                    // Sometimes, a new piece of data (namespace) will be added to a layer. Often this doesn't have any dependencies, other than adding a new join.
                    //  To make it easier to EXTEND existing layers, by default, we'll push any unknown namespaces to data_ops.fetch
                    // Thus the default behavior is "fetch all namespaces as though they don't depend on anything.
                    //  If they depend on something, only then does "data_ops[@type=fetch].from" need to be mutated
                    dependency_order.from.push(local_name);
                }
            }

            let dependencies = Array.from(dependency_order.from);

            // Now check all joins. Are namespaces valid? Are they requesting known data?
            for (let config of data_operations) {
                let {type, name, requires, params} = config;
                if (type !== 'fetch') {
                    let namecount = 0;
                    if (!name) {
                        name = config.name = `join${namecount}`;
                        namecount += 1;
                    }

                    if (entities.has(name)) {
                        throw new Error(`Configuration error: within a layer, join name '${name}' must be unique`);
                    }
                    requires.forEach((require_name) => {
                        if (!entities.has(require_name)) {
                            throw new Error(`Data operation cannot operate on unknown provider '${require_name}'`);
                        }
                    });

                    const task = new DataOperation(type, initiator, params);
                    entities.set(name, task);
                    dependencies.push(`${name}(${requires.join(', ')})`); // Dependency resolver uses the form item(depA, depB)
                }
            }
            return [entities, dependencies];
        }

        /**
         * @param {Object} plot_state Plot state, which will be passed to every adapter. Includes view extent (chr, start, end)
         * @param {Map} entities A list of adapter and join tasks. This is created internally from data layer layouts.
         *  Keys are layer-local namespaces for data types (like assoc), and values are adapter or join task instances
         *  (things that implement a method getData).
         * @param {String[]} dependencies Instructions on what adapters to fetch from, in what order
         * @returns {Promise}
         */
        getData(plot_state, entities, dependencies) {
            if (!dependencies.length) {
                return Promise.resolve([]);
            }
            // The last dependency (usually the last join operation) determines the last thing returned.
            return getLinkedData(plot_state, entities, dependencies, true);
        }
    }

    export default Requester;

    export {DataOperation as _JoinTask};