equipment/state.js

/**
 * Creates a new State of equipment
 * @class
 * @since  0.0.7
 */
class State {
    /**
     * @param {struct} equipment
     * @param {map} equipment_map
     */
    constructor(
        equipment,
        equipment_map,
    ) {
        // this.eventEmitter = new EventEmitter( 'options' );

        this.selected = [];
        // all equipment has its relevant selected and disabled properties set depending its dependencies
        // this holds equipment that is meaningfully changed i.e if the selection is changed from a previously
        // selection
        this.arr_actioned_equipment = [];

        this.totals = {
            costGross: 0,
            costNet: 0,
            costTax: 0,
        };

        this.equipment = equipment;
        this.equipment_map = equipment_map;
        this.clicked_equipment = {};
        this.arr_selected_option_ids = this.getSelectedOptionIDs();
    }

    /**
     * Handle interaction
     * @param {number} interaction_idx equipment id
     * @param {boolean} is_initializing whether we're currently setting up the configurator
     */
    process(interaction_idx, is_initializing = false) {
        // make deep copy otherwise undo state updates along with current state
        // let clicked_equipment = <Equipment>$.extend(true, {}, this.equipment[ interaction_idx ]);
        this.clicked_equipment = this.equipment[interaction_idx];

        // toggle this item, we don't need to swap the selected state if we are
        // initializing the tool and its dependencies
        if (!is_initializing) {
            this.clicked_equipment.is_selected = !this.clicked_equipment.is_selected;
        }

        // flag user interaction
        this.clicked_equipment.user_selected = this.clicked_equipment.is_selected;

        // so the user can't deselect
        if (this.clicked_equipment.is_swatch) {
            this.clicked_equipment.is_enabled = false;
        }

        // reset the equipment that is being actioned so we don't get multiples
        this.arr_actioned_equipment = [];

        this.handleDependencies(this.clicked_equipment, []);

        // for returning just the selected options
        this.arr_selected_option_ids = this.getSelectedOptionIDs();
    }

    /* eslint-disable no-param-reassign */
    /* eslint-disable no-continue */
    /**
     * Handle dependencies
     * @param equipment
     * @param already_handled
     */
    handleDependencies(equipment, already_handled) {
        // loop over affects relationships (what does this affect)
        this.processAffects(equipment, already_handled);

        // loop over affecting relationships (what will affect this)
        this.processAffectedBy(equipment, already_handled);
    }

    /**
     * Loop over the equipment to handle what this affects (what does this affect)
     */
    processAffects(equipment, already_handled) {
        for (let i = 0, cnt = equipment.affects.length; i < cnt; i += 1) {
            const dependency = equipment.affects[i];
            const affected_id = dependency.id;
            const affected_relationship = dependency.relationship;

            // get the affected equipment, set a flag to say if the dependency was actioned
            const affected = this.equipment[this.equipment_map[affected_id]];
            let affect_actioned = false;

            // skip equipment if it's already been handled
            if (already_handled.indexOf(affected) !== -1) {
                continue;
            }

            switch (affected_relationship) {
            case 'require': {
                if (equipment.is_selected) {
                    // if selecting, tick and disable required item
                    affected.is_selected = true;
                    affected.is_enabled = false;

                    affect_actioned = true;
                } else if (!affected.user_selected) {
                    // if deselecting, renable and deselect
                    // required item ONLY IF user_interaction is false
                    affected.is_selected = false;
                    affected.is_enabled = true;

                    affect_actioned = true;
                } else {
                    // renable but leave selection state as it was
                    affected.is_enabled = true;
                }
                break;
            }
            case 'require1ofA':
            case 'require1ofB':
            case 'require1ofC':
            case 'require1ofD': {
                // if the affected equipment is required and selected,
                // disable it to stop it being deselected
                if (equipment.is_selected && affected.is_selected) {
                    affected.is_enabled = false;
                } else if (!equipment.is_selected) {
                    // if deselecting, renable and deselect
                    // required item ONLY IF user_interaction is false

                    if (!affected.user_selected) {
                        affected.is_selected = false;
                    }

                    affected.is_enabled = true;

                    affect_actioned = true;
                }
                break;
            }
            case 'require1ofAdef':
            case 'require1ofBdef':
            case 'require1ofCdef':
            case 'require1ofDdef': {
                const non_def_relationship = affected_relationship.replace(/def$/, '');

                // this needs to ONLY take effect if there is no
                // require1ofX set
                if (equipment.is_selected) {
                    if (!equipment.isDependencyResolved(non_def_relationship)) {
                        // if selecting, tick and disable required item
                        affected.is_selected = true;

                        affect_actioned = true;

                        this.arr_actioned_equipment.push(affected);

                        equipment.resolveDependency(
                            non_def_relationship,
                            true,
                            affected.id,
                        );
                    }

                    // if the affected is selected it will need disabling
                    if (affected.is_selected) {
                        affected.is_enabled = false;
                    }
                } else {
                    // if deselecting, renable and deselect
                    // required item ONLY IF user_interaction is false
                    if (!affected.user_selected) {
                        affected.is_selected = false;

                        equipment.resolveDependency(
                            non_def_relationship,
                            false,
                            affected.id,
                        );
                    }
                    affected.is_enabled = true;

                    affect_actioned = true;
                }
                break;
            }
            case 'excludeEnable': {
                // deselect but leave it enabled
                // (common 17inch/18inch wheels scenario)
                if (equipment.is_selected) {
                    if (affected.is_selected) {
                        this.arr_actioned_equipment.push(affected);
                    }

                    affected.is_selected = false;
                    affected.is_enabled = true;

                    affect_actioned = true;
                }
                break;
            }
            case 'replace': {
                // when replacing, it means it's a direct swap.
                // currently used in packed paint, when replacing
                // a TBC with a color.
                // when the color is unticked we have to put the
                // tbc back
                if (equipment.is_selected) {
                    if (affected.is_selected) {
                        this.arr_actioned_equipment.push(affected);
                    }

                    // deselect the original but enable it to
                    // undo the replacement
                    affected.is_selected = false;
                    affected.is_enabled = true;

                    // disable replacement, cannot untick, must re-tick
                    // original or a different replacement
                    equipment.is_enabled = false;

                    affect_actioned = true;

                    affected.resolveDependency(
                        affected_relationship,
                        true,
                        equipment.id,
                    );
                }

                break;
            }
            default: {
                // Unhandled relationship
                // this is ok, we don't handle them all here
            }
            }

            // keep handling dependencies
            if (affect_actioned && affected.affects.length) {
                already_handled.push(equipment);
                this.handleDependencies(affected, already_handled);
            }
        }
    }

