import * as actionTypes from './Actions';
import get from 'lodash/get';
import isObject from 'lodash/isObject';
import mergeWith from 'lodash/mergeWith';
import merge from 'lodash/merge';
import filter from 'lodash/filter';
import cloneDeep from 'lodash/cloneDeep';
import isArray from 'lodash/isArray';
import find from 'lodash/find';
import findIndex from 'lodash/findIndex';
import omit from 'lodash/omit';
import forEach from 'lodash/forEach';
import reduce from 'lodash/reduce';
import immutableSet from 'Utils/Immutable/set';
import { designDataHandler } from '../DesignData/Reducer';

const initialState = {
    configuration: {
        item: {},
        optionclassifications: [],
    },
    options: [],
    optionsWithDetails: [],
    selectedoptionclassification: {},
    calculation: {},
    calculationAutoResolve: true,
    optiondetails: {},
    validation: null,
    configurationvariants: [],
    codesnippets: [],
    client: null,
    inspirations: {},
    profile: {},
    loadCode: null,
    loadIdentifier: null,
    resetCode: null, // this code is only set at initial loading and load by code - we reset to those configurations
    stockInformationOption: null,
    inspirationsIdentifier: null,
    calculationIsEnabled: true,
    checkedOptionclassifications: {},
    filteredOptionclassifications: [],
    selectedVariantIdentifier: null,
    selectedAmountsUpdated: false,
    optionInputTexts: [],
};

export function configurationHandler(state, action) {
    switch (action.type) {
        case actionTypes.RECEIVE_CONFIGURATION: {
            const configuration = action.data;

            if (!isObject(configuration)) {
                throw new Error(
                    `Server side error! The configuration must be a JSON structure! Data: ${configuration}`
                );
            }

            // update the selected option classification
            let { selectedoptionclassification } = state;
            if (selectedoptionclassification.identifier) {
                selectedoptionclassification =
                    find(configuration.optionclassifications, {
                        identifier: selectedoptionclassification.identifier,
                    }) || selectedoptionclassification;
            }

            // set validation from the received configuration
            const validation = get(configuration, 'information.validation_result');

            return {
                ...state,
                selectedoptionclassification,
                configuration,
                validation,
                resetCode: configuration.code,
                inspirationsIdentifier: configuration.item.identifier,
                checkedOptionclassifications: {},
            };
        }

        case actionTypes.RECEIVE_SWITCHOPTIONSRESULT: {
            const configuration = action.data;

            // the selectedoptionclassification should be updated with the optionclassification data from the response
            // eg. to keep the selectedoptions updated
            let { selectedoptionclassification, options } = state;
            const { optionclassifications } = configuration;
            if (selectedoptionclassification.identifier) {
                const optionclassificationData = find(optionclassifications, {
                    identifier: selectedoptionclassification.identifier,
                });
                if (optionclassificationData) {
                    selectedoptionclassification = optionclassificationData;

                    // the options should be updated with the data from the selectedoptions
                    const { selectedoptions } = selectedoptionclassification;
                    for (let soi in selectedoptions) {
                        for (let oi in options) {
                            if (selectedoptions[soi].identifier === options[oi].identifier) {
                                options[oi] = mergeWith(
                                    options[oi], // dst
                                    selectedoptions[soi], // src
                                    (dstValue, srcValue) => {
                                        // if the value present in the src, then copy it from there
                                        // selectedoption always rules
                                        if (srcValue !== null && srcValue !== undefined) {
                                            return srcValue;
                                        }
                                        return dstValue;
                                    }
                                );
                            }
                        }
                    }
                }
            }

            const optionsWithDetails = filterOptionsWithDetails(options);

            return {
                ...state,
                configuration,
                options,
                optionsWithDetails,
                selectedoptionclassification,
            };
        }

        case actionTypes.RECEIVE_SAVEDCONFIGURATION: {
            const configuration = action.data;

            // update the selected option classification
            let { selectedoptionclassification } = state;
            if (selectedoptionclassification.identifier) {
                selectedoptionclassification =
                    find(configuration.optionclassifications, {
                        identifier: selectedoptionclassification.identifier,
                    }) || selectedoptionclassification;
            }

            return {
                ...state,
                configuration,
                selectedoptionclassification,
            };
        }

        default:
            return state;
    }
}

