import get from 'lodash/get';
import find from 'lodash/find';
import map from 'lodash/map';
import keys from 'lodash/keys';
import isEqual from 'lodash/isEqual';
import difference from 'lodash/difference';
import omit from 'lodash/omit';
import debounce from 'lodash/debounce';
import AbstractConfiguratorService from '../../Framework/Services/AbstractConfiguratorService';
import * as configuratorActions from '../Reducers/Configurator/Actions';
import {
    getItem,
    getSelectedAmounts,
    getSelectedVariantIdentifier,
    isSelectedAmountsUpdated,
    hasMultipleVariantsSelected,
    getDeepestChildIdentifiers,
    isCalculationAutoResolveEnabled,
    getConfiguration,
    getChildren,
} from '../Reducers/Configurator/Selectors';
import {
    getCameraViewListForDesignAreaList,
    getDefaultDesignAreaIdentifierFromProductionMethod,
    getDesignViewListForDesignAreas,
} from '../Reducers/DesignArea/Selectors';
import { withGlobalLoader } from '../Reducers/UI/Actions';
import { getFontMapping } from '../Reducers/Fonts/Selectors';
import { Services } from '../ServiceLocator';
import { getConf, SaveTypes } from '../configuration';
import { selectMultipleOptions, setCalculationEnabled } from '../Reducers/Configurator/Actions';
import { setCalculationAutoResolveEnabled } from '../Reducers/Configurator/Actions';
import { skipNextAutoResolve } from '../Reducers/Configurator/Actions';
import { getDesignData } from '../Reducers/DesignData/Reducer';
import { getCached } from '../../Utils/Function/cached';
import { getFirstDataURLFromScreenshotsObj } from '../Modules/Visualization/Utils/getFirstDataURLFromScreenshotsObj';
import { isPlaceholderObject } from '../Reducers/DesignData/Selectors';
import createMessageInterface, {
    LISTENER_SAVE_CONFIGURATION,
    LISTENER_SWITCH_OPTIONS,
    METHOD_ADD_TO_CART,
    METHOD_CONFIGURATION_SAVED_REMOTELY,
} from '../Utils/createMessageInterface';
import { prepareDesignDataSnapshots, preparePrintDesignData } from '../Modules/Designer/Utils/SvgFixers';

const getSelectedAmountsBySizes = (item, selectedAmounts) => {
    const itemGroupMap = (rootItem, fn) => {
        const data = [];
        if (get(rootItem, 'itemGroup.children.length', 0) > 0) {
            for (let item of rootItem.itemGroup.children) {
                const mappedData = fn(item, data);
                mappedData && data.push(mappedData);
            }
        }
        return data;
    };

    return itemGroupMap(item, colorGroupItem => ({
        identifier: colorGroupItem.identifier,
        amount: get(selectedAmounts, colorGroupItem.identifier, 0),
        sizes: itemGroupMap(colorGroupItem, sizeGroupItem => ({
            amount: get(selectedAmounts, sizeGroupItem.identifier, 0),
            identifier: sizeGroupItem.identifier,
        })),
    }));
};

export default class ConfiguratorService extends AbstractConfiguratorService {
    constructor() {
        super();

        Services.store.observeStore(
            state => state.configurator.configuration,
            (() => {
                // since it's debounced, the prevConfiguration should be the configuration from the latest call
                let prevConfiguration = {};
                return debounce(configuration => {
                    this.onConfigurationChange(configuration, prevConfiguration);
                    prevConfiguration = configuration;
                }, 300);
            })()
        );
    }

    init() {
        const adminMode = getConf('adminMode');
        if (!adminMode) {
            const messageInterface = createMessageInterface();
            messageInterface.addListener(LISTENER_SWITCH_OPTIONS, async event => {
                const optionsJSON = get(event, 'data.options');
                try {
                    const optionsToSelect = JSON.parse(optionsJSON);
                    Services.store.dispatch(selectMultipleOptions(optionsToSelect));
                } catch (e) {
                    // eslint-disable-next-line no-console
                    console.error('Wrong JSON provided for switch_options:', optionsJSON);
                }
            });
            messageInterface.addListener(LISTENER_SAVE_CONFIGURATION, async event => {
                const save_id = get(event, 'data.save_id');
                const saveType =
                    get(event, 'data.save_type') === SaveTypes.CART ? SaveTypes.CART : SaveTypes.USER;
                try {
                    const configuration = await this.captureAndSaveConfiguration(saveType);
                    const code = get(configuration, 'data.code');
                    messageInterface.dispatch({ method: METHOD_CONFIGURATION_SAVED_REMOTELY, save_id, code });
                } catch (e) {
                    // eslint-disable-next-line no-console
                    console.error('Saving configuration failed, save_id was  ', save_id);
                }
            });
        }
    }

