/**
 * @fileoverview DOM manipulation utility module
 * @author NHN Ent. FE Development Lab <dl_javascript@nhnent.com>
 */
import * as domevent from './domevent';
import snippet from 'tui-code-snippet';

const aps = Array.prototype.slice;
const trim = str => str.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, '');

/**
 * Setting element style
 * @param {(HTMLElement|SVGElement)} element - element to setting style
 * @param {(string|object)} key - style prop name or {prop: value} pair object
 * @param {string} [value] - style value
 * @name css
 * @memberof tui.dom
 * @function
 * @api
 */
export function css(element, key, value) {
    const {style} = element;

    if (snippet.isString(key)) {
        style[key] = value;

        return;
    }

    snippet.forEach(key, (v, k) => {
        style[k] = v;
    });
}

/**
 * Get HTML element's design classes.
 * @param {(HTMLElement|SVGElement)} element target element
 * @returns {string} element css class name
 * @name getClass
 * @memberof tui.dom
 * @function
 * @api
 */
export function getClass(element) {
    if (!element || !element.className) {
        return '';
    }

    if (snippet.isUndefined(element.className.baseVal)) {
        return element.className;
    }

    return element.className.baseVal;
}

/**
 * Check element has specific css class
 * @param {(HTMLElement|SVGElement)} element - target element
 * @param {string} cssClass - css class
 * @returns {boolean}
 * @name hasClass
 * @memberof tui.dom
 * @function
 * @api
 */
export function hasClass(element, cssClass) {
    if (element.classList) {
        return element.classList.contains(cssClass);
    }

    const origin = getClass(element).split(/\s+/);

    return snippet.inArray(cssClass, origin) > -1;
}

/**
 * Set className value
 * @param {(HTMLElement|SVGElement)} element - target element
 * @param {(string|string[])} cssClass - class names
 * @ignore
 */
function setClassName(element, cssClass) {
    cssClass = snippet.isArray(cssClass) ? cssClass.join(' ') : cssClass;

    cssClass = trim(cssClass);

    if (snippet.isUndefined(element.className.baseVal)) {
        element.className = cssClass;
        return;
    }

    element.className.baseVal = cssClass;
}

/**
 * Add css class to element
 * @param {(HTMLElement|SVGElement)} element - target element
 * @param {...string} cssClass - css classes to add
 * @name addClass
 * @memberof tui.dom
 * @function
 */
export function addClass(element) {
    let cssClass = aps.call(arguments, 1); // eslint-disable-line prefer-rest-params

    if (element.classList) {
        const {classList} = element;
        snippet.forEach(cssClass, name => {
            classList.add(name);
        });
        return;
    }

    const origin = getClass(element);

    if (origin) {
        cssClass = [].concat(origin.split(/\s+/), cssClass);
    }

    const newClass = [];

    snippet.forEach(cssClass, cls => {
        if (snippet.inArray(cls, newClass) < 0) {
            newClass.push(cls);
        }
    });

    setClassName(element, newClass);
}

/**
 * Toggle css class
 * @param {(HTMLElement|SVGElement)} element - target element
 * @param {...string} cssClass - css classes to toggle
 * @name toggleClass
 * @memberof tui.dom
 * @function
 */
export function toggleClass(element) {
    const cssClass = aps.call(arguments, 1); // eslint-disable-line prefer-rest-params

    if (element.classList) {
        snippet.forEach(cssClass, name => {
            element.classList.toggle(name);
        });
        return;
    }

    const newClass = getClass(element).split(/\s+/);

    snippet.forEach(cssClass, name => {
        const idx = snippet.inArray(name, newClass);

        if (idx > -1) {
            newClass.splice(idx, 1);
        } else {
            newClass.push(name);
        }
    });

    setClassName(element, newClass);
}

/**
 * Remove css class from element
 * @param {(HTMLElement|SVGElement)} element - target element
 * @param {...string} cssClass - css classes to remove
 * @name removeClass
 * @memberof tui.dom
 * @function
 */
export function removeClass(element) {    // eslint-disable-line
    const cssClass = aps.call(arguments, 1); // eslint-disable-line prefer-rest-params

    if (element.classList) {
        const {classList} = element;
        snippet.forEach(cssClass, name => {
            classList.remove(name);
        });

        return;
    }

    const origin = getClass(element).split(/\s+/);

    const newClass = snippet.filter(
        origin, name => snippet.inArray(name, cssClass) < 0
    );

    setClassName(element, newClass);
}

