import EventHandler from "./event-handler.js"

const vibrate = () => {
    if ('vibrate' in window.navigator) {
        window.navigator.vibrate([200, 100, 200])
        const handler = window.setTimeout(function () {
            window.clearTimeout(handler)
            window.navigator.vibrate([])
        }, 1000)
    }
}

const isFunction = object => {
    return typeof object === 'function'
}

function selectionSet(elem) {
    const sel = document.getSelection();
    if (sel) {
        const range = document.createRange();
        range.selectNodeContents(elem);
        sel.removeAllRanges();
        sel.addRange(range);
    }
}

function selectionClear() {
    const sel = document.getSelection();
    if (sel) {
        sel.removeAllRanges();
    }
}

function copyTextUsingDOM(str) {
    const tempElem = document.createElement("div");
    tempElem.setAttribute("style", "-webkit-user-select: text !important");
    let spanParent = tempElem;
    if (tempElem.attachShadow) {
        spanParent = tempElem.attachShadow({ mode: "open" });
    }
    const span = document.createElement("span");
    span.innerText = str;
    spanParent.appendChild(span);
    document.body.appendChild(tempElem);
    selectionSet(span);
    const result = document.execCommand("copy");
    selectionClear();
    document.body.removeChild(tempElem);
    return result;
}

const copy = (text = '') => {
    if (navigator.clipboard) {
        navigator.clipboard.writeText(text)
    }
    else {
        copyTextUsingDOM(text)
    }
}

const getTextFromClipboard = () => {
    return navigator.clipboard.readText();
}

async function getAllClipboardContents() {
    try {
        const clipboardItems = await navigator.clipboard.read();
        let items = [];
        for (const clipboardItem of clipboardItems) {
            for (const mimeType of clipboardItem.types) {
                const blob = await clipboardItem.getType(mimeType);
                const arrayBuffer = await blob.arrayBuffer();
                items.push({
                    mimeType: mimeType,
                    data: new Uint8Array(arrayBuffer)
                });
            }
        }
        return items;
    } catch (error) {
        console.error('Failed to read from clipboard:', error);
    }
    return [];
}

const getUID = (prefix = 'bb') => {
    let id = "";
    do {
        const code = Math.floor(Math.random() * 1000000);
        id = `${prefix}_${code}`;
    }
    while (document.getElementById(id))

    return id;
}

const getInnerWidth = element => getWidth(element, true)

const getOuterWidth = element => {
    let width = element.getBoundingClientRect().width
    const styles = getComputedStyle(element)
    const marginLeft = parseFloat(styles.marginLeft)
    const marginRight = parseFloat(styles.marginRight)
    return width + marginLeft + marginRight
}

const getWidth = (element, self = false) => {
    let width = element.getBoundingClientRect().width
    if (self) {
        const styles = getComputedStyle(element)
        const borderLeftWidth = parseFloat(styles.borderLeftWidth)
        const borderRightWidth = parseFloat(styles.borderRightWidth)
        const paddingLeft = parseFloat(styles.paddingLeft)
        const paddingRight = parseFloat(styles.paddingRight)
        width = width - borderLeftWidth - borderRightWidth - paddingLeft - paddingRight
    }
    return width
}

const getInnerHeight = element => getHeight(element, true)

const getOuterHeight = element => {
    let height = element.getBoundingClientRect().height
    const styles = getComputedStyle(element)
    const marginTop = parseFloat(styles.marginTop)
    const marginBottom = parseFloat(styles.marginBottom)
    return height + marginTop + marginBottom
}

const getHeight = (element, self = false) => {
    let height = element.getBoundingClientRect().height
    if (self) {
        const styles = getComputedStyle(element)
        const borderTopWidth = parseFloat(styles.borderTopWidth)
        const borderBottomWidth = parseFloat(styles.borderBottomWidth)
        const paddingTop = parseFloat(styles.paddingTop)
        const paddingBottom = parseFloat(styles.paddingBottom)
        height = height - borderBottomWidth - borderTopWidth - paddingTop - paddingBottom
    }
    return height
}

