equipment/configurator.js

import { Equipment } from './equipment';
import { State } from './state';

/**
 * Creates a new equipment configurator.
 * @class
 */
class Configurator {
    /**
     * @param {struct} skv_config
     * @param {struct} skv_data Most likely data taken directly from Pluto
     */
    constructor(skv_config = {}, skv_data = {}) {
        const skv_config_defaults = {
            skv_lease_terms: {},
            cost_field: 'cost_net',
            show_as_swatch: {
                paint: false,
            },
            panels: {
                paint: false,
                'options-list': true,
            },
            vat_percentage: 20,
            arr_preselected_options_ids: [],
            skv_vehicle: {
                battery_range: '',
                battery_range_teh: '',
            },
            adjustment_percentage: 0,
            is_salsac: false,
            // only used for salsac. insurance companies like Allianz cannot reclaim vat
            salsac_employer_can_reclaim_vat: true,
            max_gross_total_price_limit: null,
            max_gross_price_item_limit: null,
        };

        // add keys to the defaults
        Object.keys(skv_config).forEach((key_top_level) => {
            // if we've got any lease terms being passed in include the lease term struct too
            if (
                key_top_level === 'skv_lease_terms'
                && Object.keys(skv_config.skv_lease_terms).length
            ) {
                Object.keys(skv_config).forEach((key_lease_term) => {
                    skv_config_defaults.skv_lease_terms[key_lease_term] = skv_config.skv_lease_terms[key_lease_term];
                });
            }

            // if we've got any swatch enabling
            if (
                key_top_level === 'show_as_swatch'
                && Object.keys(skv_config.show_as_swatch).length
            ) {
                Object.keys(skv_config).forEach((key) => {
                    skv_config_defaults.show_as_swatch[key] = skv_config.show_as_swatch[key];
                });
            }

            // if we've got any extra panel requirements
            if (
                key_top_level === 'panels'
                && Object.keys(skv_config.panels).length
            ) {
                Object.keys(skv_config.panels).forEach((key_panel) => {
                    skv_config_defaults.panels[key_panel] = skv_config.panels[key_panel];
                });
            }

            // pass in vehicle details
            if (
                key_top_level === 'skv_vehicle'
                && Object.keys(skv_config.skv_vehicle).length
            ) {
                Object.keys(skv_config.skv_vehicle).forEach((key) => {
                    skv_config_defaults.skv_vehicle[key] = skv_config.skv_vehicle[key];
                });

                // we need to cap co2 in between the tel and teh values so check
                // we have properties passed in
                if (
                    !Object.prototype.hasOwnProperty.call(skv_config.skv_vehicle, 'wltp_co2_tel')
                    || !Object.prototype.hasOwnProperty.call(skv_config.skv_vehicle, 'wltp_co2_teh')
                    || !Object.prototype.hasOwnProperty.call(skv_config.skv_vehicle, 'wltp_co2')
                ) {
                    throw new Error('Vehicle WLTP CO2, WLTP CO2 TEL and WLTP CO2 TEH figures are required to be passed in to the equipment configurator');
                }
            }

            skv_config_defaults[key_top_level] = skv_config[key_top_level];
        });

        // work out whether prices will be in gross or net
        if (
            !skv_config_defaults.is_fqt
            || (
                skv_config_defaults.is_fqt
                && skv_config.skv_lease_terms.agreement_type_code === 'pch'
            )
        ) {
            skv_config_defaults.cost_field = 'cost_gross';
        }

        this.skv_config = skv_config_defaults;
        this.skv_config.num_VAT_factor = 1 + (this.skv_config.vat_percentage / 100);

        this.equipment_map = [];
        this.open_info_toggle_state = [];
        this.open_dependency_toggle_state = [];
        // when events are fired what to provide as the action
        this.event_action = 'init';

        this.setupData(this.skv_config, skv_data);

        // expose functions
        this.calculateSalsacCost = Configurator.calculateSalsacCost;
    }

