/*
 * This file is part of BlueMap, licensed under the MIT License (MIT).
 *
 * Copyright (c) Blue (Lukas Rieger) <https://bluecolored.de>
 * Copyright (c) contributors
 *
 * 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.
 */
import {MathUtils, Vector2, Vector3} from "three";

export const VEC2_ZERO = new Vector2(0, 0);
export const VEC3_ZERO = new Vector3(0, 0, 0);
export const VEC3_X = new Vector3(1, 0, 0);
export const VEC3_Y = new Vector3(0, 1, 0);
export const VEC3_Z = new Vector3(0, 0, 1);

/**
 * Converts a url-encoded image string to an actual image-element
 * @param string {string}
 * @returns {HTMLElement}
 */
export const stringToImage = string => {
    let image = document.createElementNS('http://www.w3.org/1999/xhtml', 'img');
    image.src = string;
    return image;
};

/**
 * Creates an optimized path from x,z coordinates used by bluemap to save tiles
 * @param x {number}
 * @param z {number}
 * @returns {string}
 */
export const pathFromCoords = (x, z) => {
    let path = 'x';
    path += splitNumberToPath(x);

    path += 'z';
    path += splitNumberToPath(z);

    path = path.substring(0, path.length - 1);

    return path;
};

/**
 * Splits a number into an optimized folder-path used to save bluemap-tiles
 * @param num {number}
 * @returns {string}
 */
const splitNumberToPath = num => {
    let path = '';

    if (num < 0) {
        num = -num;
        path += '-';
    }

    let s = num.toString();

    for (let i = 0; i < s.length; i++) {
        path += s.charAt(i) + '/';
    }

    return path;
};

/**
 * Hashes tile-coordinates to be saved in a map
 * @param x {number}
 * @param z {number}
 * @returns {string}
 */
export const hashTile = (x, z) => `x${x}z${z}`;

export const generateCacheHash = () => {
    return Math.round(Math.random() * 1000000);
}

/**
 * Dispatches an event to the element of this map-viewer
 * @param element {EventTarget} the element on that the event is dispatched
 * @param event {string}
 * @param detail {object}
 * @returns {undefined|void|boolean}
 */
export const dispatchEvent = (element, event, detail = {}) => {
    if (!element || !element.dispatchEvent) return;

    return element.dispatchEvent(new CustomEvent(event, {
        detail: detail
    }));
}

/**
 * Sends a "bluemapAlert" event with a message and a level.
 * The level can be anything, but the app uses the levels
 * - debug
 * - fine
 * - info
 * - warning
 * - error
 * @param element {EventTarget} the element on that the event is dispatched
 * @param message {object}
 * @param level {string}
 */
export const alert = (element, message, level = "info") => {

    // alert event
    let printToConsole = dispatchEvent(element, "bluemapAlert", {
        message: message,
        level: level
    });

    // log alert to console
    if (printToConsole) {
        if (level === "info") {
            console.log(`[BlueMap/${level}]`, message);
        } else if (level === "warning") {
            console.warn(`[BlueMap/${level}]`, message);
        } else if (level === "error") {
            console.error(`[BlueMap/${level}]`, message);
        } else {
            console.debug(`[BlueMap/${level}]`, message);
        }
    }
}

/**
 * Source: https://stackoverflow.com/questions/494143/creating-a-new-dom-element-from-an-html-string-using-built-in-dom-methods-or-pro/35385518#35385518
 *
 * @param html {string} representing a single element
 * @return {Element}
 */
export const htmlToElement = html => {
    let template = document.createElement('template');
    template.innerHTML = html.trim();
    return template.content.firstChild;
}

/**
 * Source: https://stackoverflow.com/questions/494143/creating-a-new-dom-element-from-an-html-string-using-built-in-dom-methods-or-pro/35385518#35385518
 *
 * @param html {string} representing any number of sibling elements
 * @return {NodeList}
 */
export const htmlToElements = html => {
    let template = document.createElement('template');
    template.innerHTML = html;
    return template.content.childNodes;
}

/**
 * Schedules an animation
 * @param durationMs {number} the duration of the animation in ms
 * @param animationFrame {function(progress: number, deltaTime: number)} a function that is getting called each frame with the parameters (progress (0-1), deltaTime)
 * @param postAnimation {function(finished: boolean)} a function that gets called once after the animation is finished or cancelled. The function accepts one bool-parameter whether the animation was finished (true) or canceled (false)
 * @returns {{cancel: function()}} the animation object
 */
export const animate = function (animationFrame, durationMs = 1000, postAnimation = null) {
    let animation = {
        animationStart: -1,
        lastFrame: -1,
        cancelled: false,

        frame: function (time) {
            if (this.cancelled) return;

            if (this.animationStart === -1) {
                this.animationStart = time;
                this.lastFrame = time;
            }

            let progress = durationMs === 0 ? 1 : MathUtils.clamp((time - this.animationStart) / durationMs, 0, 1);
            let deltaTime = time - this.lastFrame;

            animationFrame(progress, deltaTime);

            if (progress < 1) window.requestAnimationFrame(time => this.frame(time));
            else if (postAnimation) postAnimation(true);

            this.lastFrame = time;
        },

        cancel: function () {
            this.cancelled = true;
            if (postAnimation) postAnimation(false);
        }
    };

    window.requestAnimationFrame(time => animation.frame(time));

    return animation;
}