    // state selector
    get state() {
        return Services.store.state.configurator;
    }

    initialize() {
        const params = {};
        const basePath = getConf('routingBasePath', '/');
        const itemRegex = new RegExp(`${basePath}(code|identifier):([^/]+)`);
        const results = itemRegex.exec(location.pathname);
        if (results) {
            params[results[1]] = results[2];
        }
        if (params.identifier) {
            this.setLoadIdentifier(params.identifier);
        } else if (params.code) {
            this.setLoadCode(params.code);
        }
        this.loadClientData();
    }

    prepareConfigurationForSave(overwrite = false, extraData) {
        const { configuration } = this.state;
        const updatedConfiguration = Services.designData.removeAllDesignAreaPlaceholders(configuration);

        // remove the code by default (to prevent overwriting the current configuration)
        const { code, ...otherData } = updatedConfiguration;

        // always remove parentCode from customdata
        const customdata = omit(updatedConfiguration.customdata || {}, ['parentCode']);

        // add parentCode only when it's set in the config
        if (getConf('addParentCodeOnSave')) {
            customdata.parentCode = code;
        }

        return {
            ...otherData,
            // add code only when the current configuration should be overwritten
            ...(overwrite ? { code } : {}),
            ...extraData,
            customdata,
        };
    }

    setCalculationAutoResolveEnabled(enabled) {
        Services.store.dispatch(setCalculationAutoResolveEnabled(enabled));
    }

    skipNextCalculationAutoResolve() {
        Services.store.dispatch(skipNextAutoResolve());
    }

    async onConfigurationChange(configuration, prevConfiguration) {
        if (
            this.state.calculationIsEnabled &&
            (!configuration.check_results || // no check results
                configuration.check_results.status) && // or the check result status is ok
            this.shouldInvalidateCalculation(prevConfiguration, configuration)
        ) {
            if (isCalculationAutoResolveEnabled(this.state)) {
                // disable automatic price updates to take direct control over error handling
                Services.store.dispatch(setCalculationEnabled(false));
                // triggering the calculation directly to make sure it's synced
                await Services.store.dispatch(configuratorActions.doCalculation());
                Services.store.dispatch(setCalculationEnabled(true));
            } else {
                Services.store.dispatch(configuratorActions.invalidateCalculation());
                Services.store.dispatch(configuratorActions.doCalculation());
            }
        }

        if (this.shouldUpdateVisualization(prevConfiguration, configuration)) {
            Services.visualization.update(configuration);
        }
    }

    shouldInvalidateCalculation(prevConfiguration, configuration) {
        return this.isRoughEqual(configuration, prevConfiguration);
    }

    shouldUpdateVisualization(prevConfiguration, configuration) {
        return this.isRoughEqual(configuration, prevConfiguration);
    }

    isRoughEqual(configuration, prevConfiguration) {
        // calculation should be invalidated only when one of the relevant properties has changed
        const pc = this.getRoughConfiguration(prevConfiguration);
        const cc = this.getRoughConfiguration(configuration);
        return !isEqual(cc, pc);
    }

    /**
     * Returns the configuration core values
     * Use this method to create a more simple comparable object that represents the same
     * configuration
     * @param configuration
     * @returns {{item: {identifier: *, price: *}, optionclassifications: Array, designdata: {}, customdata: {}}}
     */
    getRoughConfiguration(configuration) {
        const { item, optionclassifications, designdata } = configuration;
        const customdata = configuration.customdata && omit(configuration.customdata, ['comment']);

        return {
            item: item && {
                identifier: item.identifier,
                price: item.price,
            },
            optionclassifications:
                optionclassifications &&
                map(optionclassifications, oc => ({
                    identifier: oc.identifier,
                    selectedoptions: map(oc.selectedoptions, o => ({
                        identifier: o.identifier,
                        amount: o.amount,
                        price: o.price,
                    })),
                })),
            designdata: designdata && this.getRoughDesignData(designdata),
            customdata,
        };
    }

