"use strict";

require("../util/polyfill.js");
const api = require("../api.js");
const templates = require("../templates.js");
const domParser = new DOMParser();
const misc = require("./misc.js");
const uri = require("./uri.js");

function _imbueId(options) {
    if (!options.id) {
        options.id = "gen-" + Math.random().toString(36).substring(7);
    }
}

function _makeLabel(options, attrs) {
    if (!options.text) {
        return "";
    }
    if (!attrs) {
        attrs = {};
    }
    attrs.for = options.id;
    return makeElement("label", attrs, options.text);
}

function makeFileSize(fileSize) {
    return misc.formatFileSize(fileSize);
}

function makeMarkdown(text) {
    return misc.formatMarkdown(text);
}

function makeRelativeTime(time) {
    return makeElement(
        "time",
        { datetime: time, title: time },
        misc.formatRelativeTime(time)
    );
}

function makeThumbnail(url) {
    return makeElement(
        "span",
        url
            ? {
                  class: "thumbnail",
                  style: `background-image: url(\'${url}\')`,
              }
            : { class: "thumbnail empty" },
        makeElement("img", { alt: "thumbnail", src: url })
    );
}

function makeRadio(options) {
    _imbueId(options);
    return makeElement(
        "label",
        { for: options.id },
        makeElement("input", {
            id: options.id,
            name: options.name,
            value: options.value,
            type: "radio",
            checked: options.selectedValue === options.value,
            disabled: options.readonly,
            required: options.required,
        }),
        makeElement("span", { class: "radio" }, options.text)
    );
}

function makeCheckbox(options) {
    _imbueId(options);
    return makeElement(
        "label",
        { for: options.id },
        makeElement("input", {
            id: options.id,
            name: options.name,
            value: options.value,
            type: "checkbox",
            checked: options.checked !== undefined ? options.checked : false,
            disabled: options.readonly,
            required: options.required,
        }),
        makeElement("span", { class: "checkbox" }, options.text)
    );
}

function makeSelect(options) {
    return (
        _makeLabel(options) +
        makeElement(
            "select",
            {
                id: options.id,
                name: options.name,
                disabled: options.readonly,
            },
            ...Object.keys(options.keyValues).map((key) =>
                makeElement(
                    "option",
                    { value: key, selected: key === options.selectedKey },
                    options.keyValues[key]
                )
            )
        )
    );
}

function makeInput(options) {
    options.value = options.value || "";
    return _makeLabel(options) + makeElement("input", options);
}

function makeButton(options) {
    options.type = "button";
    return makeInput(options);
}

function makeTextInput(options) {
    options.type = "text";
    return makeInput(options);
}

function makeTextarea(options) {
    const value = options.value || "";
    delete options.value;
    return _makeLabel(options) + makeElement("textarea", options, value);
}

function makePasswordInput(options) {
    options.type = "password";
    return makeInput(options);
}

function makeEmailInput(options) {
    options.type = "email";
    return makeInput(options);
}

function makeColorInput(options) {
    const textInput = makeElement("input", {
        type: "text",
        value: options.value || "",
        required: options.required,
        class: "color",
    });
    const backgroundPreviewNode = makeElement("div", {
        class: "preview background-preview",
        style: `border-color: ${options.value};
                background-color: ${options.value}`,
    });
    const textPreviewNode = makeElement("div", {
        class: "preview text-preview",
        style: `border-color: ${options.value};
                color: ${options.value}`,
    });
    return makeElement(
        "label",
        { class: "color" },
        textInput,
        backgroundPreviewNode,
        textPreviewNode
    );
}

function makeNumericInput(options) {
    options.type = "number";
    return makeInput(options);
}

function makeDateInput(options) {
    options.type = "date";
    return makeInput(options);
}

function getPostUrl(id, parameters) {
    return uri.formatClientLink(
        "post",
        id,
        parameters ? { query: parameters.query } : {}
    );
}