    /**
     * Combine the data in to one single equipment array
     * @param {struct} skv_config
     * @returns {struct} Pluto API data
     */
    setupData(skv_config, skv_data) {
        const arr_equipment = [];

        // standard or option
        Object.keys(skv_data).forEach((key) => {
            const type = key;
            const cnt_groups = skv_data[type].length;
            for (let i = 0; i < cnt_groups; i += 1) {
                const skv_group = skv_data[type][i];
                let arr_temp_equipment = skv_group.items;

                // give a option/standard type to all the options and standards
                // and return array
                arr_temp_equipment = arr_temp_equipment.filter((skv_equipment) => {
                    const skv_return = skv_equipment;
                    skv_return.type = type === 'standards' ? 'standard' : 'option';
                    return skv_return;
                });

                // for finance we need to remove options like warranties or
                // grants because they have already been taken in to account by
                // the lendor so filter out any options that have "is_finance_allowed"
                // set to false
                if (skv_config.is_fqt) {
                    arr_temp_equipment = arr_temp_equipment.filter((skv_equipment) => skv_equipment.is_finance_allowed);
                }

                // put all the standards / options from a group in to to the overall
                // equipment array
                // use spread operator to add array to another array
                arr_equipment.push(...arr_temp_equipment);
            }
        });

        const arr_packed = arr_equipment.filter((skv_equipment) => skv_equipment.is_packed);

        // create empty array and load equipment into it
        this.equipment = this.createEquipmentObjectsAndMap(skv_config, arr_equipment);

        // some packed equipment may need its price modifying due to tax or finance
        this.equipment = Configurator.updatePackedEquipmentCost(skv_config, this.equipment, this.equipment_map);

        // set initial states
        this.states = {
            current: new State(this.equipment, this.equipment_map),
            undo: new State(this.equipment, this.equipment_map),
            initial: new State(this.equipment, this.equipment_map),
        };

        // if we have pre selected options, loop through each and process their
        // dependencies
        const cnt_preselected_equipment = skv_config.arr_preselected_options_ids.length;

        for (let i = 0; i < cnt_preselected_equipment; i += 1) {
            const num_option_id = skv_config.arr_preselected_options_ids[i];
            const equipment_idx = this.equipment_map[num_option_id];

            this.states.current.process(
                equipment_idx,
                true,
            );
        }

        // if we have packed options, loop through each and process their
        // dependencies
        const cnt_arr_packed = arr_packed.length;

        for (let i = 0; i < cnt_arr_packed; i += 1) {
            const num_option_id = arr_packed[i].id;
            const equipment_idx = this.equipment_map[num_option_id];

            this.states.current.process(
                equipment_idx,
                true,
            );
        }

        // update the current state with display costs
        this.addAdditionalStateData('current');
    }

    /**
     * remove equipment that isn't allowed for example those that fall outside
     * option price limiting
     * @param {struct} skv_config
     * @param {array} arr_equipment Flatted array of equipment structs (standards and options)
     * @returns {array} Array of any remaining equipment items
    */
    static removeNotAllowedEquipment(skv_config, arr_equipment) {
        const arr_return = [];

        // create a map of ids that are outside the price limit
        const remove_ids_map = arr_equipment.map((item) => {
            // if the item is packed the cost is a part of the vehicle itself
            if (
                item.type === 'option'
                && !item.is_packed
            ) {
                // if equipment is over total price limit don't show it as an available option
                if (
                    typeof skv_config.max_gross_total_price_limit === 'number'
                    && item.cost_gross > skv_config.max_gross_total_price_limit
                ) {
                    return item.id;
                }

                // if equipment is over item price limit don't show it as an available option
                if (
                    typeof skv_config.max_gross_price_item_limit === 'number'
                    && item.cost_gross > skv_config.max_gross_price_item_limit
                ) {
                    return item.id;
                }
            }

            return null;
        // remove any undefined values
        }).filter((e) => e);

        // if the equipment or any of its dependencies IDs is in the remove_ids_map
        // then remove from dependencies and/or the equipment itself that we will return
        arr_equipment.forEach((skv_equipment) => {
            const skv_new_equipment = skv_equipment;

            // may not have affected_by if running from unit tests
            if (skv_equipment.affected_by) {
                // only include "affected by" equipment that aren't in the remove ids map
                skv_new_equipment.affected_by = skv_equipment.affected_by.filter((item) => !remove_ids_map.includes(item.id));
            }

            // may not have affects if running from unit tests
            if (skv_equipment.affects) {
                skv_new_equipment.affects = skv_equipment.affects.filter((item) => !remove_ids_map.includes(item.id));
            }

            // only add the equipment if its not to be removed
            if (!remove_ids_map.includes(skv_new_equipment.id)) {
                arr_return.push(skv_new_equipment);
            }
        });

        return arr_return;
    }