/**
 * getBoundingClientRect polyfill
 * @param {HTMLElement} element - target element
 * @returns {object} rect object
 * @name getRect
 * @memberof tui.dom
 * @function
 */
export function getRect(element) {
    const rect = element.getBoundingClientRect();
    const {top, right, bottom, left} = rect;
    let {width, height} = rect;

    if (snippet.isUndefined(width) || snippet.isUndefined(height)) {
        width = element.offsetWidth;
        height = element.offsetHeight;
    }

    return {top, right, bottom, left, width, height};
}

/**
 * Convert uppercase letter to hyphen lowercase character
 * @param {string} match - match from String.prototype.replace method
 * @returns {string}
 * @name upperToHyphenLower
 * @memberof tui.dom
 * @function
 */
function upperToHyphenLower(match) {
    return `-${match.toLowerCase()}`;
}

/**
 * Set data attribute to target element
 * @param {HTMLElement} element - element to set data attribute
 * @param {string} key - key
 * @param {string} value - value
 * @name setData
 * @memberof tui.dom
 * @function
 */
export function setData(element, key, value) {
    if (element.dataset) {
        element.dataset[key] = value;

        return;
    }

    key = key.replace(/([A-Z])/g, upperToHyphenLower);

    element.setAttribute(`data-${key}`, value);
}

/**
 * Get data value from data-attribute
 * @param {HTMLElement} element - target element
 * @param {string} key - key
 * @returns {string} value
 * @name getData
 * @memberof tui.dom
 * @function
 */
export function getData(element, key) {
    if (element.dataset) {
        return element.dataset[key];
    }

    key = key.replace(/([A-Z])/g, upperToHyphenLower);

    return element.getAttribute(`data-${key}`);
}

/**
 * Remove data property
 * @param {HTMLElement} element - target element
 * @param {string} key - key
 * @name removeData
 * @memberof tui.dom
 * @function
 */
export function removeData(element, key) {
    if (element.dataset) {
        delete element.dataset[key];

        return;
    }

    key = key.replace(/([A-Z])/g, upperToHyphenLower);

    element.removeAttribute(`data-${key}`);
}

/**
 * Remove element from parent node.
 * @param {HTMLElement} element - element to remove.
 * @name removeElement
 * @memberof tui.dom
 * @function
 */
export function removeElement(element) {
    if (element && element.parentNode) {
        element.parentNode.removeChild(element);
    }
}

/**
 * Set element bound
 * @param {HTMLElement} element - element to change bound
 * @param {object} bound - bound object
 * @param {number} [bound.top] - top pixel
 * @param {number} [bound.right] - right pixel
 * @param {number} [bound.bottom] - bottom pixel
 * @param {number} [bound.left] - left pixel
 * @param {number} [bound.width] - width pixel
 * @param {number} [bound.height] - height pixel
 * @name setBound
 * @memberof tui.dom
 * @function
 */
export function setBound(element, {top, right, bottom, left, width, height} = {}) {
    const args = {top, right, bottom, left, width, height};
    const newBound = {};

    snippet.forEach(args, (value, prop) => {
        if (snippet.isExisty(value)) {
            newBound[prop] = snippet.isNumber(value) ? `${value}px` : value;
        }
    });

    snippet.extend(element.style, newBound);
}

const elProto = Element.prototype;
const matchSelector = elProto.matches ||
    elProto.webkitMatchesSelector ||
    elProto.mozMatchesSelector ||
    elProto.msMatchesSelector ||
    function(selector) {
        const doc = this.document || this.ownerDocument;
        return snippet.inArray(this, findAll(doc, selector)) > -1;
    };

/**
 * Check element match selector
 * @param {HTMLElement} element - element to check
 * @param {string} selector - selector to check
 * @returns {boolean} is selector matched to element?
 * @name matches
 * @memberof tui.dom
 * @function
 */
export function matches(element, selector) {
    return matchSelector.call(element, selector);
}

/**
 * Find parent element recursively
 * @param {HTMLElement} element - base element to start find
 * @param {string} selector - selector string for find
 * @returns {HTMLElement} - element finded or null
 * @name closest
 * @memberof tui.dom
 * @function
 */