export function optionAndOptionclassificationHandler(state, action) {
    switch (action.type) {
        case actionTypes.SELECT_OPTIONCLASSIFICATION: {
            return {
                ...state,
                selectedoptionclassification: action.optionclassification || {},
                options: [], // clear options
                optionsWithDetails: [],
            };
        }

        case actionTypes.RECEIVE_OPTIONS: {
            const options = cloneDeep(action.data);

            // the selected options of the selected optionclassification should be updated
            // eg. after the switchoption call added a partial object (it contains only the identifier and amount)
            // but only missing values are updated
            const keepDstProps = ['amount'];
            const selectedoptionclassification = cloneDeep(state.selectedoptionclassification);
            for (let oi in options) {
                const option = options[oi];
                const { selectedoptions } = selectedoptionclassification;
                for (let os in selectedoptions) {
                    const selectedoption = selectedoptions[os];
                    if (selectedoption.identifier === option.identifier) {
                        options[oi] = selectedoptions[os] = mergeWith(
                            selectedoptions[os], // dst
                            option, // src
                            (dstValue, srcValue, key) => {
                                // update only when there is a real value in the source, otherwise use the original
                                if (
                                    keepDstProps.indexOf(key) === -1 &&
                                    srcValue !== null &&
                                    srcValue !== undefined
                                ) {
                                    return srcValue;
                                }
                                return dstValue;
                            }
                        );
                    }
                }
            }

            const { selectedoptions } = selectedoptionclassification;

            // update the option data with the selected group item
            for (let os in selectedoptions) {
                const selectedoption = selectedoptions[os];
                for (let oi in options) {
                    const itemGroupChildren = get(options[oi], 'option_group.children');
                    if (itemGroupChildren instanceof Array) {
                        for (let ci in itemGroupChildren) {
                            if (itemGroupChildren[ci].identifier === selectedoption.identifier) {
                                options[oi] = mergeWith(
                                    cloneDeep(options[oi]), // dst
                                    itemGroupChildren[ci], // src
                                    (dstValue, srcValue) =>
                                        // if the value is present in the src then use it, otherwise leave the original
                                        srcValue !== null && srcValue !== undefined ? srcValue : dstValue
                                );
                            }
                        }
                    }
                }
            }

            const optionsWithDetails = filterOptionsWithDetails(options);

            return {
                ...state,
                selectedoptionclassification,
                options,
                optionsWithDetails,
            };
        }

        case actionTypes.SET_OPTIONCLASSIFICATION_CHECKED: {
            if (typeof action.optionclassification === 'object') {
                return {
                    ...state,
                    checkedOptionclassifications: {
                        ...state.checkedOptionclassifications,
                        [action.optionclassification.identifier]: true,
                    },
                };
            }
            return state;
        }

        case actionTypes.SET_CONFLICTING_SWITCHOPTION_DATA: {
            return {
                ...state,
                conflictingSwitchOptionData: action.switchOptionData,
            };
        }

        default:
            return state;
    }
}

export function calculationHandler(state, action) {
    switch (action.type) {
        case actionTypes.RECEIVE_CALCULATION: {
            const { data: calculation } = action;
            return {
                ...state,
                calculation,
                calculationAutoResolve:
                    (state.calculationAutoResolve === 'skipNext' && !calculation.errors) ||
                    state.calculationAutoResolve,
            };
        }

        case actionTypes.INVALIDATE_CALCULATION: {
            return {
                ...state,
                calculation: {
                    ...state.calculation,
                    invalidated: true,
                },
            };
        }

        case actionTypes.SET_CALCULATION_ENABLED: {
            return {
                ...state,
                calculationIsEnabled: action.enabled,
            };
        }

        case actionTypes.SET_CALCULATION_ERRORS_RESOLVED: {
            return {
                ...state,
                calculation: state.calculation.errors ? null : state.calculation,
            };
        }

        case actionTypes.SET_CALCULATION_AUTO_RESOLVE_ENABLED: {
            return {
                ...state,
                calculationAutoResolve: action.enabled,
            };
        }

        default:
            return state;
    }
}

