/*!
 * Copyright 2017 - 2020 by ChartIQ, Inc.
 * All rights reserved.
 */
import { System } from "./system";
import { Logger } from "../clients/logger";
import { Startup } from "../clients/Startup/Startup";
import { FinsembleWindow } from "./FinsembleWindow";
import pick from "lodash/pick";
import isEqual from "lodash/isEqual";
import isEqualWith from "lodash/isEqualWith";
/**
 * Compresses or expands an object in the same manner as Array.flatMap();
 *
 * Requires a Record<string,any>. Do not pass empty or primitive objects.
 *
 * Usage:
 * objectFlatMap(object, (key, value) => {
        // Remove key/value pair
        if(key === "goodbye") return [];
        // Add key/value pair
        if(key === "add") return [[key,value],[key2,value2]]
        // Retain/replace key/value pair
        return [[key,value]];
    });
 */
export const objectFlatMap = (obj, func) => Object.fromEntries(Object.entries(obj).flatMap(([k, v]) => func(k, v)));
/*
 * Convenience method for cloning an object.
 * @param from The thing you want to copy
 * @param to Where you want your copy to end up
 * @return to
 */
export function clone(from, to = {}) {
    if (from === null || typeof from !== "object") {
        return from;
    }
    if (from.constructor == Date ||
        from.constructor == RegExp ||
        from.constructor == Function ||
        from.constructor == String ||
        from.constructor == Number ||
        from.constructor == Boolean) {
        return new from.constructor(from);
    }
    to = to || new from.constructor();
    for (const n in from) {
        to[n] = typeof to[n] === "undefined" ? clone(from[n], null) : to[n];
    }
    return to;
}
/**
 * <h2>Finsemble Utility Functions</h2>
 */
export function isPercentage(val) {
    if (typeof val !== "string") {
        return false;
    }
    return val.indexOf("%") !== -1;
}
/**
 * Rounds number to n decimal places
 */
export const roundToDecimal = (num, decimal) => {
    const mod = Math.pow(10, decimal);
    return Math.round(num * mod) / mod;
};
export function crossDomain(url) {
    const parser = document.createElement("a");
    parser.href = url;
    const isSameHost = window.location.hostname === parser.hostname;
    const isSameProtocol = window.location.protocol === parser.protocol;
    const wport = window.location.port !== undefined ? window.location.port : 80;
    const pport = parser.port !== undefined ? parser.port : 80;
    const isSamePort = wport === pport;
    const isCrossDomain = !(isSameHost && isSamePort && isSameProtocol);
    Logger.system.debug(`Launcher crossDomain=${isCrossDomain} (${isSameHost}:${isSameProtocol}:${isSamePort})`);
    return isCrossDomain;
}
/**
 * Returns a finWindow or null if not found
 * @param  windowIdentifier A window identifier
 * @param  cb Optional callback containing finWindow or null if not found (or use Promise)
 * @return Promise that resolves to a finWindow or rejects if not found
 */