    /**
     * packed equipment may need its price modifying due to tax or finance
     * @param {array} equipment
     * @returns {struct}
     */
    static updatePackedEquipmentCost(skv_config, equipment, equipment_map) {
        const arr_returned_equipment = equipment;
        const arr_packed_equipment = equipment.filter((skv_equipment) => skv_equipment.is_packed);

        /**
         * get a list of options that affect packed equipment
         * if this is fqt, modify their price now to be the difference
         * between their current price and the packed equipment
         * they replace
         */
        const cnt_packed_equipment = arr_packed_equipment.length;

        for (let i = 0; i < cnt_packed_equipment; i += 1) {
            const skv_packed_equipment = arr_packed_equipment[i];

            const arr_affects_equipment = skv_packed_equipment.affects;
            const cnt_affects_equipment = arr_affects_equipment.length;

            for (let j = 0; j < cnt_affects_equipment; j += 1) {
                const skv_affects = arr_affects_equipment[j];
                // grab the affects equipment index using the equipment map
                const affects_idx = equipment_map[skv_affects.id];
                const skv_affects_equipment = arr_returned_equipment[affects_idx];

                // ignore if this equipment has a parent as the price will be defaulted to 0
                if (
                    !skv_affects_equipment.is_parent_packed
                    && skv_affects_equipment.type !== 'standard'
                ) {
                    /**
                    * we need to modify the price of a packed option
                    *
                    * if this is fqt we  add the absolute price difference
                    * so packed £120 at £10p/m option will show as £0
                    * a better £180 option at £15p/m will show as £5p/m (£5 diff from 10 up to 5)
                    * a cheaper £0 option at £0p/m will show as £10p/m (£10 diff from 10 down to 0)
                    * (cheaper option devalues the car so the driver has to make up for it)
                    *
                    * if it's tax, we add the price diffence (not absolute)
                    * so packed £120 will show as £0
                    * a better £180 will show as £60
                    * a cheaper £0 will show as -£120
                    * this is because we need the P11D to reflect the list prices
                    */

                    // new price based on difference
                    let new_price_net = skv_affects_equipment.original_net - skv_packed_equipment.original_net;

                    // for fqt, convert negative values to positive
                    if (skv_config.is_fqt) {
                        new_price_net = Math.abs(new_price_net);
                    }

                    skv_affects_equipment.cost_net = new_price_net;
                    skv_affects_equipment.cost_gross = new_price_net * skv_config.num_VAT_factor;

                    // ensure we always show a cost as adding a standard may have
                    // a cost associated
                    skv_affects_equipment.show_cost = true;
                }

                // update the equipment data to be returned
                arr_returned_equipment[affects_idx] = skv_affects_equipment;
            }
        }

        return arr_returned_equipment;
    }