const getWindowScroll = node => {
    const win = getWindow(node)
    const scrollLeft = win.pageXOffset
    const scrollTop = win.pageYOffset
    return {
        scrollLeft: scrollLeft,
        scrollTop: scrollTop
    }
}

const getWindow = node => {
    if (!node) {
        return window
    }

    if (node.toString() !== '[object Window]') {
        const ownerDocument = node.ownerDocument
        return ownerDocument ? ownerDocument.defaultView || window : window
    }

    return node
}

const normalizeLink = link => {
    let url = link
    if (url.indexOf('./') === 0) {
        url = url.substring(2)
    }
    while (url.indexOf('../') === 0) {

        url = url.substring(3)
    }
    return url
}

/**
 * 添加 script 标签到 head
 * @param {string} content
 * @returns
 */
const addScript = content => {
    // content 文件名
    const scripts = [...document.getElementsByTagName('script')]
    const url = normalizeLink(content)
    let link = scripts.filter(function (link) {
        return link.src.indexOf(url) > -1
    })
    if (link.length === 0) {
        const script = document.createElement('script')
        link.push(script)
        script.setAttribute('src', content)
        document.body.appendChild(script)
        script.onload = () => {
            script.setAttribute('loaded', true)
        }
    }
    return new Promise((resolve, reject) => {
        const handler = setInterval(() => {
            const done = link[0].getAttribute('loaded') === 'true'
            if (done) {
                clearInterval(handler)
                resolve()
            }
        }, 20)
    })
}

/**
 * 从 head 移除 script 标签
 * @param {string} content
 */
const removeScript = content => {
    const links = [...document.getElementsByTagName('script')]
    const url = normalizeLink(content)
    const nodes = links.filter(function (link) {
        return link.src.indexOf(url) > -1
    })
    for (let index = 0; index < nodes.length; index++) {
        document.body.removeChild(nodes[index])
    }
}

/**
 * 批量添加 script 标签到 head
 * @param {string[]} content
 * @returns
 */
const addScriptBatch = content => {
    const promises = content.map(item => addScript(item));
    return Promise.all(promises);
}

/**
 * 从 head 批量移除 script 标签
 * @param {string[]} content
 * @returns
 */
const removeScriptBatch = (content) => {
    const promises = content.map(item => removeScript(item));
    return Promise.all(promises);
}

/**
 * 批量添加 link 标签到 head
 * @param {string[]} href
 * @param {string} rel
 * @returns
 */
const addLinkBatch = (href, rel = "stylesheet") => {
    const promises = href.map(item => addLink(item, rel));
    return Promise.all(promises);
}

/**
 * 从 head 批量移除 link 标签
 * @param {string[]} href
 * @returns
 */
const removeLinkBatch = (href) => {
    const promises = href.map(item => removeLink(item));
    return Promise.all(promises);
}

/**
 * 添加 link 标签到 head
 * @param {string} href
 * @param {string} rel
 * @returns
 */
const addLink = (href, rel = "stylesheet") => {
    const links = [...document.getElementsByTagName('link')]
    const url = normalizeLink(href)
    let link = links.filter(function (link) {
        return link.href.indexOf(url) > -1
    })
    if (link.length === 0) {
        const css = document.createElement('link')
        link.push(css)
        css.setAttribute("rel", rel)
        css.setAttribute('href', href)
        document.getElementsByTagName("head")[0].appendChild(css)
        css.onload = () => {
            css.setAttribute('loaded', true)
        }
    }
    return new Promise((resolve, reject) => {
        const handler = setInterval(() => {
            const done = link[0].getAttribute('loaded') === 'true'
            if (done) {
                clearInterval(handler)
                resolve()
            }
        }, 20)
    })
}

/**
 * 从 head 移除 link 标签
 * @param {string} href
 */
const removeLink = href => {
    const links = [...document.getElementsByTagName('link')]
    const url = normalizeLink(href)
    const nodes = links.filter(function (link) {
        return link.href.indexOf(url) > -1
    })
    for (let index = 0; index < nodes.length; index++) {
        document.getElementsByTagName("head")[0].removeChild(nodes[index])
    }
}

