import axios from 'axios';
import first from 'lodash/first';
import get from 'lodash/get';
import find from 'lodash/find';
import isEmpty from 'lodash/isEmpty';
import isEqual from 'lodash/isEqual';
import merge from 'lodash/merge';
import { t } from 'Framework/i18n';

import { Services } from '../ServiceLocator';
import { getConf } from '../configuration';
import { getUrl } from '../Utils/getUrl';

import Observable from '../../Utils/Decorators/Observable';
import setupDesigner from '../Modules/Designer/setupDesigner';

import {
    setCanvasData,
    setDesignProductionMethod,
    setColorAmount,
    showMaxColorChangeConfirm,
    showMinFontSizeConfirm,
    showProductionMethodRemoveImagesConfirm,
    updateImageData,
    setDesignAreaTextsColor,
    replaceDesignAreaTextsColor,
    showProductionMethodChangeColorsConfirm,
    showProductionMethodChangeConfirm,
    showSwitchToProductionMethodWithVectorization,
    showSwitchToProductionMethodWithoutVectorization,
} from '../Reducers/DesignData/Actions';
import { commitChanges, editImage, updateImage } from '../Reducers/ImageGallery/Actions';

import {
    setColorOnOperations,
    replaceColorOnOperations,
    getEditDataFromOperations,
    compareOperations,
    createVectorizeColorsMap,
    removeVectorizeFromOperations,
    reduceNumberOfDistinctColorsBy,
    applyProductionMethodChangeToCanvasObjects,
    extendCanvasDataWithMetricContentSize,
} from '../Reducers/DesignData/Modifiers';

import {
    clearDesignAreaSelection,
    selectDesignArea,
    toggleHighlight,
    recalculateDesignArea,
} from '../Reducers/DesignArea/Actions';
import { selectDesignView } from '../Reducers/DesignView/Actions';
import {
    getFontSizeMin,
    getSelectedDesignAreaProperties,
    getSelectedDesignArea,
    getDesignProductionMethodByIdentifier,
    getDesignDataForSelectedDesignArea,
    getColorsFromCanvasData,
    getColors,
    getColorsWithPalette,
    findSmallestFontSizeInCanvasData,
    getDesignDataByDesignAreaIdentifier,
    getCanvasSizeFromDesignArea,
    getSelectedDesignProductionMethod,
    getSelectedDesignProductionMethodIdentifier,
    isHighlighted,
    isValidHex,
    hexToRgb,
    rgbToHex,
    createColorData,
    isAnyDesignAreaSelected,
    getVisualizationEffectWithOptions,
    getMaskDataForDesignArea,
    designAreaAppliesMask,
    parseColorData,
    isPlaceholderContent,
    contentHasText,
    isBulkNamesSelected,
    hasSelectedBulkName,
    getDesignAreaIdentifierToOpen,
    isCanvasUpdateRequired,
    rgbObjToHex,
    getDefaultDesignAreaIdentifierFromProductionMethod,
    getActiveDesignAreas,
    isColoringDisabled,
    areHashesCompatible,
    getDesignAreaImageCompatibilityInfo,
    getDesignAreaPreSelection,
    getVectorizeForDesignProductionMethods,
    getImagesByDesignArea,
    designProductionMethodHasEmbroideryVisualizationEffect,
    getMandatoryVectorizationForDesignProductionMethods,
    isVectorizedLogoMandatory,
    getColorOverlayData,
} from '../Reducers/DesignArea/Selectors';
import {
    getUserImage,
    isEditableVectorImage,
    getPreviewUrlFromImageData,
    getImageUsageInfo,
    isColorMappingCompatible,
    isColorPreviewCompatible,
    vectorFormats,
} from '../Reducers/ImageGallery/Selectors';
import {
    getClientHighlightColor,
    isDesignerProductPreviewMode,
    visualizationModeCheck,
} from '../Reducers/Configurator/Selectors';
import { getFontMapping } from '../Reducers/Fonts/Selectors';
import { isConfirmResult } from '../Reducers/Confirm/Actions';
import { selectConfigurationMode } from '../Reducers/ConfigurationMode/Actions';
import { requestLock } from '../../Utils/Async/ObjectLock';
import { initDefaultColor } from '../Reducers/DesignData/Transformers';
import { createSVGImageData } from '../Reducers/ImageGallery/Modifiers';
import { FontFaceRepository } from '../Modules/Designer/Utils/FontFaceRepository';
import { getAggregatedColorCount, isPlaceholderObject } from '../Reducers/DesignData/Selectors';
import { expandVectorizeColorsMap } from '../Modules/Designer/Utils/ImageEditUtils';
import { checkDesignElementRestrictionsWithRemovalCount } from '../Reducers/DesignArea/DesignElementSelectors';
import { getMissingViewBox } from '../Modules/Designer/Utils/SvgFixers';

import placeholderLogo from '../../Resources/Images/designer_placeholder_logo.svg';

/**
 * Preload all fonts for the given canvasData
 * @param {object} canvasData The canvas data object
 * @return {Promise<[]>}
 */
const preloadFonts = canvasData =>
    Promise.all(
        canvasData.objects
            .filter(({ type }) => type === 'Text')
            .map(({ content }) => FontFaceRepository.collectFromHTML(content))
    );

export const checkColorUsage = (() => {
    let alreadyAlertedColorCountState = {};
    return (canvasDataJSON, changeEvent) => {
        const {
            maxColorAmount: maxColorCount,
            selectedDesignArea: { identifier: designAreaIdentifier },
            colorAmount: currentColorCount,
        } = getSelectedDesignAreaProperties(Services.store.state);

        if (!designAreaIdentifier) {
            return;
        }

        const changedObjectColorCount = getColorsFromCanvasData({ objects: [changeEvent.target.toObject()] })
            .length;
        const colorCount = getColorsFromCanvasData(canvasDataJSON).length;

        const colorCountState = { colorCount: changedObjectColorCount, maxColorCount };

        if (colorCount !== currentColorCount) {
            Services.store.dispatch(setColorAmount(colorCount));
        }

        const showAlert =
            maxColorCount &&
            isTextColorChange(changeEvent) &&
            changedObjectColorCount > maxColorCount &&
            alreadyAlertedColorCountState[designAreaIdentifier].colorCount !== colorCountState.colorCount;

        alreadyAlertedColorCountState[designAreaIdentifier] = colorCountState;

        return showAlert;
    };
})();

export const isTextColorChange = changeEvent => {
    if (changeEvent.target.type !== 'Text') {
        return false;
    }

    const prevStyles = get(changeEvent, 'eventData.prevState.style', {});

    if (isEmpty(prevStyles)) {
        return false;
    }

    const prevColors = get(prevStyles, 'colors', []);
    const colors = get(changeEvent.target.toObject(), 'style.colors', []);

    return !isEqual(prevColors, colors);
};

export const getRgbValuesFromString = (str, defaultValue = { r: 0, g: 0, b: 0 }) => {
    const matches = str.match(/rgb\((.*)\)/);
    if (!matches) {
        return defaultValue;
    }
    const values = matches[1].split(',').map(number => parseInt(number));

    return { r: values[0], g: values[1], b: values[2] };
};

export const rgbStrToHex = str => rgbObjToHex(getRgbValuesFromString(str));

export const getColorsDistance = (color1, color2) => {
    return (
        Math.pow(color1.r - color2.r, 2) + Math.pow(color1.g - color2.g, 2) + Math.pow(color1.b - color2.b, 2)
    );
};

export const isColorTooBright = rgbColors => {
    if (!rgbColors) {
        return false;
    }

    const { r: red, g: green, b: blue } = rgbColors;
    if (
        typeof red !== 'number' ||
        typeof green !== 'number' ||
        typeof blue !== 'number' ||
        red < 0 ||
        red > 255 ||
        green < 0 ||
        green > 255 ||
        blue < 0 ||
        blue > 255
    ) {
        return false;
    }

    const maxBrightness = 210;
    const luma = 0.2126 * red + 0.7152 * green + 0.0722 * blue; // see https://bit.ly/2BXPOld

    return luma > maxBrightness;
};

export const calculateImageBrightness = image => {
    if (!image) {
        return;
    }
    const thumbnail = get(image, 'data.thumbnail.url', null);
    let imageUrl = null;
    if (thumbnail) {
        imageUrl = getUrl(thumbnail);
    }
    const fuzzy = 0.1;

    return new Promise((resolve, reject) => {
        const downloadedImg = new Image(200, 200);
        downloadedImg.crossOrigin = 'anonymous';
        downloadedImg.addEventListener(
            'load',
            () => {
                let canvas = document.createElement('canvas');
                let context = canvas.getContext('2d');

                canvas.width = downloadedImg.width;
                canvas.height = downloadedImg.height;

                context.drawImage(downloadedImg, 0, 0);
                const imageData = context.getImageData(0, 0, canvas.width, canvas.height);
                const data = imageData.data;

                let light = 0,
                    dark = 0;

                for (let x = 0; x < data.length; x += 4) {
                    const red = data[x];
                    const green = data[x + 1];
                    const blue = data[x + 2];
                    const alpha = data[x + 3];

                    if (alpha === 0) {
                        continue;
                    }

                    const maxRgb = Math.max(Math.max(red, green), blue);
                    if (maxRgb < 128) dark++;
                    else light++;
                }

                const dlDiff = (light - dark) / (canvas.width * canvas.height);
                image.data && image.data.thumbnail
                    ? (image.data.thumbnail.isThumbnailBright = dlDiff + fuzzy > 0)
                    : (image.isBright = dlDiff + fuzzy > 0);
                resolve(image);
            },
            false
        );
        downloadedImg.addEventListener('error', reject);

        downloadedImg.src = imageUrl || image.src;
    });
};