    /**
     * Add any required properties for the initial setup
     * @param {struct} skv_config
     * @param {array} arr_equipment Flatted array of equipment structs (standards and options)
     * @returns {array} Array of any additional or tweaked equipment structs
     */
    createEquipmentObjectsAndMap(skv_config, arr_equipment) {
        const arr_limited_equipment = Configurator.removeNotAllowedEquipment(skv_config, arr_equipment);
        const cnt_equipment = arr_limited_equipment.length;
        const arr_returned_equipment = [];
        const arr_packed_equipment = [];

        // create empty array and populate
        for (let i = 0; i < cnt_equipment; i += 1) {
            const skv_equipment = arr_limited_equipment[i];
            skv_equipment.cost_field = skv_config.cost_field;
            skv_equipment.show_as_swatch = skv_config.show_as_swatch;
            skv_equipment.adjustment_percentage = skv_config.adjustment_percentage;
            skv_equipment.is_salsac = skv_config.is_salsac;
            skv_equipment.salsac_employer_can_reclaim_vat = skv_config.salsac_employer_can_reclaim_vat;
            skv_equipment.vat_percentage = skv_config.vat_percentage;

            // see if this option needs to be preselected by a previous user action
            // most likely on another page and passed in via the url
            const preselected_idx = skv_config.arr_preselected_options_ids.findIndex((preselected_id) => preselected_id === skv_equipment.id);

            // if this option is preselected make sure we select it
            if (preselected_idx > -1) {
                skv_equipment.is_init_selected = true;
            }

            const new_equipment = new Equipment(skv_equipment);

            // store all the packed equipment
            if (skv_equipment.is_packed) {
                arr_packed_equipment.push(skv_equipment);
            }

            arr_returned_equipment.push(new_equipment);
            this.equipment_map[new_equipment.id] = i;
            // we need to keep info open state separate to the rest of the
            // equipment states
            this.open_info_toggle_state.push(
                {
                    id: new_equipment.id,
                    is_open: false,
                },
            );

            // do the same for dependencies
            this.open_dependency_toggle_state.push(
                {
                    id: new_equipment.id,
                    is_open: false,
                },
            );
        }

        return arr_returned_equipment;
    }

    /**
     * Deep clone State
     * @param {string} state_name Which state to clone?
     * @returns {object} State object
     */
    cloneState(state_name) {
        const arr_equipment = this.states[state_name].equipment;
        // deep clone equipment
        const clone_equipment = JSON.parse(JSON.stringify(arr_equipment));

        return new State(clone_equipment, this.equipment_map);
    }

    /**
     * Calculate lease cost
     * @param {number} price
     * @param {struct} skv_lease_terms
     * @returns {number}
     */
    static calculateLeaseCost(price, skv_lease_terms) {
        const { initial_payment } = skv_lease_terms;
        const total_payments = (skv_lease_terms.months + initial_payment) - 1;

        // const residual_value = price * skv_lease_terms.residual_val_proportion;
        // work out how much of the option is paid off over the contract
        // period - this is price minus how much is paid in the initial_payment payment
        // ( totalNumPayments - initial_payment ) / totalNumPayments
        const portion_of_price_to_borrow = (
            total_payments - initial_payment
        ) / total_payments;
        const loan_amount = price * portion_of_price_to_borrow;

        // work out the number of payments the person taking the lease pays
        // this is the total number of months - 1, the first month they pay
        // the initial_payment instead
        // in this function, we can just take the total payments overall
        // minus the initial_payment
        // totalNumPayments = months + initial_payment - 1
        const num_payments_to_make = total_payments - initial_payment;

        // if we have a 0 interest rate, we just need to return the
        // price divided by the total numnber of payments
        if (skv_lease_terms.interest_rate === 0) {
            return price / total_payments;
        }

        /* eslint-disable no-mixed-operators */
        const adjusted_price = loan_amount
                * skv_lease_terms.interest_rate
                / (
                    1 - 1
                    / (
                        (1 + skv_lease_terms.interest_rate) ** num_payments_to_make
                    )
                );
        /* eslint-enable no-mixed-operators */

        return adjusted_price;
    }

    /**
     * Calculate Salsac cost
     * @param {struct} skv_equipment
     * @param {struct} skv_lease_terms
     * @returns {number}
     */
    static calculateSalsacCost(skv_equipment, skv_lease_terms) {
        // for salsac cost net has half the vat added back on and initial payments
        // ignored when calculating p/m figures
        return skv_equipment.cost_net / skv_lease_terms.months;
    }

    /**
     * Get the specified state data and do any calculating required
     * @param {string} str_state current / undo / initial
     */
    addAdditionalStateData(str_state) {
        const arr_original_equipment = this.states[str_state].equipment;
        const cnt_equipment = arr_original_equipment.length;
        const arr_calculated_equpment = [];

        for (let i = 0; i < cnt_equipment; i += 1) {
            const skv_equipment = arr_original_equipment[i];

            // set the display cost of the equipment
            skv_equipment.display_cost = Configurator.getDisplayCost(
                skv_equipment,
                this.skv_config,
            );

            skv_equipment.is_info_open = this.getInfoOrDependencyState(skv_equipment.id, 'info');
            skv_equipment.is_dependency_open = this.getInfoOrDependencyState(skv_equipment.id, 'dependency');

            arr_calculated_equpment.push(skv_equipment);
        }

        this.states[str_state].equipment_extra = arr_calculated_equpment;
    }