/**
 * 自动识别 css 或者 js 链接并添加到 head
 * @param {string[]} fileList
 */
const autoAdd = (fileList) => {
    const promises = fileList.map(async (item) => {
        const extension = item.match(/\.(\w+)(\?|$)/)[1];
        if (extension === 'js') {
            return addScript(item);
        }
        else if (extension === 'css') {
            return addLink(item);
        }
    });

    return Promise.all(promises);
}

/**
 * 自动识别 css 或者 js 链接并从 head 中移除
 * @param {string[]} fileList
 */
const autoRemove = (fileList) => {
    const promises = fileList.map(async (item) => {
        const extension = item.match(/\.(\w+)(\?|$)/)[1];
        if (extension === 'js') {
            return removeScript(item);
        }
        else if (extension === 'css') {
            return removeLink(item);
        }
    });

    return Promise.all(promises);
}

const insertBefore = (element, newEl) => {
    if (element) {
        const parentNode = element.parentNode
        if (parentNode) {
            if (element) {
                parentNode.insertBefore(newEl, element)
            }
        }
    }
}

const insertAfter = (element, newEl) => {
    if (element) {
        const parentNode = element.parentNode
        if (parentNode) {
            if (element.nextElementSibling) {
                parentNode.insertBefore(newEl, element.nextElementSibling)
            }
            else {
                parentNode.appendChild(newEl)
            }
        }
    }
}

const drag = (element, start, move, end) => {
    const handleDragStart = e => {
        let notDrag = false
        if (isFunction(start)) {
            notDrag = start(e) || false
        }

        if (!notDrag) {
            if (e.cancelable) {
                e.preventDefault();
            }
            e.stopPropagation()

            document.addEventListener('mousemove', handleDragMove)
            document.addEventListener('touchmove', handleDragMove)
            document.addEventListener('mouseup', handleDragEnd)
            document.addEventListener('touchend', handleDragEnd)
        }
    }

    const handleDragMove = e => {
        if (e.touches && e.touches.length > 1) {
            return;
        }

        if (isFunction(move)) {
            move(e)
        }
    }

    const handleDragEnd = e => {
        if (isFunction(end)) {
            end(e)
        }

        const handler = window.setTimeout(() => {
            window.clearTimeout(handler)
            document.removeEventListener('mousemove', handleDragMove)
            document.removeEventListener('touchmove', handleDragMove)
            document.removeEventListener('mouseup', handleDragEnd)
            document.removeEventListener('touchend', handleDragEnd)
        }, 10)
    }

    element.addEventListener('mousedown', handleDragStart)
    element.addEventListener('touchstart', handleDragStart)
}

const getDescribedElement = (element, selector = 'aria-describedby') => {
    if (isElement(element)) {
        let id = element.getAttribute(selector)
        if (id) {
            if (id.indexOf('.') === -1) {
                id = `#${id}`
            }
            return document.querySelector(id)
        }
    }
    return null
}

const getDescribedOwner = (element, selector = 'aria-describedby') => {
    if (isElement(element)) {
        const id = element.getAttribute('id')
        if (id) {
            return document.querySelector(`[${selector}="${id}"]`)
        }
    }
    return null
}

const isElement = object => {
    if (!object || typeof object !== 'object') {
        return false
    }

    if (typeof object.jquery !== 'undefined') {
        object = object[0]
    }

    return typeof object.nodeType !== 'undefined'
}

const getElement = object => {
    // it's a jQuery object or a node element
    if (isElement(object)) {
        return object.jquery ? object[0] : object
    }

    if (typeof object === 'string' && object.length > 0) {
        return document.querySelector(object)
    }

    return null
}

const getElementById = object => {
    if (typeof object === 'string' && object.length > 0 && object.substring(0, 1) !== '.' && object.substring(0, 1) !== '#') {
        object = `#${object}`
    }

    return getElement(object);
}

const getTargetElement = (element, selector = 'data-bs-target') => {
    if (isElement(element)) {
        const id = element.getAttribute(selector)
        if (id) {
            return document.querySelector(id)
        }
    }
    return null
}