export function closest(element, selector) {
    let parent = element.parentNode;

    if (matches(element, selector)) {
        return element;
    }

    while (parent && parent !== document) {
        if (matches(parent, selector)) {
            return parent;
        }

        parent = parent.parentNode;
    }

    return null;
}

/**
 * Find single element
 * @param {(HTMLElement|string)} [element=document] - base element to find
 * @param {string} [selector] - css selector
 * @returns {HTMLElement}
 * @name find
 * @memberof tui.dom
 * @function
 */
export function find(element, selector) {
    if (snippet.isString(element)) {
        return document.querySelector(element);
    }

    return element.querySelector(selector);
}

/**
 * Find multiple element
 * @param {(HTMLElement|string)} [element=document] - base element to
 *  find
 * @param {string} [selector] - css selector
 * @returns {HTMLElement[]}
 * @name findAll
 * @memberof tui.dom
 * @function
 */
export function findAll(element, selector) {
    if (snippet.isString(element)) {
        return snippet.toArray(document.querySelectorAll(element));
    }

    return snippet.toArray(element.querySelectorAll(selector));
}

/**
 * Stop event propagation.
 * @param {Event} e - event object
 * @name stopPropagation
 * @memberof tui.dom
 * @function
 */
export function stopPropagation(e) {
    if (e.stopPropagation) {
        e.stopPropagation();

        return;
    }

    e.cancelBubble = true;
}

/**
 * Prevent default action
 * @param {Event} e - event object
 * @name preventDefault
 * @memberof tui.dom
 * @function
 */
export function preventDefault(e) {
    if (e.preventDefault) {
        e.preventDefault();

        return;
    }

    e.returnValue = false;
}

/**
 * Check specific CSS style is available.
 * @param {array} props property name to testing
 * @returns {(string|boolean)} return true when property is available
 * @name testCSSProp
 * @memberof tui.dom
 * @function
 * @example
 * //-- #1. Get Module --//
 * var domUtil = require('tui-dom'); // node, commonjs
 * var domUtil = tui.dom; // distribution file
 *
 * //-- #2. Use property --//
 * var props = ['transform', '-webkit-transform'];
 * domutil.testCSSProp(props);    // 'transform'
 */
function testCSSProp(props) {
    const {style} = document.documentElement;
    const len = props.length;

    for (let i = 0; i < len; i += 1) {
        if (props[i] in style) {
            return props[i];
        }
    }

    return false;
}

let prevSelectStyle = '';
const SUPPORT_SELECTSTART = 'onselectstart' in document;
const userSelectProperty = testCSSProp([
    'userSelect',
    'WebkitUserSelect',
    'OUserSelect',
    'MozUserSelect',
    'msUserSelect'
]);

/**
 * Disable browser's text selection behaviors.
 * @param {HTMLElement} [el] - target element. if not supplied, use `document`
 * @name disableTextSelection
 * @memberof tui.dom
 * @function
 */
export function disableTextSelection(el = document) {
    let style;

    if (SUPPORT_SELECTSTART) {
        domevent.on(el, 'selectstart', preventDefault);
    } else {
        el = (el === document) ? document.documentElement : el;
        style = el.style;
        prevSelectStyle = style[userSelectProperty];
        style[userSelectProperty] = 'none';
    }
}

/**
 * Enable browser's text selection behaviors.
 * @param {HTMLElement} [el] - target element. if not supplied, use `document`
 * @name enableTextSelection
 * @memberof tui.dom
 * @function
 */
export function enableTextSelection(el = document) {
    if (SUPPORT_SELECTSTART) {
        domevent.off(el, 'selectstart', preventDefault);
    } else {
        el = (el === document) ? document.documentElement : el;
        el.style[userSelectProperty] = prevSelectStyle;
    }
}

/**
 * Represents the text content of a node and its descendants
 * @param {HTMLElement} element - html element
 * @returns {string} text content
 * @name textContent
 * @memberof tui.dom
 * @function
 */
export function textContent(element) {
    if (snippet.isExisty(element.textContent)) {
        return element.textContent;
    }

    return element.innerText;
}

/**
 * Insert element to next of target element
 * @param {HTMLElement} element - html element to insert
 * @param {HTMLElement} target - target element
 * @name insertAfter
 * @memberof tui.dom
 * @function
 */
export function insertAfter(element, target) {
    const parent = target.parentNode;

    if (target === parent.lastChild) {
        parent.appendChild(element);
    } else {
        parent.insertBefore(element, target.nextSibling);
    }
}