function getPostEditUrl(id, parameters) {
    return uri.formatClientLink(
        "post",
        id,
        "edit",
        parameters ? { query: parameters.query } : {}
    );
}

function makePostLink(id, includeHash) {
    let text = id;
    if (includeHash) {
        text = "@" + id;
    }
    return api.hasPrivilege("posts:view")
        ? makeElement(
              "a",
              { href: uri.formatClientLink("post", id) },
              misc.escapeHtml(text)
          )
        : misc.escapeHtml(text);
}

function makeTagLink(name, includeHash, includeCount, tag) {
    const category = tag ? tag.category : "unknown";
    let text = misc.getPrettyName(name);
    if (includeHash === true) {
        text = "#" + text;
    }
    if (includeCount === true) {
        text += " (" + (tag ? tag.postCount : 0) + ")";
    }
    return api.hasPrivilege("tags:view")
        ? makeElement(
              "a",
              {
                  href: uri.formatClientLink("tag", name),
                  class: misc.makeCssName(category, "tag"),
              },
              misc.escapeHtml(text)
          )
        : makeElement(
              "span",
              { class: misc.makeCssName(category, "tag") },
              misc.escapeHtml(text)
          );
}

function makePoolLink(id, includeHash, includeCount, pool, name) {
    const category = pool ? pool.category : "unknown";
    let text = misc.getPrettyName(name ? name : pool.names[0]);
    if (includeHash === true) {
        text = "#" + text;
    }
    if (includeCount === true) {
        text += " (" + (pool ? pool.postCount : 0) + ")";
    }
    return api.hasPrivilege("pools:view")
        ? makeElement(
              "a",
              {
                  href: uri.formatClientLink("pool", id),
                  class: misc.makeCssName(category, "pool"),
              },
              misc.escapeHtml(text)
          )
        : makeElement(
              "span",
              { class: misc.makeCssName(category, "pool") },
              misc.escapeHtml(text)
          );
}

function makeUserLink(user) {
    let text = makeThumbnail(user ? user.avatarUrl : null);
    text += user && user.name ? misc.escapeHtml(user.name) : "Anonymous";
    const link =
        user && api.hasPrivilege("users:view")
            ? makeElement(
                  "a",
                  { href: uri.formatClientLink("user", user.name) },
                  text
              )
            : text;
    return makeElement("span", { class: "user" }, link);
}

function makeFlexboxAlign(options) {
    return [...misc.range(20)]
        .map(() => '<li class="flexbox-dummy"></li>')
        .join("");
}

function makeAccessKey(html, key) {
    const regex = new RegExp("(" + key + ")", "i");
    html = html.replace(
        regex,
        '<span class="access-key" data-accesskey="$1">$1</span>'
    );
    return html;
}

function _serializeElement(name, attributes) {
    return [name]
        .concat(
            Object.keys(attributes).map((key) => {
                if (attributes[key] === true) {
                    return key;
                } else if (
                    attributes[key] === false ||
                    attributes[key] === undefined
                ) {
                    return "";
                }
                const attribute = misc.escapeHtml(attributes[key] || "");
                return `${key}="${attribute}"`;
            })
        )
        .join(" ");
}

function makeElement(name, attrs, ...content) {
    return content.length !== undefined
        ? `<${_serializeElement(name, attrs)}>${content.join("")}</${name}>`
        : `<${_serializeElement(name, attrs)}/>`;
}

function emptyContent(target) {
    while (target.lastChild) {
        target.removeChild(target.lastChild);
    }
}

function replaceContent(target, source) {
    emptyContent(target);
    if (source instanceof NodeList) {
        for (let child of [...source]) {
            target.appendChild(child);
        }
    } else if (source instanceof Node) {
        target.appendChild(source);
    } else if (source !== null) {
        throw `Invalid view source: ${source}`;
    }
}