const getTransitionDelayDurationFromElement = (element, delay = 80) => {
    return getTransitionDurationFromElement(element) + delay
}

const getTransitionDurationFromElement = (element) => {
    if (!element) {
        return 0
    }

    // Get transition-duration of the element
    let { transitionDuration, transitionDelay } = window.getComputedStyle(element)

    const floatTransitionDuration = Number.parseFloat(transitionDuration)
    const floatTransitionDelay = Number.parseFloat(transitionDelay)

    // Return 0 if element or transition duration is not found
    if (!floatTransitionDuration && !floatTransitionDelay) {
        return 0
    }

    // If multiple durations are defined, take the first
    transitionDuration = transitionDuration.split(',')[0]
    transitionDelay = transitionDelay.split(',')[0]

    return (Number.parseFloat(transitionDuration) + Number.parseFloat(transitionDelay)) * 1000
}

const isVisible = element => {
    if (!isElement(element) || element.getClientRects().length === 0) {
        return false
    }

    const elementIsVisible = getComputedStyle(element).getPropertyValue('visibility') === 'visible'
    // Handle `details` element as its content may falsie appear visible when it is closed
    const closedDetails = element.closest('details:not([open])')

    if (!closedDetails) {
        return elementIsVisible
    }

    if (closedDetails !== element) {
        const summary = element.closest('summary')
        if (summary && summary.parentNode !== closedDetails) {
            return false
        }

        if (summary === null) {
            return false
        }
    }

    return elementIsVisible
}

const isDisabled = element => {
    if (!element || element.nodeType !== Node.ELEMENT_NODE) {
        return true
    }

    if (element.classList.contains('disabled')) {
        return true
    }

    if (typeof element.disabled !== 'undefined') {
        return element.disabled
    }

    return element.hasAttribute('disabled') && element.getAttribute('disabled') !== 'false'
}

const hackPopover = (popover, css) => {
    if (popover) {
        popover._isWithContent = () => true

        const getTipElement = popover._getTipElement
        let fn = tip => {
            tip.classList.add(css)
        }
        popover._getTipElement = () => {
            let tip = getTipElement.call(popover)
            fn(tip)
            return tip
        }
    }
}

const hackTooltip = function () {
    const mock = () => {
        const originalDispose = bootstrap.Tooltip.prototype.dispose;
        bootstrap.Tooltip.prototype.dispose = function () {
            originalDispose.call(this);
            // fix https://github.com/twbs/bootstrap/issues/37474
            this._activeTrigger = {};
            this._element = document.createElement('noscript'); // placeholder with no behavior
        }
    }
    registerBootstrapBlazorModule('Tooltip', null, mock);
}

const setIndeterminate = (object, state) => {
    const element = getElementById(object)
    if (isElement(element)) {
        element.indeterminate = state;
    }
}

const getOverflowParent = element => {
    let parent = element.parentNode
    while (parent.nodeType === 1) {
        const style = getComputedStyle(parent)
        const overflowY = style.getPropertyValue('overflow-y')
        if (overflowY === 'auto' || overflowY === 'scroll') {
            break;
        }
        parent = parent.parentNode
    }
    if (parent.nodeType !== 1) {
        parent = getWindow()
    }
    return parent
}

/*
 * @param {function} fn - 原函数
 * @param {number} duration - 防抖时长
 * @return {function} - 条件回调返回真时立即执行
 */
const debounce = function (fn, duration = 200, callback = null) {
    let handler = null
    return function () {
        if (handler) {
            clearTimeout(handler)
        }
        if (callback && typeof (callback) === 'function') {
            const v = callback.apply(this, arguments)
            if (v === true) {
                handler = null
            }
        }
        if (handler === null) {
            fn.apply(this, arguments)

            handler = setTimeout(() => {
                handler = null
            }, duration)
        }
        else {
            handler = setTimeout(() => {
                fn.apply(this, arguments)
            }, duration)
        }
    }
}

export function openUrl(url, target = '_blank', features = null) {
    window.open(url, target, features);
}

export function runEval(code) {
    try {
        return eval(code);
    } catch (e) {
        console.warn(e.message);
        return e.message;
    }
}