export const getColorPalettesIndex = colorPalettes => {
    const colorPalettesIndexes = [];

    for (const colorPalette of colorPalettes) {
        for (const colorPaletteColor of colorPalette.colors) {
            const colorIndex = {
                palette: colorPalette.identifier,
                identifier: colorPaletteColor.identifier,
                value: hexToRgb('#' + colorPaletteColor.hex_value),
            };

            colorIndex.valueObject = getRgbValuesFromString(colorIndex.value);

            colorPalettesIndexes.push(colorIndex);
        }
    }

    return colorPalettesIndexes;
};

export const getClosestColorPaletteColor = (colorValueObject, colorPalettesIndexes) => {
    let closestDistance = null;
    let closestColor = null;

    for (const colorPalettesIndex of colorPalettesIndexes) {
        const distance = getColorsDistance(colorValueObject, colorPalettesIndex.valueObject);

        if (closestDistance === null || distance < closestDistance) {
            closestDistance = distance;

            const rgb = colorPalettesIndex.valueObject;
            const hexValue = rgbToHex(rgb.r, rgb.g, rgb.b);
            closestColor = createColorData(
                colorPalettesIndex.identifier,
                colorPalettesIndex.palette,
                false,
                hexValue
            );
        }
    }

    return closestColor;
};

export const getMissingColorPalettesColors = (colors, colorPalettes, replaceCustomColors) => {
    const availableColorPalettes = colorPalettes.map(colorPalette => colorPalette.identifier);

    // skip if no available color palettes (most likely a data issue)
    if (!availableColorPalettes.length) {
        return [];
    }

    const colorsToReplace = colors.filter(color => {
        const { palette } = parseColorData(color.identifier);

        if (replaceCustomColors && palette === null) {
            return true;
        }

        return palette && availableColorPalettes.indexOf(palette) === -1;
    });

    if (!colorsToReplace.length) {
        return [];
    }

    const colorPalettesIndexes = getColorPalettesIndex(colorPalettes);

    return colorsToReplace.map(color => {
        const closestColor = getClosestColorPaletteColor(
            getRgbValuesFromString(color.value.startsWith('#') ? hexToRgb(color.value) : color.value),
            colorPalettesIndexes
        );

        return {
            color,
            closestColor,
        };
    });
};

export const makeOperationsCompatible = (operations, compatibilityInfo, colorPalettes) => {
    if (!operations) {
        return operations;
    }
    const missingColorPalettesColors = getMissingColorPalettesColors(
        Object.values(operations.vectorizeColorsMap || {}),
        colorPalettes,
        !operations.vectorize && compatibilityInfo.vectorsRequired
    );
    for (const missingColor of missingColorPalettesColors) {
        operations = replaceColorOnOperations(operations, missingColor.color, missingColor.closestColor);
    }
    return operations;
};

export const getSvgContent = (() => {
    const svgContentMap = new Map();
    return function getSvgContent(url, absolute = false, forceFetch = false) {
        const absUrl = getUrl(url, absolute ? undefined : getConf('network.host'));
        if (!forceFetch && svgContentMap.has(absUrl)) {
            return svgContentMap.get(absUrl);
        }
        try {
            const promise = (async function() {
                const result = await axios.get(absUrl);
                return initDefaultColor(result.data);
            })();
            svgContentMap.set(absUrl, promise);
            return promise;
        } catch (e) {
            return '';
        }
    };
})();