    /**
     * The design data can also invalidate the calculation if any of the relevant properties have changed.
     * (Eg. an object is added or removed, or a design area related option has changed.)
     * @param designData
     * @returns {null|object}
     */
    getRoughDesignData(designData) {
        if (!designData) {
            return null;
        }
        const designAreas = Object.keys(designData);
        const roughDesignData = {};
        for (let designAreaIdentifier of designAreas) {
            const designAreaData = designData[designAreaIdentifier];
            const { canvasData, designProductionMethodIdentifier, colorAmount } = designAreaData;
            roughDesignData[designAreaIdentifier] = {
                objectCount: get(canvasData, 'objects.length'),
                designProductionMethodIdentifier,
                colorAmount,
                objects: this.getRoughDesignDataObjectDetails(designAreaData),
            };
        }
        return roughDesignData;
    }

    getRoughDesignDataObjectDetails(designData) {
        const includeMetricData = getConf('designer.includeMetricContentSize', false);
        return get(designData, 'canvasData.objects', []).map(object => ({
            uuid: object.uuid,
            ...(includeMetricData
                ? {
                      x: object.metric?.x || 0,
                      y: object.metric?.y || 0,
                      width: object.metric?.width || 0,
                      height: object.metric?.height || 0,
                      rotation: object.rotation || 0,
                  }
                : {}),
        }));
    }

    //// <-- ACTIONS -->

    async saveConfiguration(configuration, configurationtype) {
        return Services.store.dispatch(
            configuratorActions.saveConfiguration(configuration, configurationtype)
        );
    }

    async getPreviewImagesForVariants(options = {}) {
        Services.designData.closeDesignAreaEditor();
        Services.designData.clearDesignAreaSelection();

        const { size = { width: 100, height: 100 } } = options;

        const designData = getDesignData(this.state);
        const configurationOptions = getConfiguration(this.state).optionclassifications;

        const cached = getCached('variantPreviewImages', designData, configurationOptions);

        if (cached.validate()) {
            return cached.getValue();
        }

        const memo = Services.designData.createHighlightMemo(false);

        const designAreas = Services.store.state.designArea.filteredDesignAreas;

        // use the first area as default if it's not defined through the production methods
        const defaultDesignAreaIdentifier =
            getDefaultDesignAreaIdentifierFromProductionMethod(Services.store.state) ||
            (designAreas.length ? designAreas[0].identifier : null);
        const areasWithDesign =
            designData &&
            Object.keys(designData).filter(designAreaIdentifier =>
                get(designData[designAreaIdentifier], 'canvasData.objects.length')
            );

        // use the default design area, or the first that contains a design
        const screenshotAreaIdentifier =
            (areasWithDesign &&
                (areasWithDesign.indexOf(defaultDesignAreaIdentifier) !== -1
                    ? defaultDesignAreaIdentifier
                    : areasWithDesign[0])) ||
            defaultDesignAreaIdentifier;

        const usedAreas = screenshotAreaIdentifier ? [screenshotAreaIdentifier] : [];

        const firstDesignViewIdentifier = getCameraViewListForDesignAreaList(designAreas, usedAreas).pop();
        const usedViews = [firstDesignViewIdentifier];

        let variants = getChildren(this.state).map(({ identifier }) => identifier);
        if (!variants[0]) {
            variants = [getItem(this.state).identifier];
        }

        try {
            const screenshots = await Services.visualization.getScreenshots({
                areaList: usedAreas,
                viewList: usedViews,
                variantList: variants,
                size,
            });
            if (!screenshots) {
                return {};
            }
            const images = {};
            Object.keys(screenshots).forEach(identifier => {
                images[variants.length === 1 ? variants[0] : identifier] = getFirstDataURLFromScreenshotsObj(
                    screenshots[identifier]
                );
            });
            cached.setValue(images);
            return images;
        } catch (e) {
            // eslint-disable-next-line no-console
            console.error('Error occurred while generating preview images:', e);
        } finally {
            memo.restore();
        }
    }
    togglePlaceholders(show) {
        const canvases = Services.designData.getDesignAreaCanvases();
        canvases.forEach(({ canvas }) => {
            for (const obj of canvas.objects) {
                if (isPlaceholderObject(obj)) {
                    obj.wrappedObject.visible = show;
                }
            }
        });
        return () => this.togglePlaceholders(true);
    }