export function getFinWindow(windowIdentifier, cb) {
    return new Promise((resolve, reject) => {
        // Default to current window
        const myWindow = System.Window.getCurrent();
        // If windowName is provided, then find that window
        if (windowIdentifier === null || windowIdentifier === void 0 ? void 0 : windowIdentifier.windowName) {
            /**
             * Try to wrap the window; if it exists, getInfo will get in
             *  to the success function. If not, it'll go into the error callback.
             */
            try {
                const remoteWindow = System.Window.wrap(windowIdentifier.windowName);
                cb === null || cb === void 0 ? void 0 : cb(remoteWindow);
                resolve(remoteWindow);
            }
            catch (err) {
                cb === null || cb === void 0 ? void 0 : cb(null);
                reject(`Window ${windowIdentifier.windowName} not found. UUID: ${windowIdentifier.uuid}`);
                console.debug(`util.getFinWindow: Window ${windowIdentifier.windowName} not found`);
            }
        }
        else if (windowIdentifier === null || windowIdentifier === void 0 ? void 0 : windowIdentifier.componentType) {
            // The name of the launcher service
            const LauncherServiceName = "LauncherService";
            if (typeof window[LauncherServiceName] !== "undefined") {
                // Do not use global objects only available in the windowService in the future
                const remoteWindow = window[LauncherServiceName].componentFinder(windowIdentifier);
                if (remoteWindow) {
                    resolve(remoteWindow);
                    cb === null || cb === void 0 ? void 0 : cb(remoteWindow);
                }
                else {
                    reject(`util.getFinWindow: Component ${windowIdentifier.componentType} not found.`);
                    cb === null || cb === void 0 ? void 0 : cb(null);
                }
            }
            else {
                reject("getFinWindow by componentType is currently only operable within LaunchService");
                cb === null || cb === void 0 ? void 0 : cb(null);
            }
        }
        else {
            // return windowDescriptor for current window
            cb === null || cb === void 0 ? void 0 : cb(myWindow);
            resolve(myWindow);
        }
    });
}
/**
 * Retrieves a windowDescriptor given a windowIdentifier
 * @param windowIdentifier The window to locate. If empty then the current window is returned.
 * @param cb Function to retrieve result (optional or use Promise)
 * @return A promise that resolves to a WindowDescriptor
 */
export function getWindowDescriptor(windowIdentifier, cb) {
    return new Promise((resolve, reject) => {
        getFinWindow(windowIdentifier)
            .then((finWindow) => {
            finWindow.getOptions((options) => {
                cb === null || cb === void 0 ? void 0 : cb(options);
                resolve(options);
            });
        })
            .catch((errorMessage) => {
            console.warn(errorMessage);
            cb === null || cb === void 0 ? void 0 : cb(null);
            reject(errorMessage);
        });
    });
}
/**
 * Returns a windowIdentifier for the current window
 * @param cb Callback function returns windowIdentifier for this window (optional or use Promise)
 * @returns A promise that resolves to a windowIdentifier
 */
export const getMyWindowIdentifier = (cb) => {
    const finWindow = System.Window.getCurrent();
    return new Promise((resolve) => {
        finWindow.getOptions((windowDescriptor) => {
            let componentType = null;
            // Figure out the component type from what was originally stored when we launched the window
            // options.customData is where our stuff is found
            const { customData } = windowDescriptor;
            if (customData && customData.component) {
                componentType = customData.component.type;
            }
            const windowIdentifier = {
                windowName: finWindow.name,
                componentType,
            };
            cb === null || cb === void 0 ? void 0 : cb(windowIdentifier);
            resolve(windowIdentifier);
        });
    });
};
/**
 * @returns Transforms an array of strings into a camel cased string.
 * @memberof Utils
 */
export function camelCase(...args) {
    let str = "";
    for (let i = 0; i < args.length; i++) {
        str = `${str} ${args[i]}`;
    }
    return str
        .replace(/\s(.)/g, ($1) => $1.toUpperCase())
        .replace(/\s/g, "")
        .replace(/^(.)/, ($1) => $1.toLowerCase());
}
export async function isWindows(cb) {
    const hostSpecs = await System.getHostSpecs();
    const OsIsWindows = hostSpecs.name.toUpperCase().includes("WINDOWS");
    cb === null || cb === void 0 ? void 0 : cb(OsIsWindows);
    return OsIsWindows;
}
/**
 * Calculates windowType for a newly spawned window
 * windowType can be set or overwritten in many ways. The intention here is to end up with a limited set of
 * final windowTypes
 * @export
 * @param config Object containing all possible values used to set windowTypes, some of these values may be unset depending on the execution path
 */