function showMessage(target, message, className) {
    if (!message) {
        message = "Unknown message";
    }
    const messagesHolderNode = target.querySelector(".messages");
    if (!messagesHolderNode) {
        return false;
    }
    const textNode = document.createElement("div");
    textNode.innerHTML = message.replace(/\n/g, "<br/>");
    textNode.classList.add("message");
    textNode.classList.add(className);
    const wrapperNode = document.createElement("div");
    wrapperNode.classList.add("message-wrapper");
    wrapperNode.appendChild(textNode);
    messagesHolderNode.appendChild(wrapperNode);
    return true;
}

function appendExclamationMark() {
    if (!document.title.startsWith("!")) {
        document.oldTitle = document.title;
        document.title = `! ${document.title}`;
    }
}

function showError(target, message) {
    appendExclamationMark();
    return showMessage(target, misc.formatInlineMarkdown(message), "error");
}

function showSuccess(target, message) {
    return showMessage(target, misc.formatInlineMarkdown(message), "success");
}

function showInfo(target, message) {
    return showMessage(target, misc.formatInlineMarkdown(message), "info");
}

function clearMessages(target) {
    if (document.oldTitle) {
        document.title = document.oldTitle;
        document.oldTitle = null;
    }
    for (let messagesHolderNode of target.querySelectorAll(".messages")) {
        emptyContent(messagesHolderNode);
    }
}

function htmlToDom(html) {
    // code taken from jQuery + Krasimir Tsonev's blog
    const wrapMap = {
        _: [1, "<div>", "</div>"],
        option: [1, "<select multiple>", "</select>"],
        legend: [1, "<fieldset>", "</fieldset>"],
        area: [1, "<map>", "</map>"],
        param: [1, "<object>", "</object>"],
        thead: [1, "<table>", "</table>"],
        tr: [2, "<table><tbody>", "</tbody></table>"],
        td: [3, "<table><tbody><tr>", "</tr></tbody></table>"],
        col: [2, "<table><tbody></tbody><colgroup>", "</colgroup></table>"],
    };
    wrapMap.optgroup = wrapMap.option;
    wrapMap.tbody = wrapMap.thead;
    wrapMap.tfoot = wrapMap.thead;
    wrapMap.colgroup = wrapMap.thead;
    wrapMap.caption = wrapMap.thead;
    wrapMap.th = wrapMap.td;

    let element = document.createElement("div");
    const match = /<\s*(\w+)[^>]*?>/g.exec(html);

    if (match) {
        const tag = match[1];
        const [depthToChild, prefix, suffix] = wrapMap[tag] || wrapMap._;
        element.innerHTML = prefix + html + suffix;
        for (let i = 0; i < depthToChild; i++) {
            element = element.lastChild;
        }
    } else {
        element.innerHTML = html;
    }
    return element.childNodes.length > 1
        ? element.childNodes
        : element.firstChild;
}

function getTemplate(templatePath) {
    if (!(templatePath in templates)) {
        throw `Missing template: ${templatePath}`;
    }
    const templateFactory = templates[templatePath];
    return (ctx) => {
        if (!ctx) {
            ctx = {};
        }
        Object.assign(ctx, {
            getPostUrl: getPostUrl,
            getPostEditUrl: getPostEditUrl,
            makeRelativeTime: makeRelativeTime,
            makeFileSize: makeFileSize,
            makeMarkdown: makeMarkdown,
            makeThumbnail: makeThumbnail,
            makeRadio: makeRadio,
            makeCheckbox: makeCheckbox,
            makeSelect: makeSelect,
            makeInput: makeInput,
            makeButton: makeButton,
            makeTextarea: makeTextarea,
            makeTextInput: makeTextInput,
            makePasswordInput: makePasswordInput,
            makeEmailInput: makeEmailInput,
            makeColorInput: makeColorInput,
            makeDateInput: makeDateInput,
            makePostLink: makePostLink,
            makeTagLink: makeTagLink,
            makePoolLink: makePoolLink,
            makeUserLink: makeUserLink,
            makeFlexboxAlign: makeFlexboxAlign,
            makeAccessKey: makeAccessKey,
            makeElement: makeElement,
            makeCssName: misc.makeCssName,
            makeNumericInput: makeNumericInput,
            formatClientLink: uri.formatClientLink,
        });
        return htmlToDom(templateFactory(ctx));
    };
}