    async createMediaDataForConfiguration(configuration) {
        const mediaData = {};
        const restorePlaceholderVisibility = this.togglePlaceholders(false);
        const memo = Services.designData.createHighlightMemo(false);

        const fontMapping = getFontMapping(Services.store.state);

        const svgs = await Services.designData.getScreenshotsOfElements(fontMapping);
        mediaData.designDataSnapshots = prepareDesignDataSnapshots(svgs);

        const fullSvgs = await Services.designData.getScreenshots(fontMapping);
        mediaData.printDesignData = preparePrintDesignData(fullSvgs);
        const usedAreas = Object.keys(fullSvgs);

        const { designArea } = Services.store.state;
        const { filteredDesignAreas } = designArea;
        const usedViews = getCameraViewListForDesignAreaList(filteredDesignAreas, usedAreas);

        const item = getItem(this.state);
        const selectedAmounts = getSelectedAmounts(this.state);

        const selectedVariants = getSelectedAmountsBySizes(item, selectedAmounts)
            .filter(item => {
                if (item.sizes.length > 0) {
                    for (const size of item.sizes) {
                        if (size.amount > 0) {
                            return true;
                        }
                    }
                } else {
                    if (item.amount > 0) {
                        return true;
                    }
                }

                return false;
            })
            .map(item => item.identifier);

        let selectedVariant = getSelectedVariantIdentifier(this.state);

        if (selectedVariants.length === 0) {
            selectedVariants.push(selectedVariant);
        }

        const designViewList = getDesignViewListForDesignAreas(Services.store.state);

        mediaData.screenshots = {};

        try {
            const screenshots = await Services.visualization.getScreenshots({
                areaList: usedAreas, // used in 2D visualization
                viewList: usedViews, // used in 3D visualization
                variantList: selectedVariants,
                designViewList, // used in 2D visualization
            });

            if (screenshots) {
                // make sure the order is kept on the server side
                for (const variant of selectedVariants) {
                    const variantImages = screenshots[variant];
                    if (variantImages) {
                        const keys = Object.keys(variantImages);
                        const orderedImages = {};
                        for (let index = 0; index < keys.length; index++) {
                            const key = keys[index];
                            orderedImages[index + key] = variantImages[key];
                        }
                        screenshots[variant] = orderedImages;
                    }
                }
            }

            mediaData.screenshots = screenshots;
        } catch (e) {
            // do not save in case of an error while generating screenshots
            // eslint-disable-next-line no-console
            console.error('Error occurred while generating screenshots:', e);
        }
        memo.restore();

        if (typeof configuration === 'object') {
            Object.assign(configuration, mediaData);
        }
        restorePlaceholderVisibility();
        return mediaData;
    }

    async captureAndSaveConfiguration(saveType, extraData = {}) {
        try {
            await this.editNameAsync(saveType);
        } catch (e) {
            // do not save if the name is not entered / confirmed
            // eslint-disable-next-line no-console
            console.error('Custom name editor not confirmed!');
            return;
        }

        const configuration = this.prepareConfigurationForSave(false, extraData);

        await this.createMediaDataForConfiguration(configuration);

        return this.saveConfiguration(configuration, saveType);
    }

    editNameAsync(saveType) {
        if (
            getConf('editCustomTitleBeforeSave') &&
            getConf('customTitleSaveTypes').indexOf(saveType) !== -1
        ) {
            Services.ui.show('customTitleEditor');
            return Services.async.create('customEditorConfirm');
        }
    }

    validateConfiguration() {
        return Services.store.dispatch(
            withGlobalLoader(configuratorActions.validateConfiguration(this.state.configuration))
        );
    }

