Source: utils/template/functions/common.js

import { Converter } from 'showdown';
import { isPlainObject } from 'lodash';
import proj4 from "proj4";

import { formatLatLng as formatLatLng_ } from "../../../map/utils/formatLatLng.js";
import { addButtonToTableRow, initAnnotations } from "../../../ui/table/Utils.js";
import { EventManager } from "../../../events/EventManager.js";
import { isNullorUndefined, roundNumber } from "../../utils.js";
import { applyUnitConversion } from "../convertUnits.js";

import "./common.css";

/**
 * Format lat/lng to a readable string with decimal degrees
 * 
 * 
 * 
 * @param {*} lat 
 * @param {*} lng 
 * @returns {HTMLElement} formatted lat/lng in a span
 */
export function formatLatLng(lat, lng) {
    if (isNullorUndefined(lat) || isNullorUndefined(lng)) return undefined;

    const span = document.createElement("span");
    span.innerText = formatLatLng_(lat, lng);

    span.addEventListener("append", e => {
        if ((span.parentNode.nodeName == "TD") && (span.parentNode.parentNode.nodeName == "TR")) {
            let toggle = false;
            addButtonToTableRow(span.parentNode.parentNode, ["fas", "fa-retweet"], "Change Format", null, (keyCell, valueCell) => {
                span.innerText = formatLatLng_(lat, lng, (!toggle) ? "degreeminutes" : null);
                valueCell.title = span.innerText;
                toggle = !toggle;
            });
        }
    })

    return span;
}

/**
 * Format lat/lng from the geometry object of a Feature
 * to a readable string with decimal degrees
 * 
 * 
 * 
 * @param {*} geometry 
 * @param {*} name 
 * @returns {HTMLElement} formatted lat/lng in a span
 */
export function formatGeometry(geometry, name) {
    if ((geometry?.type?.toLowerCase() != "point") ||
        (typeof name != "string")
    ) return undefined;

    let crs = null; // don't assume 4326, if an unsupported Projection is used
    ["4326", "3995", "3031", "3857"].forEach(epsg => {
        if (name.includes(epsg)) {
            crs = `EPSG:${epsg}`;
        }
    });

    if (!crs) return undefined;

    const point = proj4(crs, "EPSG:4326", { x: geometry.coordinates[0], y: geometry.coordinates[1] });
    return formatLatLng(point.y, point.x);
}

/**
 * Add a button to request filtering an attribute
 * 
 * 
 * 
 * @param {string} column 
 * @param {string} value 
 * @returns {string} span placeholder
 */
export function addFilterButton(column, value) {
    if (isNullorUndefined(column) || isNullorUndefined(value)) return undefined;

    const span = document.createElement("span");
    span.addEventListener("append", e => {
        if ((span.parentNode.nodeName == "TD") && (span.parentNode.parentNode.nodeName == "TR")) {
            addButtonToTableRow(span.parentNode.parentNode, ["fas", "fa-filter"], "Add Filter", "", () => {
                EventManager.fire("add_attribute_filter", {
                    column: column,
                    value: value
                });
            });
        }
    });

    return span;
}

/**
 * Make ISO Timestring more readable
 * 
 * 
 * 
 * @param {string} date in ISO format
 * @returns {HTMLElement} formatted date in a span
 */
export function formatDate(date) {
    if (typeof date != "string") return undefined;

    function _formatDate(value) {
        value = value.replace("T", ", ");
        return value.replace("Z", " UTC");
    }

    const span = document.createElement("span");
    span.innerText = _formatDate(date);

    const modes = [
        //() => new Date(date).toLocaleString(), // unused, because the localeString is misleading and converrted to local timezone
        () => date,
        () => _formatDate(date),
    ];

    span.addEventListener("append", e => {
        if ((span.parentNode.nodeName == "TD") && (span.parentNode.parentNode.nodeName == "TR")) {
            let mode = 0;
            addButtonToTableRow(span.parentNode.parentNode, ["fas", "fa-retweet"], "Change Format", null, (keyCell, valueCell) => {
                span.innerText = modes[mode]();
                valueCell.title = span.innerText;
                mode = (mode + 1) % modes.length;
            });
        }
    })

    return span;
}