    /**
     * Update config
     * @param payload
     */
    updateConfig(payload) {
        if (Object.prototype.hasOwnProperty.call(payload, 'cost_field')) {
            this.skv_config.cost_field = payload.cost_field;
        }

        if (Object.prototype.hasOwnProperty.call(payload, 'skv_lease_terms')) {
            this.skv_config.skv_lease_terms = payload.skv_lease_terms;
        }

        this.event_action = 'update configuration';

        this.dispatch();
    }

    /**
     * action
     * @param {struct} payload Type of action (click, update etc), id of equipment
     */
    action(payload) {
        switch (payload.type) {
        case 'click':
            this.event_action = 'equipment interaction';
            this.equipmentClicked(payload);
            break;

        case 'undo':
            this.event_action = 'undo interaction';
            this.undoOptionSelection();
            break;

        case 'info_toggle':
            this.event_action = 'info interaction';
            this.infoToggle(payload);
            break;

        case 'dependency_toggle':
            this.event_action = 'dependency toggle';
            this.dependencyToggle(payload);
            break;

        default:
            this.event_action = 'unrecognised action';
            throw new Error('action not recognised');
        }
    }

    /**
     * info button clicked
     * @param {struct} payload
     */
    infoToggle(payload) {
        // toggle the payloads
        this.open_info_toggle_state.forEach((skv_info) => {
            if (payload.data.id === skv_info.id) {
                // TODO: don't reassign this
                // eslint-disable-next-line
                skv_info.is_open = !skv_info.is_open;
            }
        });

        // send out the current state of the options
        this.dispatch();
    }

    /**
     * dependency button clicked
     * @param {struct} payload
     */
    dependencyToggle(payload) {
        // toggle the payloads
        this.open_dependency_toggle_state.forEach((skv_info) => {
            if (payload.data.id === skv_info.id) {
                // TODO: don't reassign this
                // eslint-disable-next-line
                skv_info.is_open = !skv_info.is_open;
            }
        });

        // send out the current state of the options
        this.dispatch();
    }

    /**
     * Starting point to handle the equipment click
     * @param {struct} payload Type of interaction and id of equipment interacted with
     */
    equipmentClicked(payload) {
        const equipment_idx = this.equipment_map[payload.data.id];

        this.currentInteraction = this.equipment[equipment_idx];

        // set undo state
        this.states.undo = this.cloneState('current');

        // process new state
        this.states.current.process(equipment_idx); // is_swatch was true

        // send out the current state of the options
        this.dispatch();
    }

    /**
     * Undo options selection
     */
    undoOptionSelection() {
        // revert current to undo
        this.states.current = this.cloneState('undo');

        this.dispatch();
    }

    /**
     * Find whether an info or dependency button is open
     * @param {number} id Equipment id
     * @returns {boolean}
     */
    getInfoOrDependencyState(id, info_or_dependency) {
        let value = false;

        const button_states = info_or_dependency === 'info' ? this.open_info_toggle_state : this.open_dependency_toggle_state;

        button_states.forEach((skv_info) => {
            if (id === skv_info.id) {
                value = skv_info.is_open;
            }
        });

        return value;
    }

    /**
     * Send out the current state of the equipment
     * @fires EQUIPMENT:STATE_CHANGE
     */
    dispatch() {
        // whenever we make changes recalculate the current state and use that
        // to determine totals, clicked equipment etc
        this.addAdditionalStateData('current');

        // generic event dispatch with the entire state of equipment
        const event = new window.CustomEvent('EQUIPMENT:STATE_CHANGE', {
            detail: {
                grouped_equipment: this.equipment_data,
                totals: this.totals_data,
                // the equipment affected by dependencies
                auto_actioned_equipment: this.states.current.arr_actioned_equipment,
                // single equipment that was clicked
                clicked_equipment: this.states.current.clicked_equipment,
                selected_options: this.selected_options,
                event_action: this.event_action,
            },
        });

        window.dispatchEvent(event);
    }

