# Source: helpers/scalable.js

    /**
     * Plugin registry of available functions that can be used in scalable layout directives.
     *
     * These "scale functions" are used during rendering to return output (eg color) based on input value
     *
     * @module LocusZoom_ScaleFunctions
     * @see {@link module:LocusZoom_DataLayers~ScalableParameter} for details on how scale functions are used by datalayers
     */

    import * as d3 from 'd3';

    /**
     * Basic conditional function to evaluate the value of the input field and return based on equality.
     * @alias module:LocusZoom_ScaleFunctions~if
     * @param {Object} parameters
     * @param {*} parameters.field_value The value against which to test the input value.
     * @param {*} parameters.then The value to return if the input value matches the field value
     * @param {*} parameters.else  The value to return if the input value does not match the field value. Optional. If not
     *   defined this scale function will return null (or value of null_value parameter, if defined) when input value fails
     *   to match field_value.
     * @param {*} value value
     */
    const if_value = (parameters, value) => {
        if (typeof value == 'undefined' || parameters.field_value !== value) {
            if (typeof parameters.else != 'undefined') {
                return parameters.else;
            } else {
                return null;
            }
        } else {
            return parameters.then;
        }
    };

    /**
     * Function to sort numerical values into bins based on numerical break points. Will only operate on numbers and
     *   return null (or value of null_value parameter, if defined) if provided a non-numeric input value. Parameters:
     * @function numerical_bin
     * @param {Object} parameters
     * @param {Number[]} parameters.breaks  Array of numerical break points against which to evaluate the input value.
     *   Must be of equal length to values parameter. If the input value is greater than or equal to break n and less than
     *   or equal to break n+1 (or break n+1 doesn't exist) then returned value is the nth entry in the values parameter.
     * @param {Array} parameters.values  Array of values to return given evaluations against break points. Must be of
     *   equal length to breaks parameter. Each entry n represents the value to return if the input value is greater than
     *   or equal to break n and less than or equal to break n+1 (or break n+1 doesn't exist).
     * @param {*} parameters.null_value
     * @param {*} value value
     * @returns {*}
     */
    const numerical_bin = (parameters, value) => {
        const breaks = parameters.breaks || [];
        const values = parameters.values || [];
        if (typeof value == 'undefined' || value === null || isNaN(+value)) {
            return (parameters.null_value ? parameters.null_value : null);
        }
        const threshold = breaks.reduce(function (prev, curr) {
            if (+value < prev || (+value >= prev && +value < curr)) {
                return prev;
            } else {
                return curr;
            }
        });
        return values[breaks.indexOf(threshold)];
    };

    /**
     * Function to sort values of any type into bins based on direct equality testing with a list of categories.
     *   Will return null if provided an input value that does not match to a listed category.
     * @function categorical_bin
     * @param {Object} parameters
     * @param {Array} parameters.categories  Array of values against which to evaluate the input value. Must be of equal
     *   length to values parameter. If the input value is equal to category n then returned value is the nth entry in the
     *   values parameter.
     * @param {Array} parameters.values  Array of values to return given evaluations against categories. Must be of equal
     *   length to categories parameter. Each entry n represents the value to return if the input value is equal to the nth
     *   value in the categories parameter.
     * @param {*} parameters.null_value  Value to return if the input value fails to match to any categories. Optional.
     */
    const categorical_bin = (parameters, value) => {
        if (typeof value == 'undefined' || !parameters.categories.includes(value)) {
            return (parameters.null_value ? parameters.null_value : null);
        } else {
            return parameters.values[parameters.categories.indexOf(value)];
        }
    };

    /**
     * Cycle through a set of options, so that the each element in a set of data receives a value different than the
     *  element before it. For example: "use this palette of 10 colors to visually distinguish 100 adjacent items"
     * This is useful when ADJACENT items must be guaranteed to yield a different result, but it leads to unstable color
     *  choices if the user pans to a region with a different number/order of items. (the same item is assigned a different color)
     *
     *  See also: stable_choice.
     * @function ordinal_cycle
     *  @param {Object} parameters
     *  @param {Array} parameters.values A list of option values
     * @return {*}
     */
    const ordinal_cycle = (parameters, value, index) => {
        const options = parameters.values;
        return options[index % options.length];
    };

    /**
     * A scale function that auto-chooses something (like color) from a preset scheme, and makes the same choice every
     * time given the same value, regardless of ordering or what other data is in the region
     *
     * This is useful when categories must be stable (same color, every time). But sometimes it will assign adjacent values
     *  the same color due to hash collisions.
     *
     * For performance reasons, this is memoized once per instance. Eg, each scalable color parameter has its own cache.
     *  This function is therefore slightly less amenable to layout mutations like "changing the options after scaling
     *  function is used", but this is not expected to be a common use case.
     *
     *  CAVEAT: Some datasets do not return true datum ids, but instead append synthetic ID fields ("item 1, item2"...)
     *    just to appease D3. This hash function only works if there is a meaningful, stable identifier in the data,
     *    like a category or gene name.
     *
     * @function stable_choice
     *
     * @param parameters
     * @param {Array} [parameters.values] A list of options to choose from
     * @param {Number} [parameters.max_cache_size=500] The maximum number of values to cache. This option is mostly used
     *  for unit testing, because stable choice is intended for datasets with a relatively limited number of
     *  discrete categories.
     * @param value
     * @param index
     */
    let stable_choice = (parameters, value, index) => {
        // Each place the function gets used has its own parameters object. This function thus memoizes per usage
        //  ("association - point color - directive 1") rather than globally ("all properties/panels")
        const cache = parameters._cache = parameters._cache || new Map();
        const max_cache_size = parameters.max_cache_size || 500;

        if (cache.size >= max_cache_size) {
            // Prevent cache from growing out of control (eg as user moves between regions a lot)
            cache.clear();
        }
        if (cache.has(value)) {
            return cache.get(value);
        }

        // Simple JS hashcode implementation, from:
        //  https://stackoverflow.com/questions/7616461/generate-a-hash-from-string-in-javascript
        let hash = 0;
        value = String(value);
        for (let i = 0; i < value.length; i++) {
            let chr = value.charCodeAt(i);
            hash  = ((hash << 5) - hash) + chr;
            hash |= 0; // Convert to 32bit integer
        }
        // Convert signed 32 bit integer to be within the range of options allowed
        const options = parameters.values;
        const result = options[Math.abs(hash) % options.length];
        cache.set(value, result);
        return result;
    };

    /**
     * Function for continuous interpolation of numerical values along a gradient with arbitrarily many break points.
     * @function interpolate
     * @parameters {Object} parameters
     * @parameters {Number[]} parameters.breaks  Array of numerical break points against which to evaluate the input value.
     *   Must be of equal length to values parameter and contain at least two elements. Input value will be evaluated for
     *   relative position between two break points n and n+1 and the returned value will be interpolated at a relative
     *   position between values n and n+1.
     * @parameters {*[]} parameters.values  Array of values to interpolate and return given evaluations against break
     *   points. Must be of equal length to breaks parameter and contain at least two elements. Each entry n represents
     *   the value to return if the input value matches the nth entry in breaks exactly. Note that this scale function
     *   uses d3.interpolate to provide for effective interpolation of many different value types, including numbers,
     *   colors, shapes, etc.
     * @parameters {*} parameters.null_value
     */
    const interpolate = (parameters, value) => {
        var breaks = parameters.breaks || [];
        var values = parameters.values || [];
        var nullval = (parameters.null_value ? parameters.null_value : null);
        if (breaks.length < 2 || breaks.length !== values.length) {
            return nullval;
        }
        if (typeof value == 'undefined' || value === null || isNaN(+value)) {
            return nullval;
        }
        if (+value <= parameters.breaks[0]) {
            return values[0];
        } else if (+value >= parameters.breaks[parameters.breaks.length - 1]) {
            return values[breaks.length - 1];
        } else {
            var upper_idx = null;
            breaks.forEach(function (brk, idx) {
                if (!idx) {
                    return;
                }
                if (breaks[idx - 1] <= +value && breaks[idx] >= +value) {
                    upper_idx = idx;
                }
            });
            if (upper_idx === null) {
                return nullval;
            }
            const normalized_input = (+value - breaks[upper_idx - 1]) / (breaks[upper_idx] - breaks[upper_idx - 1]);
            if (!isFinite(normalized_input)) {
                return nullval;
            }
            return d3.interpolate(values[upper_idx - 1], values[upper_idx])(normalized_input);
        }
    };

    /**
     * Calculate the effect direction based on beta, or the combination of beta and standard error.
     *   Typically used with phewas plots, to show point shape based on the beta and stderr_beta fields.
     *
     * @function effect_direction
     * @param parameters
     * @param parameters.'+' The value to return if the effect direction is positive
     * @param parameters.'-' The value to return if the effect direction is positive
     * @param parameters.beta_field The name of the field containing beta
     * @param parameters.stderr_beta_field The name of the field containing stderr_beta
     * @param {Object} input This function should receive the entire datum object, rather than one single field
     * @returns {null}
     */
    function effect_direction(parameters, input) {
        if (input === undefined) {
            return null;
        }

        const { beta_field, stderr_beta_field, '+': plus_result = null, '-': neg_result = null } = parameters;

        if (!beta_field || !stderr_beta_field) {
            throw new Error(`effect_direction must specify how to find required 'beta' and 'stderr_beta' fields`);
        }

        const beta_val = input[beta_field];
        const se_val = input[stderr_beta_field];

        if (beta_val !== undefined) {
            if (se_val !== undefined) {
                if ((beta_val - 1.96 * se_val) > 0) {
                    return plus_result;
                } else if ((beta_val + 1.96 * se_val) < 0) {
                    return neg_result || null;
                }
            } else {
                if (beta_val > 0) {
                    return plus_result;
                } else if (beta_val < 0) {
                    return neg_result;
                }
            }
        }
        // Note: The original PheWeb implementation allowed odds ratio in place of beta/se. LZ core is a bit more rigid
        //   about expected data formats for layouts.
        return null;
    }

    export { categorical_bin, stable_choice, if_value, interpolate, numerical_bin, ordinal_cycle, effect_direction };