    async saveAndReloadConfiguration(saveType) {
        if (this.state.configuration.item.identifier) {
            const tmpConfiguration = omit(this.state.configuration, ['code', 'date_created']);
            await this.saveConfiguration(tmpConfiguration, saveType);
            const { configuration } = this.state;
            this.setLoadCode(configuration.code);
        }

        const { selectedoptionclassification, configuration, loadIdentifier } = this.state;

        const itemIdentifier = configuration.item.identifier || loadIdentifier;

        // item identifier is required for the following actions
        if (!itemIdentifier) {
            return;
        }

        // reload inspirations
        if (getConf('loadInspirations')) {
            this.loadInspirations(itemIdentifier);
        }

        // reload configuration variants
        this.loadConfigurationVariants(itemIdentifier);

        // the optionlist should be reloaded as well if open
        if (configuration.item.identifier && selectedoptionclassification.identifier) {
            this.loadOptions(
                configuration.item.identifier,
                selectedoptionclassification.identifier,
                configuration
            );
        }
    }

    setLoadCode(code) {
        return Services.store.dispatch(configuratorActions.setLoadCode(code));
    }

    setLoadIdentifier(identifier) {
        return Services.store.dispatch(configuratorActions.setLoadIdentifier(identifier));
    }

    loadInspirations(itemIdentifier) {
        return Services.store.dispatch(configuratorActions.fetchInspirations(itemIdentifier));
    }

    loadConfigurationVariants(itemIdentifier) {
        return Services.store.dispatch(configuratorActions.fetchConfigurationVariants(itemIdentifier));
    }

    loadOptions(itemIdentifier, optionClassificationIdentifier, configuration) {
        return Services.store.dispatch(
            configuratorActions.fetchOptions(itemIdentifier, optionClassificationIdentifier, configuration)
        );
    }

    loadInitialOptions() {
        return Services.store.dispatch(
            configuratorActions.loadInitialOptionclassification(this.state.configuration)
        );
    }

    loadInitialConfiguration() {
        return Services.store.dispatch(withGlobalLoader(configuratorActions.fetchInitialConfiguration()));
    }

    loadConfigurationForDesignerProductPreview() {
        return Services.store.dispatch(
            withGlobalLoader(configuratorActions.fetchConfigurationForDesignerProductPreview())
        );
    }

    loadCodeSnippets() {
        return Services.store
            .dispatch(configuratorActions.fetchCodesnippets())
            .then(codeSnippets => this.attachCodeSnippets(codeSnippets));
    }

    loadClientData() {
        return Services.store.dispatch(withGlobalLoader(configuratorActions.fetchClientData()));
    }

    attachCodeSnippets(codeSnippets) {
        if (codeSnippets.data.length > 0) {
            codeSnippets.data.forEach(({ code }) => {
                const fragment = document.createRange().createContextualFragment(code);
                while (fragment.children.length > 0) {
                    document.head.insertBefore(fragment.children[0], document.head.firstElementChild);
                }
            });
            window.dataLayer && Services.analytics.setDataLayer(window.dataLayer);
        }
    }

    setSelectedAmount(identifier, amount, isUpdatedByUser = false) {
        return Services.store.dispatch(
            configuratorActions.setSelectedAmount(
                identifier ||
                    getSelectedVariantIdentifier(this.state) ||
                    this.state.configuration.item.identifier,
                amount,
                isUpdatedByUser
            )
        );
    }

    updateSelectedAmount(oldVariant, newVariant) {
        if (!isSelectedAmountsUpdated(this.state) && !hasMultipleVariantsSelected(this.state)) {
            if (
                !oldVariant.children &&
                !newVariant.children &&
                !oldVariant.itemGroup &&
                !newVariant.itemGroup
            ) {
                return Services.store.dispatch(
                    configuratorActions.switchSelectedAmount(oldVariant.identifier, newVariant.identifier)
                );
            }

            const oldIdentifiers = getDeepestChildIdentifiers(oldVariant);
            const newIdentifiers = getDeepestChildIdentifiers(newVariant);
            const oldIdentifierKeys = keys(oldIdentifiers);
            const newIdentifierKeys = keys(newIdentifiers);
            const sortedOldIdentifierKeys = oldIdentifierKeys.sort((a, b) =>
                a.localeCompare(b, 0, { numeric: false })
            );
            const sortedNewIdentifierKeys = newIdentifierKeys.sort((a, b) =>
                a.localeCompare(b, 0, { numeric: false })
            );
            const identifierDiffs = difference(sortedOldIdentifierKeys, sortedNewIdentifierKeys);
            if (identifierDiffs.length === 0 && isEqual(sortedOldIdentifierKeys, sortedNewIdentifierKeys)) {
                for (const key in oldIdentifiers) {
                    if (!oldIdentifiers.hasOwnProperty(key)) {
                        continue;
                    }

                    if (newIdentifiers[key]) {
                        Services.store.dispatch(
                            configuratorActions.switchSelectedAmount(oldIdentifiers[key], newIdentifiers[key])
                        );
                    }
                }
            }
        }
    }