export function getWindowType(config) {
    const DEFAULT_WINDOW_TYPE = "WebWindow";
    // All possible windowTypes. Some of these values will be converted to other types
    const validTypes = [
        "assimilation",
        "assimilated",
        "native",
        "application",
        "NativeWindow",
        "FinsembleNativeWindow",
        "CompoundWindow",
        "WebWindow",
        "WebApplication",
        "Citrix",
        "external",
    ];
    // If an invalid windowType is given, default and log an error. Note that an empty windowType
    // is not an error case. This is to let the user know that they may have made a typo setting a type in
    // the config file. We default to keep Finsemble from breaking, but the user may have intended to launch a
    // component as a different type.
    if (config.windowType && !validTypes.includes(config.windowType)) {
        Logger.system.error(`Invalid windowType: ${config.windowType}, defaulting to windowType: ${DEFAULT_WINDOW_TYPE}`);
        return DEFAULT_WINDOW_TYPE;
    }
    let ret = config.windowType || DEFAULT_WINDOW_TYPE;
    // We allow several additional windowTypes to be inputted to make the config user-friendly
    // These windowTypes need to be converted to values Finsemble can process
    switch (config.windowType) {
        case "assimilation":
        case "assimilated":
            ret = "NativeWindow";
            break;
        // backwards compatibility for removal of type application
        case "application":
            ret = "WebApplication";
            break;
        case "Web":
            ret = "WebWindow";
            break;
        // If config.windowType is unset, we'll use the DEFAULT_WINDOW_TYPE
        case "default":
            break;
        default:
            break;
    }
    if (config.compound)
        ret = "CompoundWindow";
    return ret;
}
/**
 * For the DOM element that has been passed in, this function returns a bounding box that is relative
 * to the virtual monitor space. That is, it returns the position of the DOM element on the desktop.
 * @param {HTMLElement|string} element A selector or HTMLElement
 */
export const getDesktopBoundingBox = async (element) => {
    var _a, _b;
    const el = typeof element === "string" ? document.querySelector(element) : element;
    if (!el) {
        Logger.system.error(`getDesktopBoundingBox can't find element`, element);
    }
    const box = el ? el.getBoundingClientRect() : { top: 0, left: 0, width: 0, height: 0 };
    const { windowName } = Startup.getInstance().getStartupData();
    const { wrap } = await FinsembleWindow.getInstance({ name: windowName });
    const response = await wrap.getBounds();
    const bounds = response.data;
    if (response.err) {
        Logger.system.error(`getDesktopBoundingBox: finsembleWindow.getBounds() error`, response.err);
    }
    const boundsTop = (_a = bounds === null || bounds === void 0 ? void 0 : bounds.top) !== null && _a !== void 0 ? _a : 0;
    const boundsLeft = (_b = bounds === null || bounds === void 0 ? void 0 : bounds.left) !== null && _b !== void 0 ? _b : 0;
    const boundingBox = {
        top: boundsTop + box.top,
        left: boundsLeft + box.left,
        width: box.width,
        height: box.height,
        right: 0,
        bottom: 0,
    };
    boundingBox.right = boundingBox.left + boundingBox.width;
    boundingBox.bottom = boundingBox.top + boundingBox.height;
    return boundingBox;
};
export const isPointInBox = (point, box) => {
    if (!box.bottom)
        box.bottom = box.top + box.height;
    if (!box.right)
        box.right = box.left + box.width;
    return point.x > box.left && point.x < box.right && point.y < box.bottom && point.y > box.top;
};
/**
 * Returns (via callback) true if the mouse is currently located (hovering) over the requested element.
 * @param element The element, or a selector, to check
 * @param cb A function that returns a boolean
 */