export function runFunction(code, arg) {
    try {
        var func = new Function(code);
        return func(...arg);
    } catch (e) {
        console.warn(e.message);
        return e.message;
    }
}

export function isMobile() {
    return /android|webos|iphone|ipad|ipod|blackberry|iemobile|opera mini|mobile/i.test(navigator.userAgent);
}

const hashCode = str => {
    let hash = 0;
    for (let i = 0; i < str.length; i++) {
        const char = str.charCodeAt(i);
        hash = (hash << 5) - hash + char;
        hash |= 0;
    }
    return hash;
}

export function getFingerCode() {
    const canvas = document.createElement('canvas');
    canvas.width = 200;
    canvas.height = 200;

    const ctx = canvas.getContext('2d');
    ctx.fillStyle = 'rgb(128, 0, 0)';
    ctx.fillRect(10, 10, 100, 100);

    ctx.fillStyle = 'rgb(0, 128, 0)';
    ctx.fillRect(50, 50, 100, 100);
    ctx.strokeStyle = 'rgb(0, 0, 128)'
    ctx.lineWidth = 5;
    ctx.strokeRect(30, 30, 80, 80);

    ctx.font = '20px Arial';
    ctx.fillStyle = 'rgb(0, 0, 0)';
    ctx.fillText('BootstrapBlazor', 60, 116);

    const dataURL = canvas.toDataURL();
    const hash = hashCode(dataURL);
    return hash.toString();
}

export function getHtml(options) {
    let html = '';
    let el = null;
    if (options.id) {
        el = document.getElementById(options.id);
    }
    else if (options.selector) {
        el = document.querySelector(options.selector);
    }
    if (el) {
        html = el.outerHTML;
    }
    return html;
}

export function getTheme() {
    return localStorage.getItem('theme') || document.documentElement.getAttribute('data-bs-theme') || getAutoThemeValue();
}

export function saveTheme(theme) {
    if (localStorage) {
        localStorage.setItem('theme', theme);
    }
}

export function getAutoThemeValue() {
    return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
}

export function setTheme(theme, sync) {
    if (theme === 'auto' && window.matchMedia('(prefers-color-scheme: dark)').matches) {
        document.documentElement.setAttribute('data-bs-theme', 'dark')
    }
    else {
        document.documentElement.setAttribute('data-bs-theme', theme);
    }

    if (sync === true) {
        const providers = document.querySelectorAll('.bb-theme-mode');
        providers.forEach(p => {
            const activeItem = p.querySelector(`.dropdown-item[data-bb-theme-value="${theme}"]`);
            setActiveTheme(p, activeItem)
        })
        saveTheme(theme);
    }
    EventHandler.trigger(document, 'changed.bb.theme', { theme: theme });
}

export function setActiveTheme(el, activeItem) {
    const currentTheme = el.querySelector('.active');
    if (currentTheme) {
        currentTheme.classList.remove('active');
    }

    if (activeItem) {
        activeItem.classList.add('active');
        const iconItem = activeItem.querySelector('[data-bb-theme-icon]');
        if (iconItem) {
            const icon = iconItem.getAttribute('data-bb-theme-icon');
            if (icon) {
                const toggleIcon = el.querySelector('.bb-theme-mode-active');
                if (toggleIcon) {
                    toggleIcon.outerHTML = `<i class="${icon} bb-theme-mode-active"></i>`;
                }
            }
        }
    }
}

export function switchTheme(theme, x = 0, y = 0, sync = true) {
    if (isFunction(document.startViewTransition)) {
        document.documentElement.style.setProperty('--bb-theme-x', `${x}px`);
        document.documentElement.style.setProperty('--bb-theme-y', `${y}px`);
        document.startViewTransition(() => {
            setTheme(theme, sync);
        });
    }
    else {
        setTheme(theme, sync);
    }
}