    /**
     * Loop through looking at panel group and whether its a standard or option
     * @param {array} arr_flat_equipment Array of unordered equipment
     * @param {string} panel_id String of panel id e.g paint or options-list
     * @returns {array} Array of groups (with their items) based on the panel group
     */
    static getStructuredMFgroupData(arr_flat_equipment, panel_id) {
        const arr_groups = [];

        arr_flat_equipment.forEach((skv_equipment) => {
            // standard or option
            const str_type = skv_equipment.type;

            // check we're looking at an equipment in the right panel group
            if (skv_equipment.panel_group !== panel_id) {
                return;
            }

            // see if the group already exists
            const num_group_idx = arr_groups.findIndex((skv_group) => skv_group.mf_group_name === skv_equipment.equipment_group);

            // group doesn't exist, so add it
            if (num_group_idx === -1) {
                const skv_group = {
                    id: Configurator.createGroupId(skv_equipment.equipment_group),
                    mf_group_name: skv_equipment.equipment_group,
                    standards: [],
                    options: [],
                };

                skv_group[`${str_type}s`].push(skv_equipment);
                arr_groups.push(skv_group);
            } else {
                // add the equipment to standards / options array of the existing
                // group
                arr_groups[num_group_idx][`${str_type}s`].push(skv_equipment);
            }
        });

        return arr_groups;
    }

    /**
     * Get the cost depending on whether we want net or gross, calculate effect of
     * lease terms if present, do some rounding to 2 dp
     * @param {object} skv_equipment Equipment object
     * @param {struct} skv_config Struct of config set in constructor but may subsequently be updated
     * @returns {number}
     */
    static getDisplayCost(skv_equipment, skv_config) {
        let value = skv_equipment[skv_config.cost_field];

        if (Object.keys(skv_config.skv_lease_terms).length) {
            if (skv_config.is_salsac) {
                value = Configurator.calculateSalsacCost(
                    skv_equipment,
                    skv_config.skv_lease_terms,
                );
            } else {
                value = Configurator.calculateLeaseCost(
                    value,
                    skv_config.skv_lease_terms,
                );
            }
        }

        return value.toFixed(2);
    }

    /**
     * @param {struct} skv_data Struct of standards and options structured to match the API call
     * @returns {array} Array of panels that have standards and options split up according
     * to their panel group
     */
    getPanels() {
        const arr_panels = [];

        // sort in to panels e.g paint, list
        Object.keys(this.skv_config.panels).forEach((key) => {
            const skv_panel = {
                name: key,
                groups: Configurator.getStructuredMFgroupData(this.states.current.equipment, key),
            };

            arr_panels.push(skv_panel);
        });

        return arr_panels;
    }

    /**
     * All the equipment data
     * @returns {array} Return a structured set of standards and options that match the API call
     */
    get equipment_data() {
        return this.getPanels();
    }

    /**
     * Totals of options, co2 etc
     * @returns {struct} Struct of totals
     */
    get totals_data() {
        return this.totals;
    }

