/* global Descartes, ol */

var _ = require('lodash');
var inkmap = require('@camptocamp/inkmap');
var jspdf = require('jspdf');
var GeoStylerOpenlayersParser = require('geostyler-openlayers-parser');
require('../UI/bootstrap4/elements/custom-progress.js');
require('../UI/bootstrap4/elements/custom-button.js');

var Utils = Descartes.Utils;

var GREY_PIXEL = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH5QMUDioejXxGyAAAAB1pVFh0Q29tbWVudAAAAAAAQ3JlYXRlZCB3aXRoIEdJTVBkLmUHAAAADUlEQVQI12P48uXLfgAJWAOcu0ymVwAAAABJRU5ErkJggg==';
const INCH_PER_METER = 39.3701;

var print = {
    /**
     * Renvoie une promesse qui résout avec un objet `Canvas` qui contient l'image chargée depuis l'url
     * Note : on utilise un object canvas intermédiaire pour s'assurer du bon décodage de l'image
     */
    loadImagePromise(url) {
        return new Promise((resolve, reject) => {
            var image = new Image();
            image.onload = () => {
                var canvas = document.createElement('canvas');
                canvas.width = image.width;
                canvas.height = image.height;
                var ctx = canvas.getContext('2d');
                ctx.fillStyle = 'white';
                ctx.fillRect(0, 0, canvas.width, canvas.height);
                ctx.drawImage(image, 0, 0, image.width, image.height);
                resolve(canvas);
            };
            image.onerror = () => {
                var canvas = document.createElement('canvas');
                canvas.width = 0;
                canvas.height = 0;
                resolve(canvas);
            };

            // load image through the proxy to avoid CORS issues
            // FIXME: investigate ways to load images without going through the proxy
            image.src = Descartes.Utils.makeSameOrigin(url, Descartes.PROXY_SERVER);
        });
    },

    /**
     * Methode: writeInkmapLayers
     * Convertit les couches de la carte en couches inkmap.
     *
     * Paramètres :
     * - {Array(<Descartes.Layer>)} Liste des couches à imprimer
     * - {Object} olMap Carte OpenLayers
     * - {string} mapProjection (optional) Projection de la carte imprimée
     */
    writeInkmapLayers: async function (layers, olMap, mapProjection) {
        //layer types supported by inkmap
        const layerTypes = [
            'WMS', //<Descartes.Layer.TYPE_WMS>
            '', //<Descartes.Layer.TYPE_WMSC>
            '', //<Descartes.Layer.TYPE_TMS>
            'WMTS', //<Descartes.Layer.TYPE_WMTS>
            'WFS', //<Descartes.Layer.TYPE_WFS>
            '', //<Descartes.Layer.TYPE_KML>
            '', //<Descartes.Layer.TYPE_GEOPORTAIL>
            '', //<Descartes.Layer.TYPE_GeoJSON>
            '', //<Descartes.Layer.TYPE_GenericVector>
            'OSM', //<Descartes.Layer.TYPE_OSM>
            '', //<Descartes.Layer.EditionLayer.TYPE_WFS>
            '', //<Descartes.Layer.EditionLayer.TYPE_KML>
            '', //<Descartes.Layer.EditionLayer.TYPE_GeoJSON>
            '', //<Descartes.Layer.EditionLayer.TYPE_GenericVector>
            'Annotations' //<Descartes.Layer.EditionLayer.TYPE_Annotations>
        ];

        let layersInkmap = [];
        for (const layer of layers) {
            const type = layerTypes[layer.type];
            for (const resourceLayer of layer.resourceLayers) {
                // make all layers go through the proxy to avoid CORS issues
                // FIXME: investigate ways to load layers without going through the proxy
                let serverUrl = "";
                if (type === 'WMS' || type === 'WMTS' || type === 'WFS') {
                    serverUrl = Descartes.Utils.makeSameOrigin(resourceLayer.serverUrl, (url) => {
                        const urlObj = new URL(Descartes.PROXY_SERVER, window.location.origin);
                        // by default do not encode the target url, otherwise some urls with existing params
                        // might not work well down the line
                        urlObj.search = url;
                        if (url.indexOf('?') === -1) {
                            // make sure we have at least one query mark in the target url
                            // (encode the ? in that case, otherwise it will get removed)
                            urlObj.search = url + encodeURIComponent('?');
                        } else if (url.substr(-1, 1) === '?') {
                            // if the url ends with a query mark, simply encode it to have the
                            // same result as above
                            urlObj.search = url.substr(0, url.length - 1) + encodeURIComponent('?');
                        }
                        return urlObj.toString();
                    });
                } else if (type === 'OSM') {
                    serverUrl = layer.OL_layers[0].getSource().key_;
                }
                switch (type) {
                    case 'OSM':
                        layersInkmap.push(
                            {
                                type: 'XYZ',
                                url: serverUrl,
                                opacity: layer.opacity / 100
                            }
                        );
                        break;
                    case 'WMS':
                        layersInkmap.push(
                            {
                                type: type,
                                url: serverUrl,
                                layer: resourceLayer.layerName,
                                opacity: layer.opacity / 100,
                                tiled: true,
                                version: resourceLayer.serverVersion || '1.1.1' // default version in Descartes
                            }
                        );
                        break;
                    case 'WMTS':
                        layersInkmap.push(
                            {
                                type: type,
                                url: serverUrl,
                                layer: resourceLayer.layerName,
                                opacity: layer.opacity / 100,
                                matrixSet: layer.matrixSet,
                                projection: layer.projection,
                                format: layer.format,
                                style: resourceLayer.layerStyles || 'default',
                                tileGrid: {
                                    resolutions: layer.resolutions
                                },
                                version: resourceLayer.serverVersion
                            }
                        );
                        break;
                    case 'WFS':
                        const wfsLayerSpec = {
                            type: type,
                            url: serverUrl,
                            layer: resourceLayer.layerName,
                            version: resourceLayer.serverVersion,
                            format: 'gml'
                        };
                        const wfsGeoStylerStyle = await print.toGeoStylerLayerStyle(layer);
                        if (wfsGeoStylerStyle) {
                            wfsLayerSpec.style = wfsGeoStylerStyle;
                        }
                        layersInkmap.push(wfsLayerSpec);
                        break;
                    case 'Annotations':
                        const features = layer.OL_layers[0].getSource().getFeatures();
                        const featuresCloned = [];
                        features.forEach((feature, index) => {
                            const featureCloned = feature.clone();
                            featureCloned.set('dAnnotationId', index);
                            featuresCloned.push(featureCloned);
                        });
                        const geojson = print.toGeoJSONFeatures(featuresCloned, olMap, mapProjection);
                        const annotationsLayerSpec = {
                            type: 'GeoJSON',
                            geojson
                        };
                        const annotationsGeoStylerStyle = await print.toGeoStylerFeaturesStyle('dAnnotationsStyle', featuresCloned);
                        if (annotationsGeoStylerStyle) {
                            annotationsLayerSpec.style = annotationsGeoStylerStyle;
                        }
                        layersInkmap.push(annotationsLayerSpec);
                        break;
                    default:
                        console.log(`Descartes layer type ${layer.type} not supported by d-inkmap`);
                }
            }
        }
        return layersInkmap;
    },

    /**
     * Methode: toGeoStylerLayerStyle
     * Transforme le style de la couche en style geostyler.
     */
    toGeoStylerLayerStyle: async function (layer) {
        const feature = layer.OL_layers[0].getSource().getFeatures()[0];
        if (feature === undefined) {
            console.log('Layer has not loaded yet. Using OL default style for printing.');
            return undefined;
        }

        // an OL style can be an object of a style factory (i.e. function)
        const layerStyleFunction = layer.OL_layers[0].getStyle();
        const layerStyle = typeof layerStyleFunction === 'function' ? layerStyleFunction(feature) : layerStyleFunction;

        print.patchOlStyle(layerStyle);
        const parser = new GeoStylerOpenlayersParser.OlStyleParser(ol);
        const style = await parser.readStyle(layerStyle);
        return style;
    },

    /**
     * Methode: toGeoJSONFeatures
     * Transforme les features de la couche en GeoJSON.
     */
    toGeoJSONFeatures: function (features, olMap, mapProjection) {
        const projection = mapProjection ? mapProjection : olMap.getView().getProjection().getCode();
        const optionsFormat = {
            featureProjection: olMap.getView().getProjection().getCode(),
            dataProjection: projection
        };
        const format = new ol.format.GeoJSON();
        const geojson = format.writeFeatures(features, optionsFormat);
        return JSON.parse(geojson);
    },

    /**
     * Methode: toGeoStylerFeaturesStyle
     * Transforme le style de chaque feature en style geostyler.
     * Extrait les rules en leur ajoutant un filtre sur le dAnnotationId de la feature.
     * Concatène l'ensemble des rules dans un seul style.
     */
    toGeoStylerFeaturesStyle: async function (name, features) {
        const aggStyle = {
            name,
            rules: []
        };

        for (const feature of features) {
            // an OL style can be an object of a style factory (i.e. function)
            const featureStyleFunction = feature.getStyle();
            const featureStyle = typeof featureStyleFunction === 'function' ? featureStyleFunction(feature) : featureStyleFunction;

            print.patchOlStyle(featureStyle);
            const parser = new GeoStylerOpenlayersParser.OlStyleParser(ol);
            const style = await parser.readStyle(featureStyle);
            print.patchGeoStylerStyle(style);

            const idFilter = ['==', 'dAnnotationId', feature.get('dAnnotationId')];
            style.rules.forEach(rule => {
                rule.filter ?
                    rule.filter = ['&&', idFilter, rule.filter] :
                    rule.filter = idFilter;
            });

            aggStyle.rules = aggStyle.rules.concat(style.rules);
        };

        return aggStyle;
    },

    /**
     * Methode: patchOlStyle
     * - geostyler n'accepte actuellement pas des couleurs en array
     * - descartes crée des styles line et fill avec image que geostyler interprete comme point
     */
    patchOlStyle: function (style) {
        // handle array of styles
        if (Array.isArray(style)) {
            style.forEach(this.patchOlStyle);
        }

        //transform colors to strings => to be fixed in geostyler
        if (style.getFill() && style.getFill().getColor()) {
            style.getFill().setColor(ol.color.asString(style.getFill().getColor()));
        }
        if (style.getStroke() && style.getStroke().getColor()) {
            style.getStroke().setColor(ol.color.asString(style.getStroke().getColor()));
        }
        if (style.getImage() && style.getImage().getFill() && style.getImage().getFill().getColor()) {
            style.getImage().getFill().setColor(ol.color.asString(style.getImage().getFill().getColor()));
        }
        if (style.getImage() && style.getImage().getStroke() && style.getImage().getStroke().getColor()) {
            style.getImage().getStroke().setColor(ol.color.asString(style.getImage().getStroke().getColor()));
        }
        //make sure line and fill style have no image defined => to be fixed in descartes?
        if (style.getStroke() || style.getFill() instanceof ol.style.Fill) {
            style.setImage(undefined);
        }
        //GeoStyler doesn't support empty font, default is 10px sans-serif
        if (style.getText() && !style.getText().getFont()) {
            style.getText().setFont('20px sans-serif');
        }
    },

    /**
     * Methode: patchGeoStylerStyle
     * - geostyler et openlayers gère différemment la casse des wellKnownName selon les versions
     */
    patchGeoStylerStyle: function (style) {
        style.rules.forEach(rule => {
            rule.symbolizers.forEach(symbolizer => {
                let wkn = symbolizer.wellKnownName;
                wkn = wkn && wkn[0].toUpperCase() + wkn.slice(1);
                symbolizer.wellKnownName = wkn;
            });
        });
    },

    /**
     * Methode: writeInkmapPartialSpec
     * Ecrit la spec d'impression inkmap, sauf les couches.
     *
     * Paramètres :
     * - {Object} olMap Carte OpenLayers
     * - {[number, number]} mapCenter (optional) Centre géographique de la carte imprimée (lon, lat)
     * - {[number, number]} mapSize (optional) Taille de la carte imprimée en millimètres
     * - {number} printRatio (optional) Rapport entre la taille de l'aperçu d'impression et les dimensions réelles du papier en millimètres
     * - {boolean} printScaleBar (optional) Imprimer l'échelle sur la carte, défaut true
     * - {boolean} printNorthArrow (optional) Imprimer l'indication nord sur la carte, défaut true
     * - {number} mapDpi (optional) La résolution de la carte imprimée en dpi, défaut 300
     * - {string} mapProjection (optional) Projection de la carte imprimée
     */
    writeInkmapPartialSpec: function (olMap, mapCenter, mapSize, printRatio, printScaleBar, printNorthArrow, mapDpi, mapProjection) {
        const projection = mapProjection ? mapProjection : olMap.getView().getProjection().getCode();
        const resolution = olMap.getView().getResolution();
        const metersPerUnit = olMap.getView().getProjection().getMetersPerUnit(); //might needs adaption if different proj is selected
        //determine params from olMap if not passed to function
        const center = mapCenter ? mapCenter : new ol.proj.transform(olMap.getView().get('center'), projection, 'EPSG:4326');
        const size = mapSize ? [mapSize[0], mapSize[1], 'mm'] : olMap.get('size');
        const dpi = mapDpi ? mapDpi : 300;
        const scale = printRatio ?
            printRatio * resolution * metersPerUnit * 1000 : // paper dimension is mm, resolution is m/px
            resolution * dpi * INCH_PER_METER * metersPerUnit;
        const scaleBar = printScaleBar !== undefined ? printScaleBar : true;
        const northArrow = printNorthArrow !== undefined ? printNorthArrow : true;
        return {
            layers: [],
            size,
            center,
            dpi, // print DPI is different that screen DPI
            scale,
            projection,
            scaleBar,
            northArrow
        };
    },

    /**
     * Methode: createPdfDoc
     * Initialise un document PDF avec jsPDF
     *
     * Paramètres :
     * - {[number, number]} paperSize Taille du papier d'impression en millimètres
     */
    createPdfDoc: function (paperSize) {
        var doc = new jspdf.jsPDF({
            orientation: paperSize[0] < paperSize[1] ? 'p' : 'l',
            unit: 'mm',
            format: paperSize,
            putOnlyUsedFonts: true
        });
        doc._maxdimension = Math.max.apply(this, paperSize);
        return doc;
    },

    /**
     * Methode: setPdfFontSize
     * Utilise une taille de texte proportionnelle à la taille du papier
     *
     * Paramètres :
     * - {Object} doc Document PDF créé avec `createPdfDoc`
     * - {number} size Taille du texte en points
     */
    setPdfFontSize: function (doc, size) {
        var scaledSize = size * doc._maxdimension / 420;
        doc.setFontSize(scaledSize);
    },

    /**
     * Methode: addBlockToPdfDoc
     * Ajoute un bloc contenant du texte dans le document PDF
     *
     * Paramètres :
     * - {Object} doc Document PDF créé avec `createPdfDoc`
     * - {[number, number]} position Position du bloc (coin haut/gauche) en millimètres
     * - {[number, number]} size Taille du bloc en millimètres
     * - {string} textContent Texte à afficher dans le bloc
     * - {string} textAlign Alignement: 'left', 'center', 'right' ou 'justify'
     * - {string} fontName Fonte à utiliser (par défaut : 'courier')
     * - {string} fontStyle Style du texte (par défaut : 'normal')
     * - {number} fontSize Taille du texte (par défaut : 12)
     */
    addBlockToPdfDoc: function (doc, position, size, textContent, textAlign, fontStyle, fontSize) {
        // draw bg (grey with alpha)
        doc.addImage(
            GREY_PIXEL,
            'PNG',
            position[0],
            position[1],
            size[0],
            size[1]
        );

        var margin = 3; // mm

        this.setPdfFontSize(doc, fontSize || 12);

        var textAnchor = position.slice();

        // X anchor: computed based on alignment left/center/right
        if (textAlign === 'center') {
            textAnchor[0] += size[0] / 2;
        } else if (textAlign === 'right') {
            textAnchor[0] += size[0] - margin;
         } else {
            textAnchor[0] += margin;
        }

        // Y anchor: computed based on amount of lines (text always centered vertically)
        var splittedText = doc.splitTextToSize(textContent, size[0] - margin * 2);
        var lineHeight = doc.getTextDimensions(textContent).h;
        textAnchor[1] += Math.max(margin, size[1] / 2 - (splittedText.length / 2) * lineHeight); // avoid overflow above rect top

        doc.text(
            splittedText,
            textAnchor[0],
            textAnchor[1],
            {
                align: textAlign,
                baseline: 'hanging'
            }
        );
    },

    /**
     * Methode: addImageToPdfDoc
     * Ajoute un bloc contenant une image dans le document PDF
     *
     * Paramètres :
     * - {Object} doc Document PDF créé avec `createPdfDoc`
     * - {[number, number]} position Position du bloc (coin haut/gauche) en millimètres
     * - {[number, number]} size Taille du bloc en millimètres
     * - {string} imageUrl Url de l'image
     */
    addImageToPdfDoc: function (doc, position, size, imageUrl) {
        doc.addImage(
            imageUrl,
            'PNG',
            position[0],
            position[1],
            size[0],
            size[1]
        );
    },

    /**
     * Methode: addMapLegendToPdfDoc
     * Charge les légendes de toutes les couches passées en paramètre
     * et les intègre dans un document PDF
     * Attention : cette méthode est asynchrone !
     *
     * Paramètres :
     * - {Object} doc Document PDF créé avec `createPdfDoc`
     * - {[number, number]} position Position du bloc (coin haut/gauche) en millimètres
     * - {[number, number]} size Taille du bloc en millimètres
     * - {Descartes.Layer[]} layers Tableau des couches à inclure dans la légende finale
     */
    addMapLegendToPdfDoc: async function (doc, position, size, layers) {
        // draw bg (grey with alpha)
        doc.addImage(
            GREY_PIXEL, 'PNG',
            position[0], position[1],
            size[0], size[1]
        );

        var layersWithLegend = layers
            .filter(layer => layer.legend && layer.legend.length);
        var legendTitles = layersWithLegend.map(layer => layer.title);
        var legendUrls = layersWithLegend.map(layer => layer.legend[0]);

        // load all legends in parallel
        var legendImages = await Promise.all(legendUrls.map(this.loadImagePromise));

        var margin = 3; // mm
        var fontSize = 12; // layer title legend
        this.setPdfFontSize(doc, fontSize);
        var lineHeight = doc.getTextDimensions('ABCD').h;

        // compute the scale factor for the legend images, in mm per pixel
        // by default, use 72 dpi, reduce if not enough size
        var scaleFactor = 1000 / (72 * INCH_PER_METER);

        // total height needed for titles (an empty line is added between legends)
        var totalTitleHeight = (legendTitles.length * 2 - 1) * lineHeight;
        var availableLegendHeight = size[1] - margin * 2 - totalTitleHeight;

        // these are converted to mm
        var maxLegendWidth = legendImages.reduce((prev, img) => Math.max(img.width, prev), 0) * scaleFactor;
        var totalLegendHeight = legendImages.reduce((prev, img) => img.height + prev, 0) * scaleFactor;

        var usedWidthRatio = maxLegendWidth / (size[0] - margin * 2);
        var usedHeightRatio = totalLegendHeight / availableLegendHeight;

        // adjust scale factor based on whether width or height of legends are exceeding
        scaleFactor = scaleFactor / Math.max(1, usedHeightRatio, usedWidthRatio);

        // for each layer, add the title and the legend image next to it
        var currentY = position[1] + margin;
        var baseX = position[0] + margin;
        legendImages.forEach((image, index) => {
            doc.text(
                legendTitles[index],
                baseX,
                currentY,
                { baseline: 'hanging' }
            );
            currentY += lineHeight;

            // addImage can fail if the legend image is tainted (ie it doesn't have CORS allowing headers)
            // in this case the drawing will fail and there is nothing more we can do unfortunately
            try {
                doc.addImage(
                    image,
                    'PNG',
                    baseX,
                    currentY,
                    image.width * scaleFactor,
                    image.height * scaleFactor
                );
            } catch (error) {
                this.setPdfFontSize(doc, fontSize - 4);
                doc.setTextColor(200);
                doc.text(
                    'Le chargement de la légende a échoué.',
                    baseX,
                    currentY,
                    { baseline: 'hanging', maxWidth: size[0] - margin * 2 }
                );
                doc.setTextColor(0);
                this.setPdfFontSize(doc, fontSize);
                console.warn('Failed to draw a legend image - probably a CORS issue');
            }
            currentY += image.height * scaleFactor + lineHeight;
        });
    },

    /**
     * Methode: addFooterToPdfDoc
     * Affiche le bloc des informations propriétaires en pied de page
     * Ce bloc contient le logo, service producteur, attributions et infos additionnelles
     * Attention : cette méthode est asynchrone !
     *
     * Paramètres :
     * - {Object} doc Document PDF créé avec `createPdfDoc`
     * - {[number, number]} position Position du bloc (coin haut/gauche) en millimètres
     * - {[number, number]} size Taille du bloc en millimètres
     * - {string} logoUrl Url du logo à afficher
     * - {string} producer Producteur de la carte
     * - {string} attributions Attributions
     * - {string} additionalInfo Infos additionnelles
     */
    addFooterToPdfDoc: async function (doc, position, size, logoUrl, producer, attributions, additionalInfo) {
        // draw bg (grey with alpha)
        doc.addImage(
            GREY_PIXEL, 'PNG',
            position[0], position[1],
            size[0], size[1]
        );

        var margin = 3; // mm
        var fontSize = 12;
        this.setPdfFontSize(doc, fontSize);
        var lineHeight = doc.getTextDimensions('ABCD').h;

        var currentX = position[0] + margin;
        var currentY = position[1] + margin;

        if (logoUrl) {
            var logoImage = await this.loadImagePromise(logoUrl);
            var availableWidth = (size[0] - margin * 2) / 2; // logo can only take up left half of the block
            var availableHeight = size[1] - margin * 2;
            var usedWidthRatio = logoImage.width / availableWidth;
            var usedHeightRatio = logoImage.height / availableHeight;

            // compute the scale factor for the logo, in mm per pixel
            // in order to fit
            var scaleFactor = 1 / Math.max(usedWidthRatio, usedHeightRatio);

            // addImage can fail if the legend image is tainted (ie it doesn't have CORS allowing headers)
            // in this case the drawing will fail and there is nothing moe we can do unfortunately
            try {
                doc.addImage(
                    logoImage,
                    null,
                    currentX,
                    currentY,
                    logoImage.width * scaleFactor,
                    logoImage.height * scaleFactor
                );
                currentX += logoImage.width * scaleFactor + margin;
            } catch (error) {
                console.warn('Failed to draw the footer logo - probably a CORS issue');
            }
        }

        var remainingWidth = size[0] - (currentX - position[0] - margin);
        var splittedText;
        if (producer) {
            splittedText = doc.splitTextToSize(producer, remainingWidth);
            doc.text(
                splittedText,
                currentX,
                currentY,
                { baseline: 'hanging' }
            );
            currentY += lineHeight * (splittedText.length + 1);
        }
        if (additionalInfo) {
            splittedText = doc.splitTextToSize(additionalInfo, remainingWidth);
            doc.text(
                splittedText,
                currentX,
                currentY,
                { baseline: 'hanging' }
            );
            currentY += lineHeight * (splittedText.length + 1);
        }
        if (attributions) {
            splittedText = doc.splitTextToSize(attributions, remainingWidth);
            doc.text(
                splittedText,
                currentX,
                currentY,
                { baseline: 'hanging' }
            );
        }
    },

    exportPNG: async function (printSpec) {
        const jobId = await inkmap.queuePrint(printSpec);
        inkmap.getJobStatus(jobId).subscribe((printStatus) => {
            if (printStatus.progress === 1) {
                const filename = `inkmap-${new Date().toISOString().substr(0, 10)}.png`;
                inkmap.downloadBlob(printStatus.imageBlob, filename);
            }
        });
    },

    /**
     * Methode: exportPDF
     * Imprime un document PDF avec une carte
     *
     * Paramètres :
     * - {Object} pdfDoc Document pdf
     * - {Object} printSpec Spec d'impression inkmap
     * - {(pdfDoc, mapUrl) => void} composePdfCallback Fonction appelée lorsque l'image de la carte est disponible;
     *                                                 Doit renvoyer une promesse
     */
    exportPDF: async function (pdfDoc, printSpec, composePdfCallback) {
        const btn = /** @type {CustomButton} */ document.querySelector('custom-button');
        const bar = /** @type {CustomProgress} */ document.querySelector('custom-progress');
        btn.showSpinner();
        bar.progress = 0;
        bar.status = 'pending';
        const jobId = await inkmap.queuePrint(printSpec);
        inkmap.getJobStatus(jobId).subscribe(async(printStatus) => {
            bar.progress = printStatus.progress;
            bar.status = printStatus.status;
            let mapUrl;
            if (printStatus.progress === 1) {
                btn.hideSpinner();
                mapUrl = URL.createObjectURL(printStatus.imageBlob);
                await composePdfCallback(mapUrl);
                const filename = `inkmap-${new Date().toISOString().substr(0, 10)}.pdf`;
                pdfDoc.save(filename);
            }
        });
    }
};
_.extend(Utils, print);

module.exports = Utils;