export function dataHandler(state, action) {
    switch (action.type) {
        case actionTypes.RECEIVE_OPTIONDETAILS: {
            return {
                ...state,
                optiondetails: action.data,
            };
        }

        case actionTypes.RECEIVE_VALIDATION: {
            return {
                ...state,
                validation: action.data,
            };
        }

        case actionTypes.RECEIVE_CONFIGURATIONVARIANTS: {
            return {
                ...state,
                configurationvariants: action.data,
            };
        }

        case actionTypes.RECEIVE_CODESNIPPETS: {
            if (!isArray(action.data)) {
                throw new Error(
                    `Server side error! The code snippets must be a JSON array! Data: ${action.data}`
                );
            }

            return {
                ...state,
                codesnippets: action.data || [],
            };
        }

        case actionTypes.RECEIVE_CLIENT_DATA: {
            if (action.data.success === false) {
                return state;
            }
            const clientData = action.data;
            let color = get(clientData, 'theme.highlightColor');
            if (/^[0-9a-fA-F]{6}$/i.test(color)) {
                // if color is hex with missing '#'
                color = '#' + color;
            } else if (color.charAt(0) !== '#') {
                // if color string to get rgb color code
                const element = document.createElement('div');
                document.body.appendChild(element);
                element.style.color = color;
                const styles = window.getComputedStyle(element);
                color = styles.color;
                document.body.removeChild(element);
            }
            return {
                ...state,
                client: {
                    ...clientData,
                    theme: {
                        ...clientData.theme,
                        highlightColor: color,
                    },
                },
            };
        }

        case actionTypes.RECEIVE_INSPIRATIONS: {
            return {
                ...state,
                inspirations: action.data,
            };
        }

        default:
            return state;
    }
}

export function uiHandler(state, action) {
    switch (action.type) {
        case actionTypes.SET_STOCK_INFORMATION_OPTION: {
            return {
                ...state,
                stockInformationOption: action.stockInformationOption,
            };
        }

        case actionTypes.SET_SELECTED_VARIANT_IDENTIFIER: {
            return {
                ...state,
                selectedVariantIdentifier: action.selectedVariantIdentifier,
            };
        }

        default:
            return state;
    }
}

export function baseHandler(state, action) {
    switch (action.type) {
        case actionTypes.SET_LOAD_CODE: {
            return {
                ...state,
                loadCode: action.loadCode,
                loadIdentifier: null,
            };
        }

        case actionTypes.SET_LOAD_IDENTIFIER: {
            return {
                ...state,
                loadIdentifier: action.loadIdentifier,
                loadCode: null,
            };
        }

        case actionTypes.SET_SELECTED_AMOUNT: {
            const selectedAmountsPath = 'customdata.selectedAmounts';
            const selectedAmounts = get(state.configuration, selectedAmountsPath, {});
            const configuration = immutableSet(
                state.configuration,
                selectedAmountsPath,
                action.amount === 0
                    ? omit(selectedAmounts, action.identifier)
                    : { ...selectedAmounts, [action.identifier]: action.amount }
            );
            return {
                ...state,
                selectedAmountsUpdated: !!action.isUpdatedByUser,
                configuration,
            };
        }

        case actionTypes.SWITCH_SELECTED_AMOUNT: {
            const { srcIdentifier, dstIdentifier } = action;
            const selectedAmountsPath = 'customdata.selectedAmounts';
            const selectedAmounts = get(state.configuration, selectedAmountsPath, {});
            if (!(srcIdentifier in selectedAmounts)) {
                return state;
            }
            const configuration = immutableSet(
                state.configuration,
                selectedAmountsPath,
                omit({ ...selectedAmounts, [dstIdentifier]: selectedAmounts[srcIdentifier] }, srcIdentifier)
            );
            return {
                ...state,
                configuration,
            };
        }

        case actionTypes.SET_SELECTED_AMOUNTS: {
            const selectedAmountsPath = 'configuration.customdata.selectedAmounts';
            const selectedAmounts = get(state, selectedAmountsPath, {});
            const nextAmounts = { ...selectedAmounts, ...action.sizes };
            const newState = { ...state, selectedAmountsUpdated: !!action.isUpdatedByUser };
            return immutableSet(newState, selectedAmountsPath, {
                ...reduce(
                    nextAmounts,
                    (selectedAmounts, amount, identifier) => {
                        if (amount !== 0) {
                            return { ...selectedAmounts, [identifier]: amount };
                        } else {
                            return selectedAmounts;
                        }
                    },
                    {}
                ),
            });
        }
        case actionTypes.DELETE_SELECTED_AMOUNTS: {
            const selectedAmountsPath = 'configuration.customdata.selectedAmounts';
            const selectedAmounts = cloneDeep(get(state, selectedAmountsPath, {}));
            forEach(selectedAmounts, (selectedAmount, identifier) => {
                const variantIdentifier = find(action.variant.sizes, { identifier: identifier });
                if (variantIdentifier) {
                    delete selectedAmounts[variantIdentifier.identifier];
                }
            });
            return immutableSet(state, selectedAmountsPath, selectedAmounts);
        }
        default:
            return state;
    }
}