/**
 * Only returns the given string if the first value is not null or undefined
 * 
 * 
 * 
 * @param {any} valueToCheck 
 * @param {any} then 
 * @param {any} else_ 
 * @returns {any} given value
 */
export function isDefined(valueToCheck, then, else_) {
    if (!isNullorUndefined(valueToCheck)) {
        return then;
    } else {
        return else_ ?? undefined;
    }
}

/**
 * Concats values if every value is defined (not null or undefined)
 * 
 * 
 * 
 * @param {string} values any amount of values: concat1, concat2, ...
 * @returns {string} string
 */
export function concatIfDefined(values) {
    let out = "";
    for (let i = 0; i < arguments.length; ++i) {
        if (!isNullorUndefined(arguments[i])) {
            out += arguments[i];
        } else {
            return undefined;
        }
    }
    return out;
}

/**
 * Encode String to URL Compatible characters
 * 
 * 
 * 
 * @param {string} str url compponent
 * @returns {string} encoded string
 */
export function encodeURLComponent(str) {
    if (typeof str != "string") return undefined;
    return encodeURIComponent(str);
}

/**
 * Returns the GRAY_INDEX for bathymetry raster data
 * 
 * 
 * 
 * @param {object} currentProperties 
 * @param {object[]} allFeatures 
 * @returns {string} string
 */
export function getBathymetryGrayIndex(currentProperties, allFeatures) {
    if (!isPlainObject(currentProperties) || !Array.isArray(allFeatures)) return undefined;
    if (currentProperties?.GRAY_INDEX) throw "discard_template";

    for (let i = 0; i < allFeatures.length; i++) {
        const properties = allFeatures[i].properties;
        if (('GRAY_INDEX' in properties) && (properties.GRAY_INDEX < 0)) return roundNumber(properties.GRAY_INDEX, 2) + " m";
    }
    return undefined;
}

/**
 * Round a number to the given decimals
 * 
 * 
 * 
 * @param {string} num 
 * @param {string} decimals 
 * @returns {string} formatted number
 */
export function round(num, decimals) {
    if (isNullorUndefined(num)) return undefined;
    if (typeof num === "string") num = Number.parseFloat(num);
    if (!Number.isFinite(num)) return undefined;

    decimals = Number.parseInt(decimals || 0);
    if (!Number.isFinite(decimals)) decimals = 0;
    return roundNumber(num, decimals);
}

/**
 * Split a string at a substring and return the given index
 * 
 * 
 * 
 * @param {string} str 
 * @param {string} subStr 
 * @param {number} index 
 * @returns {string} formatted string
 */
export function splitString(str, subStr, index) {
    if (!str || !subStr) return undefined;

    index = Number.parseInt(args[2]) || 0;
    const parts = str.split(subStr);

    return (index < parts.length) ? parts[index] : parts[0];
}

/**
 * Split a string at an index and get the substring
 * 
 * 
 * 
 * @param {string} str 
 * @param {number} startIndex 
 * @param {number} endIndex 
 * @returns {string} string
 */
export function substring(str, startIndex, endIndex) {
    if (!str) return undefined;

    startIndex = Number.parseInt(startIndex) || 0;
    endIndex = Number.parseInt(endIndex) || 0;

    return str.substring(startIndex, endIndex);;
}

/**
 * Defines the title in data-title for the whole popup feature.
 * The PopupRenderer will extract the title from that property to display
 * it in the header. This is directly set in the container.
 * 
 * 
 * 
 * @param {string} values multiple arguments to concat if needed
 * @returns {function} callback function to set the containers title
 */
export function setTitle(values) {
    const args = Array.from(arguments);
    if (args.length > 0) {
        return container => {
            container.dataset.title = args.join("");
        };
    }
    return undefined;
}

/**
 * Parses a json string and outputs the value of the given property
 * 
 * 
 * @param {sring} jsonstring 
 * @param {sring} keys any amount of keys
 * @returns {string} resolved value
 */
