Source: media/data/utils.js

import { mergeWith } from "lodash";
import { LodashCustomizer } from "../../plot/utils.js";
import { MediaDataService } from "../grid/MediaDataService.js";
export { ServiceDataRequest, GalleryMappingScheme, Duplicate, DataRequestProcessing }

/**
 * Request related helper methods for the gallery. 
 * @author ckraemme <ckraemme@awi.de>
 * @author rhess <robin.hess@awi.de> 
 * @private
 */
class ServiceDataRequest {

    /**
     * Request multiple JSON objects via url list.
     * @param {Object.Array} urlList - List of URLs.
     * @return {Promise} Promises to the JSON requests.
    */
    static requestMultipleJSONs(urlList) {
        let promises = []
        urlList.forEach((url) => {
            promises.push(ServiceDataRequest.requestJSON(url));
        });
        return Promise.all(promises)
    }

    /**
     * Create a request to a JSON file and return its data as an object via promise.
     * @param {string} url - URL to the JSON file.
     * @return {Promise} Promise containing json data when resolved.
    */
    static requestJSON(url) {
        return new Promise((resolve, reject) => {
            fetch(url)
                .then(response => {
                    const contentType = response.headers.get("content-type");
                    if (contentType.includes("application/json") && response.ok)
                        return response.json().then(json => { resolve(json), (error) => reject(error) })
                    else {
                        const errorMessage = `Error in JSON response:
                        HTTP status ${response.status}
                        Request: ${url}
                        Content Type: ${contentType}
                        Received Data: `.replace(/[ ]+/g, ' ')
                        return response.text().then(text => { reject(Error(errorMessage + text)) })
                    }
                }).catch((error) => reject(error))
        })
    }

    static requestXML(url) {
        return new Promise((resolve, reject) => {
            fetch(url)
                .then(response => {
                    const contentType = response.headers.get("content-type");
                    if (["application/xml", "text/xml"].some(mimeType => contentType.includes(mimeType)) && response.ok)
                        return response.text().then(text => {
                            return new window.DOMParser().parseFromString(text, "text/xml")
                        }).then(xml => {
                            resolve(xml), (error) => reject(error)
                        })
                    else {
                        const errorMessage = `Error in XML response:
                        HTTP status ${response.status}
                        Request: ${url}
                        Content Type: ${contentType}
                        Received Data: `.replace(/[ ]+/g, ' ')
                        return response.text().then(text => { reject(Error(errorMessage + text)) })
                    }
                }).catch((error) => reject(error))
        })
    }
}


/**
 * Gallery mapping scheme related class.
 * @author ckraemme <ckraemme@awi.de> 
 * @private
 */
class GalleryMappingScheme {

    static findPropertyKeyInMapping(mappingScheme, dataKeyList) {
        const findPropertyKey = (obj, value) =>
            Object.keys(obj).reduce((acc, curr) => {
                if (acc)
                    return acc
                if (typeof obj[curr] !== 'object') {
                    if (obj[curr] == value)
                        return curr
                }
                else
                    return findPropertyKey(obj[curr], value)
            }, null);

        const dataKeyListMapped = dataKeyList.reduce((acc, curr) => {
            const dataKeyMapped = findPropertyKey(mappingScheme, curr);
            return dataKeyMapped ? [...acc, dataKeyMapped] : [...acc]
        }, []);
        return dataKeyListMapped
    }

    static getMappingScheme(galleryConfigDictionary) {
        return galleryConfigDictionary['mapping'];
    }

    static apply(data, galleryConfigDictionary) {

        const _filterDict = (dict, childKey) => {
            return Object.keys(dict).reduce((acc, curr, idx) => {
                const collect = {};
                collect[curr] = dict[curr][childKey];
                return Object.assign({}, collect, acc)
            }, {})
        }

        const _customizer = (objValue, srcValue, key) => {
            return LodashCustomizer.overwriteValues(objValue, srcValue, key, patternData)
        }

        const finalScheme = GalleryMappingScheme.getMappingScheme(galleryConfigDictionary);
        const collect = [];
        const overwriteData = galleryConfigDictionary['overwrite'] || {};

        const constantData = _filterDict(overwriteData, "replacement");
        const patternData = _filterDict(overwriteData, "pattern");

        data.forEach(data => {
            let mappedData = {}
            if (data)
                mappedData = GalleryMappingScheme._execute(data, finalScheme);
            collect.push(mergeWith({}, mappedData, constantData, _customizer))
        })
        return collect
    }