export const applySvgContentOperations = (original, preview, operations, svgContent) => {
    // add missing attributes to svg tag if needed
    const svgTags = svgContent.match(/<svg[^>]*>/s);

    if (svgTags && svgTags[0]) {
        const svgTag = svgTags[0];
        const percentageHeight = /height=["']\d+%["']/;
        const percentageWidth = /width=["']\d+%["']/;
        let scaleFactor = 2048 / preview.width;
        if (preview.height > preview.width) {
            scaleFactor = 2048 / preview.height;
        }
        const pixelWidth = Math.round(preview.width * scaleFactor);
        const pixelHeight = Math.round(preview.height * scaleFactor);
        const isSvgUpload = vectorFormats.indexOf(original.format.toLowerCase()) !== -1;
        if (svgTag.indexOf('viewBox=') === -1) {
            const fallbackSize = isSvgUpload
                ? {
                      width: pixelWidth,
                      height: pixelHeight,
                  }
                : {
                      width: preview.width,
                      height: preview.height,
                  };
            const viewBox = getMissingViewBox(svgContent, fallbackSize);
            svgContent = svgContent.replace('<svg', `<svg viewBox="${viewBox}"`);
        }

        if (svgTag.indexOf('height=') === -1) {
            if (isSvgUpload) {
                svgContent = svgContent.replace('<svg', `<svg height="${pixelHeight}px"`);
            } else {
                svgContent = svgContent.replace('<svg', `<svg height="${preview.height}px"`);
            }
        }

        if (svgTag.indexOf('width=') === -1) {
            if (isSvgUpload) {
                svgContent = svgContent.replace('<svg', `<svg width="${pixelWidth}px"`);
            } else {
                svgContent = svgContent.replace('<svg', `<svg width="${preview.width}px"`);
            }
        }

        if (percentageHeight.test(svgTag)) {
            if (isSvgUpload) {
                svgContent = svgContent.replace(percentageHeight, `height="${pixelHeight}px"`);
            } else {
                svgContent = svgContent.replace(percentageHeight, `height="${preview.height}px"`);
            }
        }

        if (percentageWidth.test(svgTag)) {
            if (isSvgUpload) {
                svgContent = svgContent.replace(percentageWidth, `width="${pixelWidth}px"`);
            } else {
                svgContent = svgContent.replace(percentageWidth, `width="${preview.width}px"`);
            }
        }
    }

    // remove doctype if present (including nested entity definitions)
    svgContent = svgContent.replace(/<!DOCTYPE\s+svg[^>[]*(\[[^\]]*\])?\s*>/gis, '');

    if (
        operations &&
        operations.vectorize &&
        operations.vectorizeColorsMap &&
        Object.keys(operations.vectorizeColorsMap).length
    ) {
        const colorRegex = new RegExp(`(${Object.keys(operations.vectorizeColorsMap).join('|')})`, 'ig');
        svgContent = svgContent.replace(colorRegex, match => {
            const { value } = operations.vectorizeColorsMap[match.toLowerCase()];

            return isValidHex(value) ? value : '#000000';
        });
    }
    return 'data:image/svg+xml;base64,' + btoa(svgContent);
};

export const getSvgColors = (svgContent, designProductionMethod) => {
    const colors = svgContent.toLowerCase().match(/#[0-9a-f]{3,6}/g);
    const colorsMap = [];

    for (const colorPalette of designProductionMethod.colorPalettes) {
        for (const color of colorPalette.colors) {
            colorsMap.push({
                identifier: color.identifier,
                paletteIdentifier: colorPalette.identifier,
                value: '#' + color.hex_value,
            });
        }
    }

    const colorPalettesIndexes = getColorPalettesIndex(designProductionMethod.colorPalettes);

    return colors.map(color => {
        const paletteColor = find(colorsMap, colorsMapColor => colorsMapColor.value === color);

        // the color has a matching palette color
        if (paletteColor) {
            return {
                key: color,
                color: createColorData(
                    paletteColor.identifier,
                    paletteColor.paletteIdentifier,
                    false,
                    paletteColor.value
                ),
            };
        }

        // find the closest color in the palettes
        if (colorPalettesIndexes.length) {
            return {
                key: color,
                color: getClosestColorPaletteColor(
                    getRgbValuesFromString(hexToRgb(color)),
                    colorPalettesIndexes
                ),
            };
        }

        // treat it as a custom color (fallback case)
        return {
            key: color,
            color: createColorData(color, null, true, color),
        };
    });
};

export const isWidget = pathname => /\/widget\//.test(pathname);

export const updateColorAmountOnDesignProductionMethodChange = selectedDesignProductionMethod => {
    const state = Services.store.state;
    const selectedProductionMethod = getDesignProductionMethodByIdentifier(
        state,
        selectedDesignProductionMethod
    );
    const designAreaData = getDesignDataForSelectedDesignArea(state);
    const { colorAmount } = designAreaData;
    const selectedDesignArea = getSelectedDesignArea(state);
    const maxColorAmount = Number(get(selectedProductionMethod, 'options.maxColorAmount') || 0);
    const activeDesignAreas = getActiveDesignAreas(state);
    const isDesignAreaSelected = activeDesignAreas[selectedDesignArea.identifier];
    if (colorAmount !== 1 && colorAmount > maxColorAmount) {
        Services.designData.getSampleCanvasEditorForDesignArea().setColorAmount(1);
        isDesignAreaSelected && this.getSampleCanvasEditorForDesignArea().setColorAmount(1);
    }
};

export const EventTypes = {
    DesignAreaListChange: 'DesignDataService/DesignAreaListChange',
    DesignViewListChange: 'DesignDataService/DesignViewListChange',
};

export default class DesignDataService {
    static serviceName = 'DesignDataService';

    observable = new Observable(this);

    isInPreviewMode = false;

    /**
     *
     * @type {function|null}
     * @private
     */
    _loadCanvasData = null;

    /**
     * A function that returns the canvas object associated to the given design area
     * @type {function|null}
     * @private
     */
    _getCanvas = null;

    /**
     *
     * @type {function|null}
     * @private
     */
    _serializeCanvas = null;

    /**
     * Use this function to sync debounced canvas change handlers
     * @type {function|null}
     * @private
     */
    _syncCanvasChange = null;

    _isCanvasDataUpdateEnabled = true;

    init() {
        Services.store.observeStore(
            state => state.designArea.filteredDesignAreas,
            async (filteredDesignAreas, prevDesignAreas) => {
                try {
                    const initList = await this.initCanvases(filteredDesignAreas, prevDesignAreas);
                    // only the newly initialized canvases has to be loaded
                    const areasToLoad = initList
                        .filter(({ isNew }) => isNew)
                        .map(({ designArea }) => designArea);
                    // the canvas data is there first with the configuration, but since the canvas instances are
                    // associated with the design areas the data can be loaded only when the design areas are available
                    !isDesignerProductPreviewMode() &&
                        areasToLoad.length &&
                        (await this.loadCanvasDataFromState(areasToLoad));

                    this.selectInitialDesignView();
                    this.designAreaListChangeListener(filteredDesignAreas, prevDesignAreas);
                    this.openDesignAreaEditorIfEditingParamIsSet();
                } catch (e) {
                    // eslint-disable-next-line no-console
                    console.error('Error occurred in processing design area list change:', e);
                }
            }
        );

        Services.store.observeStore(
            state => state.designArea.maxZoom2d,
            maxZoom2d => {
                Services.visualization.update2DMaxZoom(maxZoom2d);
            }
        );
        // in calculation widget mode the canvas events should be disabled,
        // the update is done directly after the modifications
        if (isWidget(location.pathname)) {
            this.toggleCanvasDataUpdate(false);
            Services.store.observeStore(
                getSelectedDesignProductionMethodIdentifier,
                selectedDesignProductionMethod => {
                    updateColorAmountOnDesignProductionMethodChange(selectedDesignProductionMethod);
                }
            );
        }

        Services.store.observeStore(
            ({ configurator: { selectedoptionclassification } }) => selectedoptionclassification,
            this.componentChangeHandler.bind(this)
        );

        setupDesigner(this);
    }

    async openDesignAreaEditorIfEditingParamIsSet() {
        if (this._editingHandlerDone) {
            return; // skip if already opened once
        }
        const query = new URLSearchParams(location.search);
        if (!query.has('_editing')) {
            return; // simply return if the param is not set
        }
        const isDesignerModeAvailable = get(
            Services.store.state,
            'configurationMode.availableModes.designer'
        );
        if (!isDesignerModeAvailable) {
            return; // skip if designer mode is not available
        }
        const selectedMode = get(Services.store.state, 'configurationMode.selectedMode');
        if (selectedMode !== 'designer') {
            Services.store.dispatch(selectConfigurationMode('designer'));
        }
        const viewChangedPromise = Services.visualization.getViewChangedPromise();
        if (viewChangedPromise) {
            await viewChangedPromise;
            this.openDesignAreaEditor();
            this._editingHandlerDone = true;
        }
    }

    componentChangeHandler(component, prevComponent) {
        const identifier = get(component, 'identifier');
        if (identifier && identifier !== get(prevComponent, 'identifier')) {
            this.selectViewForComponent(component);
        }
    }

    selectViewForComponent(component) {
        const designViewIdentifier =
            // use the view linked to the component
            get(component, 'creatorView.identifier') ||
            // if no linked view, use the last selected view
            get(Services.store.state, 'designView.lastSelectedDesignView.identifier') ||
            // if nothing was selected, use the initial design view
            this.getInitialDesignViewIdentifier() ||
            // if no initial design view, use the first view from the list
            get(Services.store.state, 'designView.designViews[0].identifier');

        const currentDesignViewIdentifier = get(
            Services.store.state,
            'designView.selectedDesignView.identifier'
        );

        if (currentDesignViewIdentifier !== designViewIdentifier) {
            const switchViewOnComponentSelection = getConf('switchViewOnComponentSelection', true);
            this.selectDesignView(designViewIdentifier, { saveLast: !switchViewOnComponentSelection });
        }
    }

    /**
     * Register a function to be called to load the canvas data into the active canvas
     * or into the one that's related to the provided design area.
     * @param {function} fn
     * @see loadCanvasDataFromState
     * @see createCanvasChangeListener
     */
    registerLoadCanvasData(fn) {
        this._loadCanvasData = fn;
    }

    /**
     * Register a function to be called to load the canvas data into the active canvas
     * or into the one that's related to the provided design area.
     * @param {function} fn
     * @see updateCanvasData
     * @see createCanvasChangeListener
     */
    registerUpdateCanvasData(fn) {
        this._updateCanvasData = fn;
    }

    /**
     * Register a function to be called to get the canvas that is associated with the given design area
     * @param {function} fn
     * @see redrawAllDesignAreas
     */
    registerGetCanvas(fn) {
        this._getCanvas = fn;
    }

    /**
     * Register a function to be called to initialize a canvas
     * @param {function} fn
     * @see initCanvases
     */
    registerInitCanvas(fn) {
        this._initCanvas = fn;
    }

    /**
     * Register a function to be called to remove a canvas
     * @param {function} fn
     * @see initCanvases
     */
    registerRemoveCanvas(fn) {
        this._removeCanvas = fn;
    }

    /**
     * Calls the registered _initCanvas function for all design areas
     * @param designAreas
     * @param prevDesignAreas
     */
    async initCanvases(designAreas, prevDesignAreas) {
        // clean up first: remove canvases that are not mapped to any design areas
        this._removeCanvas &&
            prevDesignAreas
                .filter(prevArea => !designAreas.some(area => prevArea.identifier === area.identifier))
                .forEach(prevArea => this._removeCanvas(prevArea));

        const state = Services.store.state;
        return await Promise.all(
            designAreas.map(designArea => {
                const maskData = getMaskDataForDesignArea(state, designArea);
                const canvasSize = getCanvasSizeFromDesignArea(state, designArea);
                const prevDesignArea = find(prevDesignAreas, { identifier: designArea.identifier });
                const colorOverlayData = getColorOverlayData(state, designArea);
                return (
                    this._initCanvas &&
                    this._initCanvas({ designArea, prevDesignArea, canvasSize, maskData, colorOverlayData })
                );
            })
        );
    }

    /**
     * Calls syncCanvases with the registered _loadCanvasData function
     * @param {array} designAreas
     */
    async updateCanvasDataFromState(designAreas = this.getDesignAreas()) {
        return await this.syncCanvases(this._updateCanvasData, designAreas);
    }

    /**
     * Calls syncCanvases with the registered _loadCanvasData function
     * @param {array} designAreas
     */
    async loadCanvasDataFromState(designAreas = this.getDesignAreas()) {
        return await this.syncCanvases(this._loadCanvasData, designAreas);
    }

    /**
     * Calls the given updateFn function for all the design areas
     * which have associated canvasData in the configuration
     * @param {function} updateFn
     * @param {array} designAreas
     * @return {Promise<void>}
     */
    async syncCanvases(updateFn, designAreas) {
        const state = Services.store.state;
        await Promise.all(
            designAreas.map(async designArea => {
                const designData = getDesignDataByDesignAreaIdentifier(state, designArea.identifier);
                const canvasData = get(designData, 'canvasData');
                if (canvasData) {
                    await preloadFonts(canvasData);
                    const canvasSize = getCanvasSizeFromDesignArea(state, designArea);
                    return updateFn && updateFn(canvasData, designArea, canvasSize);
                }
            })
        );
        window.setTimeout(() => {
            this.updateVisualization();
        }, 100);
    }

    getDesignAreas() {
        const { designArea } = Services.store.state;
        const { filteredDesignAreas } = designArea;
        return filteredDesignAreas;
    }

    getDesignAreaByIdentifier(designAreaIdentifier) {
        return this.getDesignAreas().find(({ identifier }) => identifier === designAreaIdentifier);
    }

    removeBulkNameItems(designData) {
        const designArea = getSelectedDesignArea(Services.store.state);
        const { canvasData } = designData;
        if (!canvasData) {
            return;
        }
        const objects = canvasData ? canvasData.objects.filter(obj => !obj.isBulkName) : [];
        this.setCanvasData({ ...canvasData, objects });
        this.loadCanvasDataFromState([designArea]);
    }

    designAreaListChangeListener(filteredDesignAreas, prevDesignAreas) {
        // apply pre-selection only after loading the design areas
        if (filteredDesignAreas.length && !prevDesignAreas?.length) {
            this.applyDesignAreaPreSelection(filteredDesignAreas);
        }

        Services.visualization.updateDesignAreaList(filteredDesignAreas);
    }

    registerCanvasSerializer(fn) {
        this._serializeCanvas = fn;
    }

    async updateCanvasData(canvas, eventData) {
        if (!this._serializeCanvas) {
            throw new TypeError(
                `The registerCanvasSerializer method has to be called first with a valid function!`
            );
        }
        const canvasDataJSON = this._serializeCanvas(canvas);

        // check the color usage before storing the canvas data, so the user can decide to
        // 1. proceed with more colors than the allowed or
        // 2. undo the changes
        //added check to avoid the checkColorUsage in Widget view(as evenData is undefined)
        const showAlert = eventData ? checkColorUsage(canvasDataJSON, eventData.changeEvent) : false;

        if (showAlert && getConf('displayColorAmountWarnings', true)) {
            Services.ui.reportError(t('colorAmountAlert.colorChangeReverted'));

            const { canvasData } = getDesignDataForSelectedDesignArea(Services.store.state);

            // Restore Graphics Editor state
            this._loadCanvasData && this._loadCanvasData(canvasData);

            return;
        }

        this.setCanvasData(canvasDataJSON);
    }

    createCanvasChangeListener({ syncCanvasChange }) {
        this._syncCanvasChange = syncCanvasChange;
        return async (...args) => {
            if (this._isCanvasDataUpdateEnabled) {
                await this.updateCanvasData(...args);
            }
        };
    }

    syncCanvasChange() {
        this._syncCanvasChange && this._syncCanvasChange();
    }

    toggleCanvasDataUpdate(value = !this._isCanvasDataUpdateEnabled) {
        this._isCanvasDataUpdateEnabled = value;
    }

    /**
     * Call the registered listener function when the design area changes
     * Passes the selected design area and the canvas size to the listener function
     * @param listenerFn
     */
    registerDesignAreaChangeListener(listenerFn) {
        Services.store.observeStore(getSelectedDesignArea, (selectedDesignArea, prevSelectedDesignArea) => {
            const state = Services.store.state;
            const canvasSize = getCanvasSizeFromDesignArea(state, selectedDesignArea);
            listenerFn({ selectedDesignArea, prevSelectedDesignArea, canvasSize });
        });
    }

    registerDesignProductionMethodChangeListener(listenerFn) {
        Services.store.observeStore(getSelectedDesignProductionMethodIdentifier, listenerFn);
    }

    getDesignAreaCanvases() {
        return this.getDesignAreas().map(designArea => ({ designArea, canvas: this._getCanvas(designArea) }));
    }

    removeAllDesignAreaPlaceholders(configuration) {
        const state = Services.store.state;
        let updatedConfiguration = configuration;
        if (!isWidget(location.pathname)) {
            const designAreas = get(state, 'designArea.filteredDesignAreas', []);
            designAreas.forEach(designArea => {
                const { canvasData } = get(updatedConfiguration, `designdata.${designArea.identifier}`, {});
                if (canvasData) {
                    const objects = canvasData.objects.filter(obj => {
                        if (!isPlaceholderObject(obj)) {
                            return true;
                        }
                    });
                    updatedConfiguration = {
                        ...updatedConfiguration,
                        designdata: {
                            ...updatedConfiguration.designdata,
                            [designArea.identifier]: {
                                ...updatedConfiguration.designdata[designArea.identifier],
                                canvasData: objects.length > 0 ? { ...canvasData, objects } : null,
                            },
                        },
                    };
                }
            });
        }
        return updatedConfiguration;
    }

    removeDesignAreaPlaceholders(
        removeTextPlaceholders = true,
        removeImagePlaceholders = true,
        designArea = getSelectedDesignArea(Services.store.state)
    ) {
        this.syncCanvasChange();

        const state = Services.store.state;

        if (!designArea.identifier) {
            return;
        }

        const { canvasData } = getDesignDataByDesignAreaIdentifier(state, designArea.identifier);

        if (!canvasData) {
            return;
        }

        const objects = canvasData.objects.filter(obj => {
            if (removeTextPlaceholders && obj.type === 'Text') {
                return !isPlaceholderContent(obj.content) && contentHasText(obj.content);
            }
            if (removeImagePlaceholders && obj.type === 'Image') {
                return !obj.imageData.isPlaceHolderImage;
            }
            return true;
        });

        // replace objects and sync canvas if something was removed
        if (objects.length !== canvasData.objects.length) {
            this.setCanvasData({
                ...canvasData,
                objects,
            });
            this.loadCanvasDataFromState([designArea]);
        }
    }

    selectDesignArea(identifier) {
        Services.ui.show('designer');
        Services.store.dispatch(selectDesignArea(identifier));

        if (!this.isInPreviewMode) {
            Services.visualization.disableUserMovement();
        }
    }

    selectDesignView(identifier, options) {
        Services.store.dispatch(selectDesignView(identifier, options));
    }

    getInitialDesignViewIdentifier() {
        let designAreaToShow;

        const preselectedDesignArea = get(getConf('designerPreSelection', {}), 'designAreas[0].identifier');

        if (preselectedDesignArea) {
            designAreaToShow = this.getDesignAreaByIdentifier(preselectedDesignArea);
        }
        if (!designAreaToShow) {
            const state = Services.store.state;
            const designAreaIdentifier =
                getSelectedDesignArea(state).identifier ||
                getDefaultDesignAreaIdentifierFromProductionMethod(state);
            designAreaToShow =
                this.getDesignAreaByIdentifier(designAreaIdentifier) ||
                first(state.designArea.filteredDesignAreas);
        }

        if (designAreaToShow) {
            return designAreaToShow.designView.identifier;
        }
        return false;
    }

    selectInitialDesignView() {
        const initialDesignViewIdentifier = this.getInitialDesignViewIdentifier();
        initialDesignViewIdentifier && this.selectDesignView(initialDesignViewIdentifier);
    }

    selectFirstDesignArea() {
        const firstDesignAreaIdentifier = get(
            Services.store.state,
            'designArea.filteredDesignAreas[0].identifier'
        );
        if (firstDesignAreaIdentifier) {
            this.selectDesignArea(firstDesignAreaIdentifier);
        } else {
            // eslint-disable-next-line no-console
            console.log('There is no design area to select.');
        }
    }

    clearDesignAreaSelection() {
        this.removeDesignAreaPlaceholders();
        const hasActiveDesignArea = !isEmpty(Services.visualization.getActiveDesignArea());
        // If default Design Area is set then the clear would skip as there will be no selected designArea
        // So forcing clearDesignAreaSelection when there is ActiveDesignArea(Default).
        Services.store.dispatch(clearDesignAreaSelection(hasActiveDesignArea));
        Services.visualization.enableUserMovement();
        Services.visualization.freeUserMovement();
    }

    openDesignAreaEditor() {
        const state = Services.store.state;
        Services.ui.show('designer');
        const designAreaIdentifier = getDesignAreaIdentifierToOpen(state);
        this.clearDesignAreaSelection();
        if (designAreaIdentifier) {
            this.selectDesignArea(designAreaIdentifier);
        } else {
            this.selectFirstDesignArea();
        }
    }

    closeDesignAreaEditor() {
        if (Services.ui.isVisible('designer')) {
            Services.ui.hide('designer');
            Services.visualization.enableUserMovement();
            Services.visualization.restrictUserMovement();

            Services.designData.clearSelectionOnCurrentDesignArea();

            // the canvases should be drawn on the textures to see the changes that were made in the editor
            Services.designData.updateVisualization();
        }
    }

    /**
     * General close handler for design area editor, that calls the correct actions based on the state
     * @return {boolean}
     */
    handleCloseIfNeeded() {
        const state = Services.store.state;

        // if it's 3D and the editor is open, then close it, so we can enter into the "closer look" mode
        // this lets the user see the visualization in the same view, but without the design area editor
        if (visualizationModeCheck(state).is3D() && Services.ui.isVisible('designer')) {
            Services.designData.closeDesignAreaEditor();
            return true;
        }

        // in this case it's 2D, or the design area was already closed by the previous block
        // so the second "click away" should clear the selection (which will trigger switching to default view)
        // if a design area was selected, clear the selection
        if (isAnyDesignAreaSelected(state)) {
            Services.designData.clearDesignAreaSelection();
            return true;
        }
        return false;
    }

    handleDesignProductionMethodChangeFromVectorizationToNonVectorization = async designProductionMethodIdentifier => {
        const state = Services.store.state;

        const { currentVectorsRequired, newVectorsRequired } = getVectorizeForDesignProductionMethods(
            state,
            designProductionMethodIdentifier
        );
        const designArea = getSelectedDesignArea(state);
        const images = getImagesByDesignArea(state, designArea.identifier);

        if (currentVectorsRequired && !newVectorsRequired && images.length) {
            await Services.store.dispatch(showSwitchToProductionMethodWithoutVectorization());
            return true;
        }
    };

    handleDesignProductionMethodChangeFromNonVectorizationToVectorization = async designProductionMethodIdentifier => {
        const state = Services.store.state;

        const { currentVectorsRequired, newVectorsRequired } = getVectorizeForDesignProductionMethods(
            state,
            designProductionMethodIdentifier
        );

        const designArea = getSelectedDesignArea(state);
        const nonVectorizedImages = getImagesByDesignArea(state, designArea.identifier).filter(
            obj => !get(obj, 'imageData.operations.vectorize')
        );

        if (
            nonVectorizedImages.length === 1 &&
            ((!currentVectorsRequired && newVectorsRequired) ||
                !isColorPreviewCompatible(
                    state,
                    nonVectorizedImages[0].imageData,
                    designProductionMethodIdentifier
                ))
        ) {
            await Services.store.dispatch(showSwitchToProductionMethodWithVectorization());
            return true;
        }
    };

    handleDesignProductionMethodChangeToMandatoryVectorization = async designProductionMethodIdentifier => {
        const state = Services.store.state;

        const {
            currentVectorsMandatory,
            newVectorsMandatory,
        } = getMandatoryVectorizationForDesignProductionMethods(state, designProductionMethodIdentifier);

        const designArea = getSelectedDesignArea(state);
        const nonColorizedImages = getImagesByDesignArea(state, designArea.identifier).filter(
            obj => !get(obj, 'imageData.gallery') && !get(obj, 'imageData.displayColorPreview')
        );

        if (
            nonColorizedImages.length === 1 &&
            ((!currentVectorsMandatory && newVectorsMandatory) ||
                !isColorPreviewCompatible(
                    state,
                    nonColorizedImages[0].imageData,
                    designProductionMethodIdentifier
                ))
        ) {
            await Services.store.dispatch(showSwitchToProductionMethodWithVectorization());
            return true;
        }
    };

    checkColorUsageForDesignProductionMethod = async designProductionMethodIdentifier => {
        const state = Services.store.state;
        const { maxColorAmount, designProductionMethod } = getSelectedDesignAreaProperties(state);
        const newProductionMethod = getDesignProductionMethodByIdentifier(
            state,
            designProductionMethodIdentifier
        );
        const newMaxColorAmount = parseInt(get(newProductionMethod, 'options.maxColorAmount', 0));
        const currentVectorsRequired = !!get(designProductionMethod, 'options.vectorsRequired');
        const newVectorsRequired = !!get(newProductionMethod, 'options.vectorsRequired');
        const designArea = getSelectedDesignArea(state);
        const colorPreviewRequired =
            designProductionMethodHasEmbroideryVisualizationEffect(newProductionMethod) ||
            isVectorizedLogoMandatory(newProductionMethod);

        if (
            (newMaxColorAmount !== 0 && newMaxColorAmount < maxColorAmount) ||
            (!currentVectorsRequired && newVectorsRequired) ||
            colorPreviewRequired
        ) {
            const canvas = this._getCanvas(designArea);

            const images = canvas
                ? canvas.objects.filter(obj => {
                      if (obj.type !== 'Image') {
                          return false;
                      }

                      return (
                          !get(obj, 'imageData.operations.vectorize') ||
                          !isColorPreviewCompatible(state, obj.imageData, designProductionMethodIdentifier)
                      );
                  })
                : [];

            if (images.length > 1) {
                await Services.store.dispatch(showProductionMethodRemoveImagesConfirm());

                images.forEach(object => {
                    object.remove();
                });

                await this.updateCanvasData(canvas);
            }
        }

        const missingColorPalettesColors = getMissingColorPalettesColors(
            getColorsWithPalette(state),
            newProductionMethod.colorPalettes,
            !currentVectorsRequired && newVectorsRequired
        );

        if (missingColorPalettesColors.length) {
            // for methods where coloring is disabled we do not have to replace the colors
            // or show the overlay
            if (!isColoringDisabled(state, newProductionMethod.identifier)) {
                await Services.store.dispatch(showProductionMethodChangeColorsConfirm());
                await this.replaceSelectedDesignAreaElementsColor(
                    missingColorPalettesColors,
                    newProductionMethod
                );
            }
        }
    };

    replaceSelectedDesignAreaElementsColor = async (missingColorPalettesColors, designProductionMethod) => {
        const state = Services.store.state;
        const designArea = getSelectedDesignArea(state);
        const designData = getDesignDataByDesignAreaIdentifier(state, designArea.identifier);
        const objects = get(designData, 'canvasData.objects', []);

        await Promise.all(
            objects
                .filter(({ type }) => type === 'Image')
                .map(({ imageData }) => getUserImage(state, imageData.identifier))
                .filter(
                    (image, idx, arr) =>
                        arr.indexOf(image) === idx && // uniq
                        !isColorMappingCompatible(
                            designProductionMethod.colorPalettes,
                            get(image, 'operations.vectorizeColorsMap')
                        )
                )
                .map(image => {
                    const operations = missingColorPalettesColors.reduce(
                        (currentOperations, missingColor) =>
                            replaceColorOnOperations(
                                currentOperations,
                                missingColor.color,
                                missingColor.closestColor
                            ),
                        image.operations
                    );

                    return Services.designData.editFile(operations, image, {
                        immediateCommit: true,
                        forceUpdate: true,
                        designProductionMethod,
                    });
                })
        );

        for (const missingColor of missingColorPalettesColors) {
            Services.store.dispatch(
                replaceDesignAreaTextsColor(
                    designArea.identifier,
                    missingColor.color.identifier,
                    {
                        rgb: missingColor.color.value,
                        hex: rgbStrToHex(missingColor.color.value),
                    },
                    missingColor.closestColor.identifier,
                    missingColor.closestColor.value
                )
            );
        }

        await this.loadCanvasDataFromState([designArea]);
    };

    async vectorizeSingleImageAndShowEditDialog(identifier) {
        const showImageEditDialog =
            (await this.handleDesignProductionMethodChangeFromNonVectorizationToVectorization(identifier)) ||
            (await this.handleDesignProductionMethodChangeToMandatoryVectorization(identifier));

        if (showImageEditDialog) {
            Services.ui.show('globalLoader');

            const state = Services.store.state;
            const designArea = getSelectedDesignArea(state);
            const canvas = this._getCanvas(designArea);

            const images = getImagesByDesignArea(state, designArea.identifier);
            const firstNonVectorizedImage = images.find(
                obj =>
                    !get(obj, 'imageData.gallery') &&
                    (!get(obj, 'imageData.operations.vectorize') ||
                        !get(obj, 'imageData.displayColorPreview'))
            );

            try {
                const operations = {
                    ...firstNonVectorizedImage.imageData.operations,
                    vectorize: true,
                };
                const imageData = firstNonVectorizedImage.imageData;

                //returing if placeHolderImage, from executing edit operations
                if (imageData.isPlaceHolderImage) {
                    return;
                }
                await Services.designData.editFile(operations, imageData, { immediateCommit: true });

                const imageToSelect = canvas.objects
                    .filter(object => object.type === 'Image')
                    .find(obj => obj._imageData.identifier === imageData.identifier);
                imageToSelect.selected = true;

                await Services.designer.openImageEditDialog(
                    firstNonVectorizedImage.imageData.identifier,
                    canvas,
                    {
                        placeholderImageUpdate: false,
                        submitOnExitClick: true,
                    }
                );
            } finally {
                Services.ui.hide('globalLoader');
            }
        }
    }

    async restoreVectorizedImages(identifier) {
        const displayImagesAsUnVectorized = await this.handleDesignProductionMethodChangeFromVectorizationToNonVectorization(
            identifier
        );

        if (displayImagesAsUnVectorized) {
            const state = Services.store.state;
            const designArea = getSelectedDesignArea(state);
            let images = getImagesByDesignArea(state, designArea.identifier);
            images = images.filter(img => !get(img, 'imageData.isPlaceHolderImage', false)); //excluding placeHolderImage from executing edit operations

            for (const image of images) {
                const operations = removeVectorizeFromOperations(image.imageData.operations);
                await Services.designData.editFile(operations, image.imageData, {
                    immediateCommit: true,
                    displayColorPreview: getConf('autoPreviewVectorization', true),
                });
            }

            await this.loadCanvasDataFromState([designArea]);
        }
    }

    async updateObjectsAfterProductionMethodChange(oldProductionMethod, newProductionMethod) {
        const { canvasData } = getDesignDataForSelectedDesignArea(Services.store.state);
        if (!canvasData) {
            return;
        }
        const { objects } = canvasData;
        const newObjects = applyProductionMethodChangeToCanvasObjects(
            newProductionMethod,
            oldProductionMethod,
            objects
        );
        if (newObjects === objects) {
            return;
        }
        this.setCanvasData({ ...canvasData, objects: newObjects });
        const designArea = getSelectedDesignArea(Services.store.state);
        await this.updateCanvasDataFromState([designArea]);
    }

    checkDesignElementRestrictions(targetDesignProductionMethodIdentifier) {
        const state = Services.store.state;
        const designData = getDesignDataForSelectedDesignArea(state);
        const designProductionMethod = getDesignProductionMethodByIdentifier(
            state,
            targetDesignProductionMethodIdentifier
        );

        const result = checkDesignElementRestrictionsWithRemovalCount(designData, designProductionMethod);

        if (result) {
            Services.ui.reportError(
                t(
                    `designElementRestrictions.productionMethodSwitchMessages.${result.type}${
                        result.max === 0 ? '_0' : result.toBeRemovedCount === 1 ? '_1' : ''
                    }`,
                    result
                )
            );
            return false;
        }
        return true;
    }

    async setDesignProductionMethod(identifier) {
        if (!this.checkDesignElementRestrictions(identifier)) {
            return;
        }

        this.clearSelectionOnCurrentDesignArea();
        // check if the selected design production method has a higher minimum font size
        // than any of the text elements on the canvas
        const state = Services.store.state;
        const designData = getDesignDataForSelectedDesignArea(state);
        const selectedDesignProduction = getDesignProductionMethodByIdentifier(state, identifier);
        const bulkNameSelected = hasSelectedBulkName(designData);
        if (!isWidget(location.pathname)) {
            const fontSizeMin = getFontSizeMin(state, identifier);
            const smallestFontSize = findSmallestFontSizeInCanvasData(designData.canvasData);
            try {
                smallestFontSize < fontSizeMin &&
                    (await Services.store.dispatch(showMinFontSizeConfirm(fontSizeMin)));
            } catch (e) {
                return; // rejected, do nothing
            }

            try {
                await this.checkColorUsageForDesignProductionMethod(identifier);
                await this.vectorizeSingleImageAndShowEditDialog(identifier);
                await this.restoreVectorizedImages(identifier);
            } catch (e) {
                // return only if the error is a confirm result
                if (isConfirmResult(e)) {
                    return;
                }
                throw e;
            }

            try {
                bulkNameSelected &&
                    !selectedDesignProduction.allowBulkNames &&
                    (await Services.store.dispatch(showProductionMethodChangeConfirm()));
            } catch (e) {
                return; // rejected, do nothing
            }
        }

        const oldDesignProductionMethod = getSelectedDesignProductionMethod(state);

        Services.store.dispatch(setDesignProductionMethod(identifier));

        if (isCanvasUpdateRequired(getSelectedDesignArea(state), selectedDesignProduction)) {
            Services.store.dispatch(recalculateDesignArea());
        }

        this.updateCanvasToCurrentDesignProductionMethod();
        if (bulkNameSelected && !selectedDesignProduction.allowBulkNames) {
            this.removeBulkNameItems(designData);
            !isBulkNamesSelected(Services.store.state.configurator) && Services.configurator.clearBulkNames();
        }

        await this.updateObjectsAfterProductionMethodChange(
            oldDesignProductionMethod,
            selectedDesignProduction
        );
    }

    updateCanvasToCurrentDesignProductionMethod() {
        // the mask should be updated when switching design production method
        const state = Services.store.state;
        const designArea = getSelectedDesignArea(state);
        const canvas = this._getCanvas(designArea);
        const maskHandler = canvas.getMaskHandler();
        const maskData = getMaskDataForDesignArea(state, designArea);
        if (maskData) {
            maskHandler.init(maskData);
        } else {
            maskHandler && maskHandler.dispose();
        }

        const colorOverlayData = getColorOverlayData(state, designArea);
        const colorOverlay = canvas.getColorOverlay();
        if (colorOverlayData?.color) {
            colorOverlay?.init(colorOverlayData);
        } else {
            colorOverlay?.dispose();
        }
    }

    setCanvasData(canvasData, targetDesignAreaIdentifier) {
        const extendedCanvasData = this.getExtendedCanvasData(canvasData);
        Services.store.dispatch(setCanvasData(extendedCanvasData, targetDesignAreaIdentifier));
    }

    getExtendedCanvasData(canvasData) {
        if (!getConf('designer.includeMetricContentSize', false) || !canvasData?.objects?.length) {
            return canvasData;
        }

        return extendCanvasDataWithMetricContentSize(canvasData);
    }

    async setColorAmount(maxColorCount) {
        // check the current color usage before applying the selected color count
        // so if the color count is more than the selected maxColorCount the user can decide to
        // 1. proceed with the new value
        // 2. leave the current value (skip applying the newly selected one)
        const currentColorCount = getSelectedDesignAreaProperties(Services.store.state).colorAmount;
        const colorCount = getColors(Services.store.state).length;

        if (
            maxColorCount &&
            colorCount > maxColorCount &&
            !isWidget(location.pathname) &&
            getConf('displayColorAmountWarnings', true)
        ) {
            try {
                await Services.store.dispatch(
                    showMaxColorChangeConfirm(colorCount, maxColorCount, currentColorCount)
                );
            } catch (e) {
                return;
            }
        }

        Services.store.dispatch(setColorAmount(maxColorCount));
    }

    /**
     * Call the updateTexture function to update every area on the visualization.
     * That calls back to the provided drawingFunction with the getDrawingContext at the first argument.
     *
     */
    async updateVisualization(updateEventObject = {}) {
        if (!Services.visualization.isReadyToRender()) {
            return;
        }

        // if textures are not available to draw on, just render the model (eg. creator mode without design)
        if (!Services.visualization.isReadyToRenderCustomDesign()) {
            Services.visualization.render();
            return;
        }

        const isDesignAreaSelected = !!getSelectedDesignArea(Services.store.state).identifier;
        const isDesignerOpen = isDesignAreaSelected && Services.ui.isVisible('designer');
        const isScreenshotEvent = get(updateEventObject, 'data.screenshot');

        const drawingFunction =
            isDesignerOpen && !isScreenshotEvent && !this.isInPreviewMode
                ? this.redrawEmptyAreas
                : this.redrawAllDesignAreas;

        await Services.visualization.updateTexture({
            updateEventObject,
            drawingFunction: drawingFunction.bind(this),
        });
    }

    getFontSizeMin() {
        return getFontSizeMin(Services.store.state);
    }

    /**
     * Gets the drawing context and draws all design areas (that has existing canvas) on the texture.
     * It also handles the highlighting of the design areas.
     * @param {function} getDrawingContext
     * @param {object} updateEventObject
     */
    async redrawAllDesignAreas(getDrawingContext, updateEventObject, options = {}) {
        const state = Services.store.state;

        const showHighlight = get(state, 'designArea.showHighlight');

        const textureBuilderConfig = merge(
            {
                highlight: {
                    borderColor: getClientHighlightColor(state),
                    backgroundColor: getConf('visualization.designAreaHighlightBackgroundColor'),
                },
            },
            options.textureBuilderConfig
        );

        const variantIdentifier = get(updateEventObject, 'data.variant', false);

        const effectEnabled = !isDesignerProductPreviewMode();

        const drawingFn = async (textureCtx, getTextureBuilder, designAreas = this.getDesignAreas()) => {
            const {
                getTextureImage,
                drawCanvasToArea,
                drawCanvasToAreaDirect,
                highlightDesignArea,
                drawOn,
                getParams,
            } = getTextureBuilder(textureBuilderConfig);

            if (!getTextureImage()) {
                // eslint-disable-next-line no-console
                console.warn('Texture image is missing!', designAreas);
                return;
            }

            if (showHighlight) {
                for (const designArea of designAreas) {
                    const canvas = this._getCanvas(this.getDesignAreaByIdentifier(designArea.identifier));
                    if (!canvas) {
                        continue;
                    }
                    const releaseLock = await requestLock(canvas);
                    const maskHandler = canvas.getMaskHandler();
                    if (designAreaAppliesMask(state, designArea) && maskHandler) {
                        const restoreOutlineVisibility = maskHandler.toggleOutline(true, {
                            getZoomFactor: ({ zoom }) => {
                                // currentZoom is only available for 2D, for 3D it is always 1
                                const threeFactor = Math.min(getParams().currentZoom, 1) || 1;
                                return zoom * threeFactor;
                            },
                        });
                        const restoreOutlineOnly = maskHandler.showOutlineOnly();
                        const drawToContext = canvas.drawToContext.bind(canvas);
                        await drawCanvasToAreaDirect({
                            drawToContext,
                            designArea,
                        });
                        restoreOutlineVisibility?.();
                        restoreOutlineOnly?.();
                    } else {
                        highlightDesignArea(designArea);
                    }
                    releaseLock();
                }
            }

            for (const designArea of designAreas) {
                const canvas = this._getCanvas(this.getDesignAreaByIdentifier(designArea.identifier));
                if (!canvas) {
                    continue;
                }
                const releaseLock = await requestLock(canvas);

                const selectedObject = get(canvas, 'selectedObject');
                if (selectedObject) {
                    selectedObject.selected = false;
                }

                const restoreColorOverlayVisibility = canvas.getColorOverlay()?.toggle(false);
                const restoreOutlineVisibility = canvas.getMaskHandler()?.toggleOutline(false);

                const drawToContext = canvas.drawToContext.bind(canvas);
                const getSvg = () => canvas.getScreenshot(getFontMapping(state));
                await drawCanvasToArea({
                    drawToContext,
                    designArea,
                    getSvg,
                    ...(effectEnabled &&
                        getVisualizationEffectWithOptions(state, designArea, variantIdentifier)),
                });
                restoreColorOverlayVisibility?.();
                restoreOutlineVisibility?.();
                releaseLock();
            }

            drawOn(textureCtx);
        };

        try {
            await getDrawingContext(
                drawingFn,
                true /* update alpha */,
                true /* clean up texture before drawing */
            );
        } catch (e) {
            // eslint-disable-next-line no-console
            console.warn('Could not get drawing context, texture update is skipped!', e.message);
        }
    }

    redrawEmptyAreas(getDrawingContext) {
        getDrawingContext(
            async () => {
                // for now do nothing here, only clear the area
            },
            true /* update alpha */,
            true /* clean up texture before drawing */
        );
    }

    toggleHighlight(show, update = true) {
        Services.store.dispatch(toggleHighlight(show));
        if (update) {
            this.updateVisualization();
        }
    }

    toggleIsInPreviewMode(isInPreviewMode) {
        this.isInPreviewMode = isInPreviewMode;

        if (!this.isInPreviewMode) {
            const hasSelectedDesignArea = !!getSelectedDesignArea(Services.store.state).identifier;

            if (hasSelectedDesignArea) {
                Services.visualization.disableUserMovement();
            }
        }
    }

    createHighlightMemo(tmpState) {
        const origState = isHighlighted(Services.store.state);
        this.toggleHighlight(tmpState, false);
        return {
            restore: () => {
                this.toggleHighlight(origState, true);
            },
        };
    }

    async getScreenshots(fontMapping, options = {}) {
        const { screenshotFn = 'getScreenshot' } = options;
        return (
            await Promise.all(
                this.getDesignAreas()
                    .map(area => ({ id: area.identifier, canvas: this._getCanvas(area) }))
                    .filter(
                        ({ canvas }) =>
                            canvas && canvas[screenshotFn] && canvas.objects && canvas.objects.length > 0
                    )
                    .map(async ({ id, canvas }) => {
                        const releaseLock = await requestLock(canvas);
                        const restoreOutlineVisibility = canvas.getMaskHandler()?.toggleOutline(false);
                        const restoreColorOverlayVisibility = canvas.getColorOverlay()?.toggle(false);
                        const result = [id, await canvas[screenshotFn](fontMapping)];
                        restoreOutlineVisibility?.();
                        restoreColorOverlayVisibility?.();
                        releaseLock();
                        return result;
                    })
            )
        ).reduce((screenShots, [id, screenshot]) => ({ ...screenShots, [id]: screenshot }), {});
    }

    async getScreenshotsOfElements(fontMapping) {
        return await this.getScreenshots(fontMapping, { screenshotFn: 'getScreenshotsOfElements' });
    }

    /**
     * Provides a simple interface to add or remove sample text to the canvas related to the given designAreaIdentifier.
     * It also provides a function to modify the color amount, it simply clears the canvas and re-adds the sample text
     * with the planned number of colors.
     * Note: this function has more notable side effects
     *  - it modifies the selected design area if the designAreaIdentifier is provided
     *  - it clears the canvas in the interface methods, so all the previously added items will be removed
     * @param designAreaIdentifier
     * @returns {{toggleSampleText: (function(*=)), setColorAmount: (function(*=))}}
     */
    getSampleCanvasEditorForDesignArea(designAreaIdentifier = false) {
        designAreaIdentifier && Services.store.dispatch(selectDesignArea(designAreaIdentifier));
        const designArea = Services.store.state.designArea.selectedDesignArea;
        const identifier = designArea.identifier;
        const canvas = this._getCanvas(designArea);
        if (canvas) {
            const previouslyContainedSampleText = canvas.getLength() > 0;
            const addSampleText = () => {
                const state = Services.store.state;
                const designAreaData = getDesignDataByDesignAreaIdentifier(state, identifier);
                const colorAmount = Math.max(get(designAreaData, 'colorAmount', 0), 1);
                const text = t('calculationWidget.textContent', { colorAmount });
                Services.designer.addText(canvas, text, colorAmount);
                // eslint-disable-next-line no-console
                console.log(`Sample text added to canvas: ${identifier}`);
            };
            const clearTextObjects = () => {
                const objects = canvas.objects.filter(obj => obj.type === 'Text');
                objects.forEach(obj => obj.remove());
            };
            return {
                toggleSampleText: active => {
                    if (active) {
                        addSampleText();
                    } else {
                        canvas.clear();
                    }
                    this.updateCanvasData(canvas);
                },
                setColorAmount: value => {
                    clearTextObjects();
                    this.setColorAmount(value);
                    previouslyContainedSampleText && addSampleText();
                    const imgObjects = canvas.objects.filter(obj => obj.type !== 'Text');
                    let canvasData = this._serializeCanvas(canvas);
                    if (imgObjects.length > 0) {
                        canvasData = this.calculateObjectPosition(canvas);
                    }
                    this.setCanvasData(canvasData, identifier);
                },
            };
        }
    }

    async addSampleImageForDesignAreas(designAreaIdentifiers, active) {
        await Promise.all(
            designAreaIdentifiers.map(async designAreaIdentifier => {
                const designArea = this.getDesignAreaByIdentifier(designAreaIdentifier);
                const canvas = this._getCanvas(designArea);
                const state = Services.store.state;
                const imageSrc = placeholderLogo;
                if (active) {
                    const imageData = createSVGImageData({
                        fileName: 'designer_placeholder_logo.svg',
                        imageUrl: imageSrc,
                        thumbUrl: imageSrc,
                        isPlaceHolderImage: true,
                        galleryImage: false,
                    });
                    await Services.designer.addImageFromUrl(
                        canvas,
                        getPreviewUrlFromImageData(imageData.preview),
                        imageData,
                        { enlargeImage: true }
                    );
                    const canvasDataJSON = this.calculateObjectPosition(canvas);
                    // eslint-disable-next-line no-console
                    console.log(`Sample image added to canvas: ${designAreaIdentifier}`);
                    this.setCanvasData(canvasDataJSON, designAreaIdentifier);
                } else {
                    const designData = getDesignDataByDesignAreaIdentifier(state, designAreaIdentifier);
                    const { canvasData } = designData;
                    if (!canvasData) {
                        return;
                    }
                    const objects = canvasData ? canvasData.objects.filter(obj => obj.type === 'Text') : [];
                    this.setCanvasData({ ...canvasData, objects }, designAreaIdentifier);
                    await this.loadCanvasDataFromState([designArea]);
                }
                return {
                    canvas,
                    designAreaIdentifier,
                };
            })
        );
    }

    async addImageToDesignArea(designArea, imageData) {
        const canvas = this._getCanvas(designArea);
        if (!canvas) {
            return false;
        }
        return await Services.designer.addImageFromUrl(
            canvas,
            getPreviewUrlFromImageData(imageData.preview),
            imageData,
            { enlargeImage: true, scaleToHalf: false, autoSelect: false }
        );
    }

    calculateObjectPosition(canvas) {
        const canvasData = this._serializeCanvas(canvas);
        const textObject = canvasData ? canvasData.objects.find(obj => obj.type === 'Text') : [];
        const imageObject = canvasData ? canvasData.objects.find(obj => obj.type === 'Image') : [];
        const canvasTextObject = canvas.objects.find(obj => obj.uuid === textObject.uuid);
        const canvasImageObject = canvas.objects.find(obj => obj.uuid === imageObject.uuid);
        if (canvasTextObject && canvasImageObject) {
            const canvasImageObjectSize = canvasImageObject.getBoundSize();
            const canvasTextObjectSize = canvasTextObject.getBoundSize();
            textObject.y = imageObject.y + canvasImageObjectSize.height / 2 + canvasTextObjectSize.height;
            const objects = [].concat(textObject, imageObject);
            return { ...canvasData, objects };
        }
        return canvasData;
    }

    applyDesignAreaPreSelection(filteredDesignAreas) {
        const preSelectionDesignAreaIdentifier = get(
            getConf('designerPreSelection', {}),
            'designAreas[0].identifier'
        );

        if (preSelectionDesignAreaIdentifier) {
            this.selectDesignArea(preSelectionDesignAreaIdentifier);
        }

        for (let area of filteredDesignAreas) {
            const selectedDesignArea = getDesignAreaPreSelection(area.identifier);

            if (selectedDesignArea.identifier) {
                const preSelectionColorAmount = +get(selectedDesignArea, 'colorAmount', 1);

                if (!isNaN(preSelectionColorAmount)) {
                    const sampleCanvasEditor = Services.designData.getSampleCanvasEditorForDesignArea(
                        area.identifier
                    );
                    sampleCanvasEditor.setColorAmount(preSelectionColorAmount);
                    sampleCanvasEditor.toggleSampleText(true);
                }
            }
        }
    }

    changeAllColorsNeeded() {
        const { maxColorAmount, hasEngravingBackgroundColors } = getSelectedDesignAreaProperties(
            Services.store.state
        );

        return maxColorAmount === 1 && !hasEngravingBackgroundColors;
    }

    async setImageAndTextColor(color) {
        if (!this.changeAllColorsNeeded()) {
            return;
        }

        const designArea = getSelectedDesignArea(Services.store.state);
        const designData = getDesignDataByDesignAreaIdentifier(Services.store.state, designArea.identifier);
        const objects = get(designData, 'canvasData.objects', []);
        const hasmultipleObjects = objects.length > 1;
        if (!hasmultipleObjects) {
            return;
        }
        const canvas = this._getCanvas(designArea);
        const isImageSelected = canvas.selectedObject && canvas.selectedObject.type === 'Image';
        const selectedImageIdentifier = isImageSelected
            ? get(Services.store.state, 'imageGallery.selectedImage.identifier', null)
            : null;

        Services.store.dispatch(setDesignAreaTextsColor(designArea.identifier, color));

        for (const object of objects) {
            if (object.type === 'Image' && object.imageData.identifier !== selectedImageIdentifier) {
                const image = getUserImage(Services.store.state, object.imageData.identifier);
                const operations = setColorOnOperations(image.operations, color);

                await Services.designData.editFile(operations, image);
            }
        }
        await this.updateCanvasDataFromState([designArea]);
    }

    async getGalleryImageColors(url) {
        const svgContent = await getSvgContent(url, url.startsWith('http'));

        const designProductionMethod = getSelectedDesignProductionMethod(Services.store.state);

        return createVectorizeColorsMap({}, getSvgColors(svgContent, designProductionMethod));
    }

    async getSVGImageSize(url) {
        const svgContent = await getSvgContent(url, url.startsWith('http'));
        const svgTag = svgContent.match(/<svg[^>]+>/)?.[0];
        const width = svgContent.match(/width="([^"]+)"/)?.[1];
        const height = svgContent.match(/height="([^"]+)"/)?.[1];
        if (width && height) {
            return {
                width: parseFloat(width),
                height: parseFloat(height),
            };
        }
        const viewBoxValues = svgTag.match(/viewBox="([^"]+)"/)?.[1]?.split(' ');
        if (viewBoxValues.length === 4) {
            return {
                width: parseFloat(viewBoxValues[2]),
                height: parseFloat(viewBoxValues[3]),
            };
        }
        return {
            width: 300,
            height: 300,
        };
    }

    updateDesignAreaImagesWithSelectedImageData(originalImageIdentifier = '') {
        const imageIdentifier = get(Services.store.state, 'imageGallery.selectedImage.identifier');
        const newImage = find(Services.store.state.imageGallery.userImages, {
            identifier: imageIdentifier,
        });
        this.removeDesignAreaPlaceholders(false, true);
        newImage && Services.store.dispatch(updateImageData(newImage, originalImageIdentifier));
    }

    /**
     * Calls editFile in a queued way to make sure the current image data state is not updated simultaneously
     * by other editFileQueued calls
     * Note: it works only with editFileQueued calls (does not guard against calling editFile directly)
     */
    async editFileQueued(operations, image, options = {}) {
        const releaseLock = await requestLock(this);
        try {
            return await this.editFile(operations, image, options);
        } finally {
            releaseLock();
        }
    }

    async editFile(operations, image, options = {}) {
        // patch operations to keep other unchanged values
        operations = {
            ...image.operations,
            ...operations,
        };

        const {
            immediateCommit = false,
            designProductionMethod = getSelectedDesignProductionMethod(Services.store.state),
            addMode = false,
            displayColorPreview,
        } = options;

        let imageIdentifier = image.identifier;
        let preview = image.preview;

        const compatibilityInfoHash = getDesignAreaImageCompatibilityInfo(designProductionMethod);
        const imageIsCompatible = areHashesCompatible(image.compatibilityInfoHash, compatibilityInfoHash);
        const operationsMatching = compareOperations(image.operations, operations);
        const vectorizedLogoMandatory = isVectorizedLogoMandatory(designProductionMethod);

        const colorPreviewRequired =
            designProductionMethodHasEmbroideryVisualizationEffect(designProductionMethod) ||
            vectorizedLogoMandatory;

        const imageDataPatch = {
            compatibilityInfoHash,
            colorPreviewRequired,
            vectorizedLogoMandatory,
            ...(displayColorPreview !== undefined && { displayColorPreview }),
        };

        if (!operationsMatching || !imageIsCompatible) {
            if (!image.operations) {
                operations.vectorizeThreshold = null;
            }

            operations = makeOperationsCompatible(
                operations,
                compatibilityInfoHash,
                designProductionMethod.colorPalettes
            );

            if (image.gallery) {
                operations = {
                    ...(operations || {}),
                    vectorizeColorsMap: operations.vectorizeColorsMap,
                };
            } else {
                const { designAreas, count } = getImageUsageInfo(Services.store.state, image.identifier);

                const createCopy =
                    !!(designAreas > 1 && image.compatibilityInfoHash && !imageIsCompatible) ||
                    (!operationsMatching && count > (addMode ? 0 : 1));

                const editData = getEditDataFromOperations(
                    image.fileName,
                    operations,
                    designProductionMethod
                );

                const currentEditData = getEditDataFromOperations(
                    image.fileName,
                    image.operations || {},
                    designProductionMethod
                );

                const isBackendNeeded = createCopy || !isEqual(editData, currentEditData);

                if (isBackendNeeded) {
                    await Services.store.dispatch(
                        editImage(
                            {
                                ...editData,
                                createCopy,
                            },
                            {
                                ...imageDataPatch,
                                operations,
                                identifier: imageIdentifier,
                                incomplete: true,
                            },
                            { immediateCommit }
                        )
                    );
                    const {
                        imageGallery: { selectedImage },
                    } = Services.store.state;
                    preview = get(selectedImage, 'preview');
                    imageIdentifier = get(selectedImage, 'identifier');
                }
            }
        }

        if (!preview || !preview.url) {
            throw new Error('Preview image url is not specified!');
        }

        if (isEditableVectorImage(preview)) {
            const designProductionMethod = getSelectedDesignProductionMethod(Services.store.state);
            const svgContent = await getSvgContent(
                preview.url,
                preview.url.startsWith('http'),
                !operationsMatching
            );
            const svgColors = getSvgColors(svgContent, designProductionMethod);

            const vectorizeColorsMap = createVectorizeColorsMap(operations.vectorizeColorsMap, svgColors);
            const thresholdChanged =
                operations.vectorizeThreshold !== get(image, 'operations.vectorizeThreshold', 0);

            // prepare initial values for the vectorize feature
            if (!operations.vectorizeThreshold) {
                const maxPotentialColorAmount =
                    +get(preview, 'additionalData.maxPotentialColorAmount') || svgColors.length;
                operations = {
                    ...operations,
                    vectorizeMaxColorAmount: maxPotentialColorAmount,
                    vectorizeThreshold: getAggregatedColorCount(vectorizeColorsMap),
                };

                const vectorizeOriginalColorsMap = createVectorizeColorsMap({}, svgColors);
                operations = {
                    ...operations,
                    vectorizeOriginalColorsMap,
                    vectorizeColorsMap,
                };
            } else if (thresholdChanged) {
                operations.vectorizeColorsMap = expandVectorizeColorsMap(
                    vectorizeColorsMap,
                    designProductionMethod.colorPalettes,
                    operations.vectorizeThreshold
                );

                // gallery images are not edited by the api, colors have to be reduced programmatically
                if (image.gallery) {
                    const numberOfAggregatedColors = getAggregatedColorCount(operations.vectorizeColorsMap);

                    const numberOfColorsToRemove = numberOfAggregatedColors - operations.vectorizeThreshold;
                    if (numberOfColorsToRemove > 0) {
                        operations.vectorizeColorsMap = reduceNumberOfDistinctColorsBy(
                            operations.vectorizeColorsMap,
                            numberOfColorsToRemove
                        );
                    }
                }
            } else {
                operations.vectorizeColorsMap = vectorizeColorsMap;
            }
        }

        Services.store.dispatch(
            updateImage(
                imageIdentifier,
                {
                    operations,
                    ...imageDataPatch,
                    incomplete: false,
                },
                immediateCommit
            )
        );
        if (immediateCommit) {
            this.updateDesignAreaImagesWithSelectedImageData(image.identifier);
        }

        return {
            vectorizeColorsMap: operations.vectorizeColorsMap,
            vectorizeThreshold: operations.vectorizeThreshold,
            vectorizeMaxColorAmount: operations.vectorizeMaxColorAmount,
            vectorizeOriginalColorsMap: operations.vectorizeOriginalColorsMap,
        };
    }

    async resetEditedFile(image) {
        const state = Services.store.state;
        if (!image) {
            image = state.imageGallery.selectedImage;
        }
        const { operations } = image;
        const {
            vectorizeOriginalColorsMap: { ...vectorizeColorsMap },
        } = operations;
        let resetOperations;
        if (image.gallery) {
            resetOperations = {
                ...operations,
                vectorizeColorsMap,
            };
        } else {
            const designProductionMethod = getSelectedDesignProductionMethod(state);
            resetOperations = {
                ...operations,
                ...(!designProductionMethod.options.vectorsRequired
                    ? removeVectorizeFromOperations(operations)
                    : {
                          vectorizeColorsMap,
                          vectorizeThreshold: null,
                      }),
            };
        }
        return await this.editFileQueued(resetOperations, image);
    }

    commitChanges() {
        Services.store.dispatch(commitChanges());
    }

    clearSelectionOnCurrentDesignArea() {
        const designArea = getSelectedDesignArea(Services.store.state);
        const canvas = this._getCanvas(designArea);
        if (canvas) {
            canvas.clearSelection();
        }
    }
}