/**
 * Source: https://gist.github.com/gre/1650294
 * @type {{
 *      easeOutCubic: (function(number): number),
 *      linear: (function(number): number),
 *      easeOutQuint: (function(number): number),
 *      easeInQuart: (function(number): number),
 *      easeInOutQuint: (function(number): number),
 *      easeInQuad: (function(number): number),
 *      easeOutQuart: (function(number): number),
 *      easeInCubic: (function(number): number),
 *      easeInQuint: (function(number): number),
 *      easeOutQuad: (function(number): number),
 *      easeInOutQuad: (function(number): number),
 *      easeInOutCubic: (function(number): number),
 *      easeInOutQuart: (function(number): number)
 *      }}
 */
export const EasingFunctions = {
    // no easing, no acceleration
    linear: t => t,
    // accelerating from zero velocity
    easeInQuad: t => t*t,
    // decelerating to zero velocity
    easeOutQuad: t => t*(2-t),
    // acceleration until halfway, then deceleration
    easeInOutQuad: t => t<.5 ? 2*t*t : -1+(4-2*t)*t,
    // accelerating from zero velocity
    easeInCubic: t => t*t*t,
    // decelerating to zero velocity
    easeOutCubic: t => (--t)*t*t+1,
    // acceleration until halfway, then deceleration
    easeInOutCubic: t => t<.5 ? 4*t*t*t : (t-1)*(2*t-2)*(2*t-2)+1,
    // accelerating from zero velocity
    easeInQuart: t => t*t*t*t,
    // decelerating to zero velocity
    easeOutQuart: t => 1-(--t)*t*t*t,
    // acceleration until halfway, then deceleration
    easeInOutQuart: t => t<.5 ? 8*t*t*t*t : 1-8*(--t)*t*t*t,
    // accelerating from zero velocity
    easeInQuint: t => t*t*t*t*t,
    // decelerating to zero velocity
    easeOutQuint: t => 1+(--t)*t*t*t*t,
    // acceleration until halfway, then deceleration
    easeInOutQuint: t => t<.5 ? 16*t*t*t*t*t : 1+16*(--t)*t*t*t*t
}

/**
 * Returns the offset position of an element
 *
 * Source: https://plainjs.com/javascript/styles/get-the-position-of-an-element-relative-to-the-document-24/
 *
 * @param element {Element}
 * @returns {{top: number, left: number}}
 */
export const elementOffset = element => {
    let rect = element.getBoundingClientRect(),
        scrollLeft = window.pageXOffset || document.documentElement.scrollLeft,
        scrollTop = window.pageYOffset || document.documentElement.scrollTop;
    return { top: rect.top + scrollTop, left: rect.left + scrollLeft }
}

/**
 * Very simple deep equals, should not be used for complex objects. Is designed for comparing parsed json-objects.
 * @param object1 {object}
 * @param object2 {object}
 * @returns {boolean}
 */
export const deepEquals = (object1, object2) => {
    if (Object.is(object1, object2)) return true;

    let type = typeof object1;
    if (type !== typeof object2) return false;

    if (type === 'number' || type === 'boolean' || type === 'string') return false;

    if (Array.isArray(object1)){
        let len = object1.length;
        if (len !== object2.length) return false;
        for (let i = 0; i < len; i++) {
            if (!deepEquals(object1[i], object2[i])) return false;
        }

        return true;
    }

    for (let property in object1) {
        if (!object1.hasOwnProperty(property)) continue;
        if (!deepEquals(object1[property], object2[property])) return false;
    }

    return true;
}

/**
 * Adds one listener to multiple events
 * @param target {EventTarget}
 * @param types {string|string[]}
 * @param listener {EventListenerOrEventListenerObject | null}
 * @param options {boolean | AddEventListenerOptions?}
 */
export const addEventListeners = (target, types, listener, options) => {
    if (!Array.isArray(types)){
        types = types.trim().split(" ");
    }

    types.forEach(type => target.addEventListener(type, listener, options));
}

/**
 * Removes one listener to multiple events
 * @param target {EventTarget}
 * @param types {string|string[]}
 * @param listener {EventListenerOrEventListenerObject | null}
 * @param options {boolean | EventListenerOptions?}
 */
export const removeEventListeners = (target, types, listener, options) => {
    if (!Array.isArray(types)){
        types = types.trim().split(" ");
    }

    types.forEach(type => target.removeEventListener(type, listener, options));
}

/**
 * Softly clamps towards a minimum value
 * @param value {number}
 * @param min {number}
 * @param stiffness {number}
 * @returns {number}
 */
export const softMin = (value, min, stiffness) => {
    if (value >= min) return value;
    let delta = min - value;
    if (delta < 0.0001) return min;
    return value + delta * stiffness;
}

/**
 * Softly clamps towards a maximum value
 * @param value {number}
 * @param max {number}
 * @param stiffness {number}
 * @returns {number}
 */
export const softMax = (value, max, stiffness) => {
    if (value <= max) return value;
    let delta = value - max;
    if (delta < 0.0001) return max;
    return value - delta * stiffness;
}

/**
 * Softly clamps towards a minimum and maximum value
 * @param value {number}
 * @param min {number}
 * @param max {number}
 * @param stiffness {number}
 * @returns {number}
 */
export const softClamp = (value, min, max, stiffness) => {
    return softMax(softMin(value, min, stiffness), max, stiffness);
}