    /**
     * Loop over the equipment to handle affecting relationships (what will affect this)
     */
    processAffectedBy(equipment, already_handled) {
        for (let i = 0, cnt = equipment.affected_by.length; i < cnt; i += 1) {
            const dependency = equipment.affected_by[i];
            const affecting_id = dependency.id;
            const affecting_relationship = dependency.relationship;

            // get the affecting equipment, set a flag to say if the dependency was actioned
            const affecting = this.equipment[this.equipment_map[affecting_id]];
            let affect_actioned = false;

            // skip equipment if it's already been handled
            if (already_handled.indexOf(affecting) !== -1) {
                continue;
            }

            // Where a user chooses a CAP packed vehicle that has a special
            // residual value, the packed element must not be deslectable.
            // otherwise this will break the assumptions on how package options
            // are priced
            if (equipment.matchtype_name === 'pack') {
                // if this is required in a group by another option
                // we need to let that option know
                if (equipment.is_selected) {
                    affect_actioned = true;
                    affecting.is_enabled = false;
                    affecting.show_cost = false;
                }
            } else {
                switch (affecting_relationship) {
                case 'require1ofA':
                case 'require1ofB':
                case 'require1ofC':
                case 'require1ofD':
                    // if this is required in a group by another option
                    // we need to let that option know
                    if (equipment.is_selected) {
                        affect_actioned = true;

                        affecting.resolveDependency(
                            affecting_relationship,
                            true,
                            equipment.id,
                        );

                        // if this is required and the affecting equipment
                        // is selected, we need to disable it
                        if (affecting.is_selected) {
                            equipment.is_enabled = false;
                        } else {
                            equipment.is_enabled = true;
                        }
                    }
                    break;
                case 'require1ofAdef':
                case 'require1ofBdef':
                case 'require1ofCdef':
                case 'require1ofDdef':

                    // if this is required in a group by another option
                    // we need to let that option know
                    if (equipment.is_selected) {
                        affect_actioned = true;

                        affecting.resolveDependency(
                            affecting_relationship,
                            true,
                            equipment.id,
                        );

                        // if this is required and the affecting equipment
                        // is selected, we need to disable it
                        if (affecting.is_selected) {
                            equipment.is_enabled = false;
                        } else {
                            equipment.is_enabled = true;
                        }
                    }
                    break;
                case 'replace':
                    if (equipment.is_selected) {
                        equipment.is_enabled = false;
                    }
                    break;
                default:
                    // Unhandled relationship
                    // this is ok, we don't handle them all here
                }
            }
            // keep handling dependencies
            if (affect_actioned && equipment.affects.length) {
                already_handled.push(affecting);
                this.handleDependencies(equipment, already_handled);
            }
        }
    }

    /**
     * Loop over the equipment to see which options are selected, run after
     * dependencies have been handled
     * @returns {array} Array of selected options
     */
    getSelectedOptionIDs() {
        // loop over affects relationships (what does this affect)
        const cnt_equipment = this.equipment.length;
        const arr_return = [];
        for (let i = 0; i < cnt_equipment; i += 1) {
            const skv_equipment = this.equipment[i];

            if (
                (
                    skv_equipment.is_selected
                    // && !skv_equipment.is_packed
                    && skv_equipment.type === 'option'
                ) || (
                    skv_equipment.is_selected
                    && skv_equipment.user_selected
                    && skv_equipment.type === 'standard'
                )
            ) {
                arr_return.push(skv_equipment.id);
            }
        }

        return arr_return;
    }

    /* eslint-enable no-continue */
    /* eslint-enable no-param-reassign */
}

export {
    State,
};