export function configurationModifierHandler(state, action) {
    switch (action.type) {
        case actionTypes.SET_CUSTOM_TITLE: {
            return {
                ...state,
                configuration: {
                    ...state.configuration,
                    customdata: {
                        ...state.configuration.customdata,
                        title: action.title,
                    },
                },
            };
        }

        case actionTypes.SET_COMMENT: {
            return {
                ...state,
                configuration: {
                    ...state.configuration,
                    customdata: {
                        ...state.configuration.customdata,
                        comment: action.comment,
                    },
                },
            };
        }

        case actionTypes.CLEAR_BULKNAMES: {
            return immutableSet(state, 'configuration.customdata.bulkNames', {});
        }

        case actionTypes.SAVE_BULKNAMES: {
            const bulkNamesPath = 'configuration.customdata.bulkNames';
            const bulkNames = get(state, bulkNamesPath, {});
            return immutableSet(state, bulkNamesPath, {
                ...bulkNames,
                [action.identifier]: action.bulkNamesData,
            });
        }
        case actionTypes.DELETE_SELECTED_AMOUNT: {
            const selectedAmountsPath = 'configuration.customdata.selectedAmounts';
            const selectedAmounts = cloneDeep(get(state, selectedAmountsPath, {}));

            delete selectedAmounts[action.identifier];

            return immutableSet(state, selectedAmountsPath, selectedAmounts);
        }
        case actionTypes.SET_SELECTED_CALCULATION: {
            return immutableSet(
                state,
                `configuration.customdata.userSelectedCalculation.${action.identifier}`,
                action.value
            );
        }

        case actionTypes.SET_OPTION_INPUT_TEXT: {
            const optionInputTexts = state.optionInputTexts;
            const optionInputTextIndex = findIndex(optionInputTexts, ['optionIdentifier', action.identifier]);
            if (optionInputTextIndex !== -1) {
                optionInputTexts[optionInputTextIndex] = {
                    optionIdentifier: action.identifier,
                    inputText: action.value,
                };
            } else if (optionInputTextIndex === -1 && action.value) {
                optionInputTexts.push({
                    optionIdentifier: action.identifier,
                    inputText: action.value,
                });
            }
            return {
                ...state,
                optionInputTexts,
            };
        }

        case actionTypes.RESET_OPTION_INPUT_TEXTS: {
            const optionInputTexts = [];

            return {
                ...state,
                optionInputTexts,
            };
        }

        default:
            return state;
    }
}

export function filterOptionclassifications(state, prevState) {
    // if the optionclassifications has changed it should be re-filtered
    if (prevState.configuration.optionclassifications !== state.configuration.optionclassifications) {
        return {
            ...state,
            filteredOptionclassifications: state.configuration.optionclassifications,
        };
    }
}

const subReducers = {
    baseHandler,
    configurationHandler,
    optionAndOptionclassificationHandler,
    calculationHandler,
    dataHandler,
    uiHandler,
    configurationModifierHandler,
    designDataHandler,
};

const stateChangeReducers = {
    filterOptionclassifications,
};

let reducerArray = Object.values(subReducers);
let stateReducerArray = Object.values(stateChangeReducers);

export const setSubReducers = customSubReducers => {
    merge(subReducers, customSubReducers);
    reducerArray = Object.values(subReducers);
};

export const setStateChangeReducers = customStateChangeReducers => {
    merge(stateChangeReducers, customStateChangeReducers);
    stateReducerArray = Object.values(stateChangeReducers);
};

export default function configuratorReducer(prevState = initialState, action = {}) {
    // apply the reducers after each other (from left to right)
    // the idea comes from reduce-reducers package (https://github.com/redux-utilities/reduce-reducers)
    const nextState = reducerArray.reduce(
        (newState, reducer) => reducer(newState, action) || newState,
        prevState
    );

    // apply the state change reducers after each other (from left to right)
    const state = stateReducerArray.reduce(
        (newState, reducer) => reducer(newState, prevState, action) || newState,
        nextState
    );

    return state;
}

function filterOptionsWithDetails(options) {
    return filter(
        options,
        option => !!(option.abstract || option.description || option.detailImageExists === true)
    );
}