    /**
     * Loop through all equipment and add up totals
     * @returns {struct} Struct of totals
     */
    get totals() {
        const skv_return = {};

        let num_total_options = 0;
        let num_total_taxable = 0;
        let num_total_gross = 0;
        let num_total_net = 0;
        // what we're changing the vehicle by
        let num_change_co2 = 0;
        // what we're changing the vehicle by
        let num_change_battery_range = 0;

        const arr_equipment = this.states.current.equipment_extra;
        const cnt_equipment = arr_equipment.length;

        for (let i = 0; i < cnt_equipment; i += 1) {
            const skv_equipment = arr_equipment[i];
            const equipment_cost = skv_equipment.display_cost;
            const { taxable_cost, cost_gross, cost_net } = skv_equipment;

            if (
                skv_equipment.is_selected
            ) {
                num_total_options += parseFloat(equipment_cost);
                num_total_gross += parseFloat(cost_gross);
                num_total_net += parseFloat(cost_net);

                if (skv_equipment.is_taxable) {
                    num_total_taxable += taxable_cost;
                }

                if (skv_equipment.change_co2) {
                    num_change_co2 += skv_equipment.change_co2;
                }
                if (skv_equipment.change_battery_range) {
                    num_change_battery_range += skv_equipment.change_battery_range;
                }
            }
        }

        skv_return.num_total_options = num_total_options;
        skv_return.num_total_taxable = num_total_taxable;
        skv_return.num_total_gross = num_total_gross;
        skv_return.num_total_net = num_total_net;

        // what we're changing the battery range by, defaults to 0
        skv_return.num_change_battery_range = num_change_battery_range;

        // if theres a valid battery range passed in add the battery change to it
        if (
            this.skv_config.skv_vehicle.battery_range
            && this.skv_config.skv_vehicle.battery_range_teh
        ) {
            skv_return.num_vehicle_battery_range = this.skv_config.skv_vehicle.battery_range
                                                    + num_change_battery_range;

            // if the battery range falls below teh value, cap it to the teh value
            if (skv_return.num_vehicle_battery_range < this.skv_config.skv_vehicle.battery_range_teh) {
                skv_return.num_vehicle_battery_range = this.skv_config.skv_vehicle.battery_range_teh;
            }

            // also check whether we should cap the battery change value
            const allowed_drop_in_range = this.skv_config.skv_vehicle.battery_range_teh - this.skv_config.skv_vehicle.battery_range;

            if (num_change_battery_range < allowed_drop_in_range) {
                skv_return.num_change_battery_range = allowed_drop_in_range;
            }
        } else {
            skv_return.num_vehicle_battery_range = '';
        }

        // what we're changing the vehicle co2 by, defaults to 0
        skv_return.num_change_co2 = num_change_co2;

        const vehicle_co2_uncapped = this.skv_config.skv_vehicle.wltp_co2 + num_change_co2;
        skv_return.num_vehicle_co2 = vehicle_co2_uncapped;

        // ensure the change co2 value is in between the TEL (Test Energy Low,
        // light vehicle more efficient) and TEH (Test Energy High, heavy
        // vehicle with options less efficient) values
        if (this.skv_config.skv_vehicle.wltp_co2_tel > vehicle_co2_uncapped) {
            skv_return.num_vehicle_co2 = this.skv_config.skv_vehicle.wltp_co2_tel;
        }

        if (this.skv_config.skv_vehicle.wltp_co2_teh < vehicle_co2_uncapped) {
            skv_return.num_vehicle_co2 = this.skv_config.skv_vehicle.wltp_co2_teh;
        }

        // if a total price limit is set check whether we've exceeded it
        if (
            typeof this.skv_config.max_gross_total_price_limit === 'number'
            && skv_return.num_total_gross > this.skv_config.max_gross_total_price_limit
        ) {
            skv_return.is_total_gross_limit_exceeded = true;
        } else {
            skv_return.is_total_gross_limit_exceeded = false;
        }

        return skv_return;
    }

    /**
     * Loop over the equipment to see which options are selected, run after
     * dependencies have been handled
     * @returns {array} Array of selected options
     */
    get selected_options() {
        // current equipment state
        const arr_equipment = this.states.current.equipment_extra;
        // array of equipment ids
        const arr_selected_ids = this.states.current.arr_selected_option_ids;

        const cnt_selected_ids = arr_selected_ids.length;
        const arr_return = [];

        for (let i = 0; i < cnt_selected_ids; i += 1) {
            const id = arr_selected_ids[i];
            // use the map to get the index in the flat equipment array
            const equipment_idx = this.equipment_map[id];
            // push in to an array to return
            const skv_equipment = arr_equipment[equipment_idx];
            arr_return.push(skv_equipment);
        }

        return arr_return;
    }

    /**
     * Build a group id based on the mf group name
     * @param {string} str_name Name of group
     * @returns {string} id
     */
    static createGroupId(str_name) {
        // replace all alphanumeric characters with a dash
        let str_id = str_name;

        // get everything that isn't alphanumeric or is a dash surrounded by spaces
        const regex = /( - )|([^a-zA-Z0-9])/gi;
        // replace with a dash
        str_id = str_id.replace(regex, '-');
        // now lowercase everything
        str_id = str_id.toLowerCase();

        return str_id;
    }
}

export {
    Configurator,
};