import get from 'lodash/get';
import * as storage from '../../Storage';
import { imageDataKey, loaderKey } from './Constants';
import { Services } from '../../ServiceLocator';
import {
    designProductionMethodHasEmbroideryVisualizationEffect,
    getCameraViewListForDesignAreaList,
    getDefaultDesignAreaIdentifierFromProductionMethod,
    getSelectedDesignProductionMethodForDesignArea,
    isVectorizedLogoMandatory,
} from '../../Reducers/DesignArea/Selectors';
import { getSelectedVariantIdentifier } from '../../Reducers/Configurator/Selectors';
import { setSelectedImageData } from '../../Reducers/ImageGallery/Actions';
import { createVectorizeColorsMap } from '../../Reducers/DesignData/Modifiers';
import { getSvgColors, getSvgContent } from '../../Services/DesignDataService';
import { getFirstDataURLFromScreenshotsObj } from '../../Modules/Visualization/Utils/getFirstDataURLFromScreenshotsObj';
import createMessageInterface from '../../Utils/createMessageInterface';

/**
 * A helper function that prepares the image data to be used on the design area
 * In case of design production methods with embroidery effect it also creates the color map for the image,
 * so it can be shown as it was added from the image edit dialog.
 * It uses memoization in order to minimize the execution time for repeated requests
 */
const prepareImageData = (function createImageDataPreProcessor() {
    let lastHash;
    const imageDataMemo = new Map();

    function getImageDataHash(imageData) {
        return JSON.stringify(
            ['identifier', 'preview.url', 'operations.clipping', 'operations.clippingColor'].map(prop =>
                get(imageData, prop)
            )
        );
    }

    return async function prepareImageData(designArea, imageData) {
        const hash = getImageDataHash(imageData);
        if (hash !== lastHash) {
            lastHash = hash;
            imageDataMemo.clear();
        }
        const { state } = Services.store;
        const designProductionMethod = getSelectedDesignProductionMethodForDesignArea(state, designArea);

        if (imageDataMemo.has(designProductionMethod.identifier)) {
            return imageDataMemo.get(designProductionMethod.identifier);
        }

        let patchedImageData;
        const colorPreviewRequired = designProductionMethodHasEmbroideryVisualizationEffect(
            designProductionMethod
        );
        const vectorizedLogoMandatory = isVectorizedLogoMandatory(designProductionMethod);

        if (colorPreviewRequired) {
            // creating the color map, as it was added from the image edit dialog
            const svgContent = await getSvgContent(
                imageData.preview.url,
                imageData.preview.url.startsWith('http'),
                true
            );
            const svgColors = getSvgColors(svgContent, designProductionMethod);
            const vectorizeColorsMap = createVectorizeColorsMap({}, svgColors);
            patchedImageData = {
                ...imageData,
                colorPreviewRequired: true,
                displayColorPreview: true,
                colorPreviewEnabled: !vectorizedLogoMandatory,
                operations: {
                    ...imageData.operations,
                    vectorizeColorsMap,
                },
            };
        } else {
            // the color replacement call returns the same url for every call, making the browser serve the image
            // from cache, to force reloading a random query string has to be added to the url
            patchedImageData = {
                ...imageData,
                preview: {
                    ...imageData.preview,
                    url: imageData.preview.url.replace(/svg|png$/, `png?${Math.random()}`),
                },
            };
        }
        imageDataMemo.set(designProductionMethod.identifier, patchedImageData);
        return patchedImageData;
    };
})();

/**
 * Creates a preview item object, which is responsible for
 *  - loading the product,
 *  - adding the image to the design area,
 *  - creating the preview image
 *  - additionally it helps to control the flow
 * @param identifier
 * @param event
 */
function createPreviewItem(identifier, event) {
    const PreviewItemStatus = {
        pending: 1,
        loading: 2,
        loaded: 3,
        screenshotting: 4,
        finished: 5,
    };

    let status = PreviewItemStatus.pending;
    let image;
    let error;

    /**
     * @return {Promise<void>}
     */
    async function load() {
        status = PreviewItemStatus.loading;
        try {
            Services.configurator.setLoadIdentifier(identifier);
            await Services.configurator.loadConfigurationForDesignerProductPreview();
            status = PreviewItemStatus.loaded;
        } catch (e) {
            error = e.toString();
            status = PreviewItemStatus.finished;
        }
    }

    /**
     * @param {object} imageData
     * @param {object} size
     * @return {Promise<{identifier: string, image: string, error: string, event: *}|undefined>}
     */
    async function createImage(imageData, size) {
        if (status === PreviewItemStatus.loaded) {
            status = PreviewItemStatus.screenshotting;
        } else {
            return;
        }
        try {
            const { state } = Services.store;
            const { filteredDesignAreas } = state.designArea;
            const defaultDesignAreaIdentifier = getDefaultDesignAreaIdentifierFromProductionMethod(state);
            const designArea =
                filteredDesignAreas.find(({ identifier }) => identifier === defaultDesignAreaIdentifier) ||
                filteredDesignAreas[0];
            const areaList = designArea ? [designArea.identifier] : [];
            const viewList = getCameraViewListForDesignAreaList(filteredDesignAreas, areaList);
            const variantList = [getSelectedVariantIdentifier(state.configurator)];

            Services.designData.toggleHighlight(false, false);

            if (designArea) {
                await Services.designData.addImageToDesignArea(
                    designArea,
                    await prepareImageData(designArea, imageData)
                );
            }
            const screenshot = await Services.visualization.getScreenshots({
                areaList,
                viewList,
                variantList,
                size,
            });
            image = getFirstDataURLFromScreenshotsObj(screenshot);
            status = PreviewItemStatus.finished;
        } catch (e) {
            error = e.toString();
            status = PreviewItemStatus.finished;
        }
        return {
            identifier,
            image,
            error,
            event,
        };
    }

    return {
        load,
        createImage,
        isFinished: () => status === PreviewItemStatus.finished,
    };
}