export function parseJSON(jsonstring, keys) {
    const args = Array.from(arguments);
    if (args.length > 1) {
        try {
            let obj = JSON.parse(args[0]);
            for (let i = 1; i < args.length; ++i) {
                if (args[i] in obj) {
                    obj = obj[args[i]];
                } else {
                    return undefined;
                }
            }
            return obj;
        } catch (e) {
            return undefined;
        }
    }
    return undefined;
}

/**
 * Prevents the copy button to be displayed in a table row
 * 
 * @returns {HTMLElement} temporary <span>
 * 
 */
export function noCopy() {
    const span = document.createElement("span");
    span.addEventListener("append", e => {
        if ((span.parentNode.tagName == "TD") && (span.parentNode.parentNode.tagName == "TR")) {
            span.parentNode.parentNode.classList.add("no-copy");
        }

        span.remove();
    })

    return span;
}

/**
 * Prevents content from being rendered by inserting a comment if the site matches
 * 
 * @returns {string} html comment open
 * 
 */
export function hideOnGalleryStart() {
    // FixMe: HideOnSite does not work anymore. Workaround using global variable
    return (window.vefGalleryVisible) ? "<!--" : "";
}

/**
 * Prevents content from being rendered by inserting a comment if the site matches
 * 
 * @returns {string} html comment close
 * 
 */
export function hideOnGalleryEnd() {
    // FixMe: HideOnSite does not work anymore. Workaround using global variable
    return (window.vefGalleryVisible) ? "-->" : "";
}


/**
 * Display a layers legend graphic
 * 
 * @param {Layer} layer
 * @returns {string} Legend Graphic
 * 
 */
export function displayLayerLegend(layer) {
    return layer.getLegendGraphic();
}

/**
 * Join Array values as a string
 * 
 * 
 * 
 * @param {string[]} inputArray 
 * @param {string} separator 
 * @returns {string} 
 */
export function joinArray(inputArray, separator) {
    if (inputArray === "string") {
        try {
            inputArray = JSON.parse(inputArray)
        }
        catch (error) {
            inputArray = JSON.parse(inputArray.replaceAll("'", '"'))
        }
    }

    if (Array.isArray(inputArray)) {
        return inputArray.join(separator || ", ");
    }
    return inputArray || undefined;
}

/**
 * Parse markdown
 * 
 * 
 * 
 * @param {string} markdown 
 * @param {number} maxHeight  
 * @param {boolean} simplifiedAutoLink default is false
 * @returns {HTMLElement} div containing parsed markdown
 */
export function parseMarkdown(markdown, maxHeight, simplifiedAutoLink = false) {
    if (typeof markdown != "string") return undefined;

    simplifiedAutoLink = ((simplifiedAutoLink === true) || (simplifiedAutoLink === "true"));

    maxHeight = Number.parseFloat(maxHeight) || 0;
    const div = document.createElement("div");

    const setStyle = target => {
        target.style.maxHeight = maxHeight + "px";
        target.insertAdjacentHTML("beforeend", "<a class='markdown-readmore' href='#'><i class='fas fa-eye'></i> read more</a>");
        const readMore = target.querySelector("a.markdown-readmore");
        readMore.onclick = e => {
            e.preventDefault();
            readMore.remove();
            target.style.maxHeight = null;
        };
    }

    div.classList.add('invisible');
    if (maxHeight) {
        div.addEventListener("append", e => {
            if (Number.isFinite(maxHeight)) {
                //setTimeout(()=>{},0) is executed at the end of the event loop to ensure that the div is rendered in the DOM.
                //Required for getting the client height.
                setTimeout(() => {
                    if (div.clientHeight > maxHeight) {
                        setStyle(div);
                        div.classList.remove('invisible');
                    } else {
                        let duration = 0;
                        const intervalSize = 25;
                        const maxDuration = 60 * 1000;
                        const interval = setInterval(() => {
                            duration += intervalSize;
                            if (!div.clientHeight && (duration < maxDuration)) return;
                            if (div.clientHeight > maxHeight) {
                                setStyle(div);
                            }
                            div.classList.remove('invisible');
                            clearInterval(interval);
                        }, intervalSize);
                    }
                }, 0)
            }
        });
    } else {
        div.classList.remove('invisible');
    }

    const markdownConverter = new Converter({
        tables: true,
        noHeaderId: true,
        literalMidWordUnderscores: true,
        simplifiedAutoLink: simplifiedAutoLink,
        excludeTrailingPunctuationFromURLs: simplifiedAutoLink
    });

    div.classList.add("markdown-template");
    div.innerHTML = markdownConverter.makeHtml(markdown);

    return div;
}