function decorateValidator(form) {
    // postpone showing form fields validity until user actually tries
    // to submit it (seeing red/green form w/o doing anything breaks POLA)
    let submitButton = form.querySelector(".buttons input");
    if (!submitButton) {
        submitButton = form.querySelector("input[type=submit]");
    }
    if (submitButton) {
        submitButton.addEventListener("click", (e) => {
            form.classList.add("show-validation");
        });
    }
    form.addEventListener("submit", (e) => {
        form.classList.remove("show-validation");
    });
}

function disableForm(form) {
    for (let input of form.querySelectorAll("input")) {
        input.disabled = true;
    }
}

function enableForm(form) {
    for (let input of form.querySelectorAll("input")) {
        input.disabled = false;
    }
}

function syncScrollPosition() {
    window.requestAnimationFrame(() => {
        if (
            history.state &&
            Object.prototype.hasOwnProperty.call(history.state, "scrollX")
        ) {
            window.scrollTo(history.state.scrollX, history.state.scrollY);
        } else {
            window.scrollTo(0, 0);
        }
    });
}

function slideDown(element) {
    const duration = 500;
    return new Promise((resolve, reject) => {
        const height = element.getBoundingClientRect().height;
        element.style.maxHeight = "0";
        element.style.overflow = "hidden";
        window.setTimeout(() => {
            element.style.transition = `all ${duration}ms ease`;
            element.style.maxHeight = `${height}px`;
        }, 50);
        window.setTimeout(() => {
            resolve();
        }, duration);
    });
}

function slideUp(element) {
    const duration = 500;
    return new Promise((resolve, reject) => {
        const height = element.getBoundingClientRect().height;
        element.style.overflow = "hidden";
        element.style.maxHeight = `${height}px`;
        element.style.transition = `all ${duration}ms ease`;
        window.setTimeout(() => {
            element.style.maxHeight = 0;
        }, 10);
        window.setTimeout(() => {
            resolve();
        }, duration);
    });
}

function monitorNodeRemoval(monitoredNode, callback) {
    const mutationObserver = new MutationObserver((mutations) => {
        for (let mutation of mutations) {
            for (let node of mutation.removedNodes) {
                if (node.contains(monitoredNode)) {
                    mutationObserver.disconnect();
                    callback();
                    return;
                }
            }
        }
    });
    mutationObserver.observe(document.body, {
        childList: true,
        subtree: true,
    });
}

document.addEventListener("input", (e) => {
    if (e.target.classList.contains("color")) {
        let bkNode = e.target.parentNode.querySelector(".background-preview");
        let textNode = e.target.parentNode.querySelector(".text-preview");
        bkNode.style.backgroundColor = e.target.value;
        bkNode.style.borderColor = e.target.value;
        textNode.style.color = e.target.value;
        textNode.style.borderColor = e.target.value;
    }
});

// prevent opening buttons in new tabs
document.addEventListener("click", (e) => {
    if (e.target.getAttribute("href") === "" && e.which === 2) {
        e.preventDefault();
    }
});

module.exports = {
    htmlToDom: htmlToDom,
    getTemplate: getTemplate,
    emptyContent: emptyContent,
    replaceContent: replaceContent,
    enableForm: enableForm,
    disableForm: disableForm,
    decorateValidator: decorateValidator,
    makeTagLink: makeTagLink,
    makePostLink: makePostLink,
    makePoolLink: makePoolLink,
    makeCheckbox: makeCheckbox,
    makeRadio: makeRadio,
    syncScrollPosition: syncScrollPosition,
    slideDown: slideDown,
    slideUp: slideUp,
    monitorNodeRemoval: monitorNodeRemoval,
    clearMessages: clearMessages,
    appendExclamationMark: appendExclamationMark,
    showError: showError,
    showSuccess: showSuccess,
    showInfo: showInfo,
};