    clearBulkNames() {
        return Services.store.dispatch(configuratorActions.clearBulkNames());
    }

    saveBulkNames(identifier, bulkNamesData) {
        return Services.store.dispatch(configuratorActions.saveBulkNames(identifier, bulkNamesData));
    }

    switchSelectedAmount(oldIdentifier, newIdentifier) {
        this.setSelectedVariantIdentifier(newIdentifier);
        const item = this.state.configuration.item;
        const oldVariant = find(item.itemGroup.children, function(o) {
            return o.identifier === oldIdentifier;
        });
        const newVariant = find(item.itemGroup.children, function(o) {
            return o.identifier === newIdentifier;
        });
        const selectedAmounts = getSelectedAmounts(this.state);
        const firstSelectedAmountIdentifier = keys(selectedAmounts)[0];
        const oldSelectedAmountVariant = find(item.itemGroup.children, function(o) {
            if (o.itemGroup) {
                const children = o.itemGroup.children;
                return children.find(child => child.identifier === firstSelectedAmountIdentifier);
            } else {
                return o.identifier === firstSelectedAmountIdentifier;
            }
        });
        if (!getConf('restrictAmountselectionToSelectedVariant', false)) {
            // Updates selected Amounts from previous selectedVariant amount to new variant
            this.updateSelectedAmount(oldSelectedAmountVariant, newVariant);
            return;
        }

        if (!oldVariant.children && !newVariant.children) {
            return Services.store.dispatch(
                configuratorActions.switchSelectedAmount(oldVariant.identifier, newVariant.identifier)
            );
        }

        const oldIdentifiers = getDeepestChildIdentifiers(oldVariant);
        const newIdentifiers = getDeepestChildIdentifiers(newVariant);

        for (const key in oldIdentifiers) {
            if (!oldIdentifiers.hasOwnProperty(key)) {
                continue;
            }

            if (newIdentifiers[key]) {
                Services.store.dispatch(
                    configuratorActions.switchSelectedAmount(oldIdentifiers[key], newIdentifiers[key])
                );
            }
        }
    }

    setConfigurationComment(comment) {
        return Services.store.dispatch(configuratorActions.setComment(comment));
    }

    setSelectedCalculation(identifier, value) {
        return Services.store.dispatch(configuratorActions.setSelectedCalculation(identifier, value));
    }

    setSelectedVariantIdentifier(identifier) {
        return Services.store.dispatch(configuratorActions.setSelectedVariantIdentifier(identifier));
    }

    setOptionInputText(identifier, value) {
        return Services.store.dispatch(configuratorActions.setOptionInputText(identifier, value));
    }

    /**
     * A simple method for saving the configuration without any additional design data or screenshot
     * @param {string} saveType
     * @param {object} extraData
     * @returns {Promise}
     */
    prepareAndSaveConfiguration(saveType, extraData = {}) {
        const configuration = this.prepareConfigurationForSave(false, extraData);
        return this.saveConfiguration(configuration, saveType);
    }

    addToCartCallback(configuration) {
        // after saving call parent function if existing or send postMessage (depending on the configuration)
        if (window.parent !== undefined) {
            const addToCartMethod = getConf('addToCartMethod');

            if (addToCartMethod === 'postMessage' && window.parent.postMessage !== undefined) {
                const appConf = getConf();
                const targetOrigin = get(appConf, 'addToCartPostMessageTargetOrigin', '*');

                window.parent.postMessage(
                    { method: METHOD_ADD_TO_CART, code: configuration.code },
                    targetOrigin
                );
            } else if (addToCartMethod === 'functionCall' && window.parent.addToCart !== undefined) {
                window.parent.addToCart(configuration.code);
            }
        }
    }
}