/**
 * Simple unit formatter. Find the best unit prefix.
 * 
 * 
 * @param {number} value 
 * @param {string} unit 
 * @param {string} targetUnit 
 * @param {number} precision 
 * @returns {HTMLElement} span contianing result
 */
export function formatUnit(value, unit, targetUnit, precision) {
    if (!value) return
    if (!targetUnit) targetUnit = unit
    if (!precision) precision = 3

    const span = document.createElement('span');
    span.innerText = applyUnitConversion(value, unit, targetUnit, precision)

    return span;
}

/**
 * Creates a dropdown filter element with given values.
 *
 * @param {string} values  A string of values separated by the specified separator.
 * @param {string} separator  The character used to separate the values in the string.
 * @param {number} long  The longitude value to be used in the event.
 * @param {number} lat  The latitude value to be used in the event.
 * @returns {HTMLSelectElement} The created dropdown element.
 */
export function createFilterDropdown(values, separator, long, lat) {
    values = values.split(separator)
    const dropdown = document.createElement("select");
    dropdown.className = "pop-up_select_dropdown";

    let item = document.createElement("option");
    item.innerText = "- - -";
    item.setAttribute("selected", "");
    item.disabled = true;

    dropdown.appendChild(item);

    values.forEach(element => {
        let item = document.createElement("option");
        item.value = element;
        item.innerText = element;
        dropdown.appendChild(item);
    });

    async function caller() {
        EventManager.fire("values_Magnitute_filter", {
            magnitude: values,
            longitude: long,
            latitude: lat,
            dropdown: dropdown
        });
    }

    caller();
    return dropdown
}

/**
 * Creates a table element from the given properties object.
 * 
 * 
 * @param {object} properties 
 * @param {number} count default = 0 to print all properties
 * @param {string} excludedKeys comma seperated list of excluded keys
 * @param {string} annotations default = "false"
 * 
 * @returns {HTMLElement} The created table element.
 */
export function generateTable(properties, count, annotations = "false", excludedKeys = "") {
    count = parseInt(count || 0);
    excludedKeys = excludedKeys.split(",").map(key => key.trim());
    annotations = (annotations === "true");

    const table = document.createElement("table");
    const thead = document.createElement("thead");
    const tbody = document.createElement("tbody");
    table.appendChild(thead);
    table.appendChild(tbody);
    let index = 0;
    if (properties) {
        for (const [key, value] of Object.entries(properties)) {
            if (excludedKeys.includes(key)) continue;
            if ((properties[key] === null) || properties[key] === undefined) continue;
            if (key.startsWith("_")) continue;
            if (count && index++ >= count) break;
            const row = document.createElement("tr");
            const keyCell = document.createElement("td");
            const valueCell = document.createElement("td");

            keyCell.innerText = key;
            valueCell.innerText = value;

            row.appendChild(keyCell);
            row.appendChild(valueCell);

            tbody.appendChild(row);
        }
    }

    if (annotations) initAnnotations(table, properties, excludedKeys);

    return table;
}

/**
 * Converts a given string to uppercase.
 * 
 * @param {string} value - The string to be converted to uppercase.
 * @returns {string} The uppercase version of the input string.
 */
export function toUpperCase(value) {
    return value.toUpperCase();
}

/**
 * Converts the given string to lowercase.
 * 
 * @param {string} value - The string to be converted to lowercase.
 * @returns {string} The lowercase version of the input string.
 */
export function toLowerCase(value) {
    return value.toLowerCase();
}