const deepMerge = (obj1, obj2, skipNull = true) => {
    for (const key in obj2) {
        if (obj2.hasOwnProperty(key)) {
            if (obj2[key] instanceof Object && obj1[key] instanceof Object) {
                obj1[key] = deepMerge(obj1[key], obj2[key]);
            }
            else {
                const value = obj2[key];
                if (skipNull && (value === null || value === void 0)) {
                    continue;
                }
                obj1[key] = obj2[key];
            }
        }
    }
    return obj1;
}

export function registerBootstrapBlazorModule(name, identifier, callback) {
    window.BootstrapBlazor = window.BootstrapBlazor || {};
    window.BootstrapBlazor[name] = window.BootstrapBlazor[name] || {
        _init: false,
        _items: [],
        register: function (id, cb) {
            if (id) {
                this._items.push(id);
            }
            if (this._init === false) {
                this._init = true;
                if (isFunction(cb)) {
                    cb(this);
                }
            }
            return this;
        },
        dispose: function (id, cb) {
            if (id) {
                this._items = this._items.filter(item => item !== id);
            }
            if (this._items.length === 0) {
                this._init = false;
                if (isFunction(cb)) {
                    cb(this);
                }
            }
        }
    };

    return window.BootstrapBlazor[name].register(identifier, callback);
}

export function setTitle(title) {
    document.title = title;
}

export function calcCenterPosition(el) {
    const rect = el.getBoundingClientRect();
    return {
        x: rect.left + el.offsetWidth / 2,
        y: rect.top + el.offsetHeight / 2
    }
}

export function setMemorialMode(memorial) {
    const el = document.documentElement;
    if (memorial) {
        const theme = el.getAttribute('data-bs-theme');
        if (theme) {
            el.setAttribute('data-bs-original-theme', theme);
        }
        el.setAttribute('data-bs-theme', 'dark');
        el.setAttribute('data-bb-theme', 'memorial');
    }
    else {
        const theme = el.getAttribute('data-bs-original-theme');
        el.removeAttribute('data-bs-theme');
        el.removeAttribute('data-bb-theme');
        if (theme) {
            el.setAttribute('data-bs-theme', theme);
        }
    }
}

export function drawImage(canvas, image, offsetWidth, offsetHeight) {
    canvas.width = offsetWidth * devicePixelRatio;
    canvas.height = offsetHeight * devicePixelRatio;
    canvas.style.width = `${offsetWidth}px`;
    canvas.style.height = `${offsetHeight}px`;
    const context = canvas.getContext('2d');
    context.scale(devicePixelRatio, devicePixelRatio);
    context.drawImage(image, 0, 0, offsetWidth, offsetHeight);
}

/**
 *  @param {File} file
 *  @returns {Blob}
 */
export function readFileAsync(file) {
    return new Promise((resolve, reject) => {
        const reader = new FileReader();

        reader.onload = () => {
            const blob = new Blob([reader.result], { type: file.type });
            resolve(blob);
        };

        reader.onerror = (error) => {
            reject(error);
        };

        reader.readAsArrayBuffer(file);
    });
}

export function getNetworkInfo() {
    if (navigator.connection) {
        const { downlink, effectiveType, rtt } = navigator.connection;
        return {
            downlink: downlink,
            networkType: effectiveType,
            rTT: rtt
        };
    }
    return null;
}

export function getClientHubId() {
    return localStorage.getItem('bb_hub_connection_id');
}

export {
    autoAdd,
    autoRemove,
    addLinkBatch,
    removeLinkBatch,
    addScriptBatch,
    removeScriptBatch,
    addLink,
    addScript,
    copy,
    deepMerge,
    debounce,
    drag,
    getTextFromClipboard,
    getAllClipboardContents,
    insertBefore,
    insertAfter,
    isDisabled,
    isElement,
    isFunction,
    isVisible,
    getElement,
    getElementById,
    getDescribedElement,
    getDescribedOwner,
    getHeight,
    getInnerHeight,
    getInnerWidth,
    getOuterHeight,
    getOuterWidth,
    getOverflowParent,
    getTargetElement,
    getTransitionDelayDurationFromElement,
    getWidth,
    getWindow,
    getWindowScroll,
    getUID,
    hackTooltip,
    hackPopover,
    removeLink,
    removeScript,
    setIndeterminate,
    vibrate
}