/**
 * Helper interface for localStorage
 * @param {object} options
 */
function createStorage(options = {}) {
    const { localStorage = storage } = options;

    return {
        /**
         * @param {string} key
         * @param {any} value
         */
        set(key, value) {
            localStorage.local.setItem(key, JSON.stringify(value));
        },
        /**
         * @param {string} key
         * @return {any}
         */
        get(key) {
            try {
                const serializedValue = localStorage.local.getItem(key);
                return serializedValue && JSON.parse(serializedValue);
            } catch (e) {
                // ignore errors
            }
        },
    };
}

/**
 * Provides interface to both the container window (with postMessage API) and the React component
 * Further responsibilities:
 *  - stores the applied image in localStorage or in memory if not available and restores it on page load
 *  - hooks into the visualization service to get notified when it is ready to create the preview image
 * @param {object} options
 */
export function createDesignerProductPreviewManager(options = {}) {
    const { localStorage = createStorage(), messageInterface = createMessageInterface() } = options;

    let currentImageData = localStorage.get(imageDataKey);
    let size = { width: 200, height: 200 };
    let currentItem;
    const previewRequests = [];
    const previewStack = [];

    //// actions provided for the container window

    /**
     * Usage from the container window:
     * configuratorIframe.postMessage({
     *   type: 'setItemIdentifiers',
     *   identifiers: ['product1', 'product2', ...]
     * })
     * @param event
     */
    messageInterface.addListener('setItemIdentifiers', function setItemIdentifiers(event) {
        previewRequests.length = 0; // clear all previous element
        const identifiers = get(event, 'data.identifiers') || [];
        identifiers.forEach(identifier => {
            previewRequests.push({ identifier, event });
        });

        // if image is available, update products when new identifiers arrive
        if (get(currentImageData, 'preview.url')) {
            updateImages();
        }
    });

    /**
     * Usage from the container window:
     * configuratorIframe.postMessage({
     *   type: 'setSize',
     *   size: { width: 800, height: 600 }
     * })
     * @param event
     */
    messageInterface.addListener('setSize', function setSize(event) {
        const width = +get(event, 'data.size.width');
        const height = +get(event, 'data.size.height');
        if (width) {
            size.width = width;
        }
        if (height) {
            size.height = height;
        }
    });

    //// actions dispatched to the container window

    // notifies the origin if a preview image is ready to be shown
    function dispatchPreviewItem(screenshotItem) {
        const { event, ...previewData } = screenshotItem;
        messageInterface.dispatch(
            { type: 'configuratorware_designer_preview_widget_dataready', ...previewData },
            { targetWindow: event.source }
        );
    }

    // tells the container window the preview widget is ready and started listening for actions
    function dispatchLoaded() {
        messageInterface.dispatch({ type: 'configuratorware_designer_preview_widget_loaded' });
    }

    //// flow control helpers

    function processNextProduct() {
        setTimeout(() => {
            if (currentItem && !currentItem.isFinished()) {
                return;
            }
            currentItem = previewStack.pop();
            if (!currentItem) {
                Services.ui.hide(loaderKey);
                return;
            }
            currentItem.load();
        });
    }

    async function createPreviewImage() {
        if (!currentItem) {
            processNextProduct();
            return;
        }

        const previewItem = await currentItem.createImage(currentImageData, size);

        if (!currentItem.isFinished() || !previewItem) {
            return;
        }
        dispatchPreviewItem(previewItem);

        processNextProduct();
    }

    function updateImages() {
        currentItem = null;
        Services.ui.show(loaderKey);
        previewRequests.forEach(({ identifier, event }) => {
            previewStack.push(createPreviewItem(identifier, event));
        });
        processNextProduct();
    }

    //// the public interface provided to the React component

    /**
     * @param {function} [options.onRestoreImage]
     */
    function init(options = {}) {
        const { onRestoreImage } = options;

        // first rendering triggers the preview image creation
        Services.visualization.registerVisualizationUpdateListener(createPreviewImage);

        // restore the image data directly (if there is any)
        currentImageData && Services.store.dispatch(setSelectedImageData(currentImageData));
        currentImageData && onRestoreImage && setTimeout(() => onRestoreImage(currentImageData));

        dispatchLoaded();
    }

    /**
     * @param {object} [imageData]
     */
    function useImage(imageData = Services.store.state.imageGallery.selectedImage) {
        currentImageData = imageData;
        localStorage.set(imageDataKey, imageData);
        previewRequests.length && updateImages();
    }

    return {
        init,
        useImage,
    };
}