    static _execute(data, mappingScheme) {
        let dataCopy = structuredClone(data);

        const recursiveMapping = (obj, obj_scheme) => {
            let stack = {};
            if (typeof obj_scheme !== 'object')
                console.error('Wrong input data:', obj_scheme, obj);

            for (let key in obj_scheme) {
                if (typeof obj_scheme[key] !== 'object' && key in obj) {
                    //required for: {"viewer.media.dash": "videoSrc.1", "viewer.media.video": "videoSrc[0]"}
                    const keyIsList = obj_scheme[key].match(/(.+)(?:\[|\.)(\d+)\]?$/)
                    if (keyIsList) {
                        const keyName = keyIsList[1]
                        const index = keyIsList[2];
                        let data = stack[keyName] || [];
                        data.splice(index, 0, obj[key]);
                        stack[keyName] = data;
                    }
                    else
                        stack[obj_scheme[key]] = obj[key]
                }
                else if (key in obj && obj[key])
                    stack = Object.assign({}, stack, recursiveMapping(obj[key], obj_scheme[key]));
            }
            return stack
        }

        return recursiveMapping(dataCopy, mappingScheme);
    }
}


/**
 * Class that provides static methods for duplicates.
 * @author ckraemme <ckraemme@awi.de>
 */
class Duplicate {
    /**
     * Remove all duplicates of dictionaries in a list of flat dictionaries based on keyMatch, but preserve specific data by merging the values of keyGroup first.
     * @param {Array} dataList - List of flat dictionaries.
     * @param {String} keyMatch - Key name where algorithm should be applied if found with the same value in an other dictionary.
     * @returns Array of filtered dictionaries.
     */
    static findAllKeyValuePairs(dataList, keyMatch = 'pid') {
        /**
         * Find a key value pair in a list of flat dictionaries and return all indices of dictionaries having this combination.
         * @param {Array} data 
         * @param {String} key 
         * @param {String} value 
         * @returns Array of indices.
         */
        function findKeyValuePair(data, key, value) {
            const matchedIndices = data.reduce((acc, curr, index) => {
                return curr[key] == value ? [...acc, index] : acc
            }, [])
            return matchedIndices
        }

        const collect = [];
        dataList.forEach((item) => {
            if (!item) {
                collect.push(undefined)
                return
            }
            const matchedIndices = dataList.map((mitem, mindex) => {
                if (mitem && (mitem[keyMatch] == item[keyMatch]))
                    return mindex
            }).filter(fitem => fitem != null ? true : false)
            item['duplicateIndices'] = matchedIndices;
            collect.push(item);
        })
        return collect
    }
}

class DataRequestProcessing {
    static resizeArray(arr, newSize, defaultValue) {
        while (newSize > arr.length)
            arr.push(defaultValue);
        return arr
    }

    static mergeData(dataItems, newDataItems, startIndex, count) {
        DataRequestProcessing.resizeArray(dataItems, startIndex + count, undefined)
        dataItems.splice(startIndex, count, ...newDataItems);
        return dataItems
    }

    static harmonizeData(dataItems, galleryConfigDict) {
        const mappedDataItems = GalleryMappingScheme.apply(dataItems, galleryConfigDict);
        const unifiedDataItems = MediaDataService.unifyData(mappedDataItems);
        return unifiedDataItems
    }

    static includeSidebarData(dataItems, dataItemsHarmonized) {
        const collect = structuredClone(dataItemsHarmonized);
        dataItems.forEach((item, idx) => {
            collect[idx]['sidebarData'] = item;
        })
        return collect
    }
}