export const isMouseOverDOMElement = async (element, cb = Function.prototype) => {
    const boundingBox = await getDesktopBoundingBox(element);
    const position = await System.getMousePosition();
    const result = isPointInBox(position, boundingBox);
    cb(result);
    return result;
};
export const fastClone = (obj) => {
    // This has been tested a good amount. Previous to this commit we were using a mix of deepmerge and JSON.parse(JSON.stringify()).
    // Trying lodash/deepclone made tests take 2-3s.
    // JSON.parse everywhere made them take ~ 1s.
    // Using JSON.parse on arrays and deep merge on objects makes them take 7-900ms.
    if (!obj)
        return obj;
    if (Array.isArray(obj)) {
        return obj.slice();
    }
    try {
        return JSON.parse(JSON.stringify(obj));
    }
    catch (e) {
        Logger.error("fastClone error, falling back to structured clone", e);
    }
    try {
        // @ts-ignore structuredClone() will be added to Typescript 4.7
        return structuredClone(obj);
    }
    catch (e) {
        Logger.error("fastClone error, structured clone failed", e);
    }
};
/** Returns exactly what's passed to it. Useful for higher-order functions. */
export function identity(arg) {
    return arg;
}
/*
typed-promisify, https://github.com/notenoughneon/typed-promisify

MIT License

Copyright (c) 2016 Emma Kuo

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
 */
// DH 3/11/2019 - I've removed the type-inferring overloads, as they aren't
// working correctly.
/**
 * Wraps a callback accepting function in a promise. The callback must have the type
 * specified in StandardCallback, and the wrapped function *MUST* call the callback
 * on all possible code paths.
 */
export function promisify(f, thisContext) {
    return function () {
        const args = Array.prototype.slice.call(arguments);
        return new Promise((resolve, reject) => {
            args.push((err, result) => (err ? reject(err) : resolve(result)));
            f.apply(thisContext, args);
        });
    };
}
/**
 * Creates a promise that rejects after the specified time with
 * the given message.
 */
export function timeoutPromise(ms, message) {
    return new Promise((resolve, reject) => {
        const id = setTimeout(() => {
            clearTimeout(id);
            reject(message);
        }, ms);
    });
}
/**
 * Wraps a promise in another promise that either rejects after the specified number of milliseconds
 * or resolves with the result of the promise.
 */
export function wrapWithTimeout(promise, ms, message) {
    return Promise.race([promise, timeoutPromise(ms, message)]);
}
/**
 * Deep equal doesn't work properly if the objects aren't exactly equal
 * We have several places in the code that attach extra parameters to bounds objects
 * This function will test equality on bounds for the only left, right, top, bottom, width and height
 * @param {} bounds1
 * @param {*} bounds2
 */
export function checkIfBoundsAreEqual(bounds1, bounds2) {
    if (!bounds1 || !bounds2)
        return false;
    const keepKeys = ["left", "right", "top", "bottom", "width", "height"];
    const updatedBounds1 = pick(bounds1, keepKeys);
    const updatedBounds2 = pick(bounds2, keepKeys);
    return isEqual(updatedBounds1, updatedBounds2);
}
/**
 * Uses set timeout to provide a mechanism like sleep. e.g. await sleep(1000). Can be used in conjunction with Promise.race to implement timeouts.
 *
 * @export
 * @param {Number} ms number of milliseconds to wait for
 * @param {Boolean} rejectPromise if true, will reject instead of resolving the promise
 * @returns
 */
export function sleep(ms, rejectPromise = false) {
    const waitFor = (resolve, reject) => {
        rejectPromise
            ? setTimeout(() => {
                try {
                    reject("timeout expired");
                }
                catch (_a) { }
            }, ms)
            : setTimeout(resolve, ms);
    };
    return new Promise(waitFor);
}
/**
 * Like isEqual but Array order is ignored
 * @param value1
 * @param value2
 */
export const isEqualIgnoringArrayOrder = (value1, value2) => {
    const clonedValue1 = fastClone(value1);
    const clonedValue2 = fastClone(value2);
    return isEqualWith(clonedValue1, clonedValue2, (firstValue, secondValue) => {
        if (Array.isArray(firstValue) && Array.isArray(secondValue)) {
            return isEqualWith(firstValue.sort(), secondValue.sort());
        }
    });
};
/**
 * Checks whether an object is iterable
 * @param obj the object to check
 */
export const isIterable = (obj) => {
    // checks for null and undefined
    if (obj === null || obj === undefined) {
        return false;
    }
    // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols
    return typeof obj[Symbol.iterator] === "function";
};
//# sourceMappingURL=util.js.map