import { Dict } from "src/util";

const VERSION = "0.4.0";

export interface BBCodeConfig {
    showQuotePrefix?: boolean;
    classPrefix?: string;
    mentionPrefix?: string;
}

// default options
const defaults: BBCodeConfig = {
    showQuotePrefix: true,
    classPrefix: "bbcode_",
    mentionPrefix: "@"
};

export const version = VERSION;

// copied from here:
// http://blog.mattheworiordan.com/post/13174566389/url-regular-expression-for-links-with-or-without-the had to make an
// update to allow / in the query string, since some sites will have a / there made another update to support colons in
// the query string made another update to disallow an ending dot(.)
const URL_PATTERN = new RegExp("(" // overall match
    + "(" // brackets covering match for protocol (optional) and domain
    + "([A-Za-z]{3,9}:(?:\\/\\/)?)" // allow something@ for email addresses
    + "(?:[\\-;:&=\\+\\$,\\w]+@)?[A-Za-z0-9\\.\\-]+[A-Za-z0-9\\-]"
    // anything looking at all like a domain, non-unicode domains
    + "|" // or instead of above
    + "(?:www\\.|[\\-;:&=\\+\\$,\\w]+@)" // starting with something@ or www.
    + "[A-Za-z0-9\\.\\-]+[A-Za-z0-9\\-]" // anything looking at all like a domain
    + ")" // end protocol/domain
    + "(" // brackets covering match for path, query string and anchor
    + "(?:\\/[\\+~%\\/\\.\\w\\-_]*)?" // allow optional /path
    + "\\??(?:[\\-\\+=&;%@\\.\\w_\\/:]*)" // allow optional query string starting with ?
    + "#?(?:[\\.\\!\\/\\\\\\w]*)" // allow optional anchor #anchor
    + ")?" // make URL suffix optional
    + ")");

function doReplace(content: string, matches: Replacement[], options: BBCodeConfig) {
    let i, obj, regex, hasMatch, tmp;
    // match/replace until we don't change the input anymore
    do {
        hasMatch = false;
        for (i = 0; i < matches.length; ++i) {
            obj = matches[i];
            regex = new RegExp(obj.e, "gi");
            tmp = content.replace(regex, obj.func.bind(undefined, options));
            if (tmp !== content) {
                content = tmp;
                hasMatch = true;
            }
        }
    } while (hasMatch);
    return content;
}

function listItemReplace(options: BBCodeConfig, fullMatch: string, tag: string, value: string) {
    return "<li>" + doReplace(value.trim(), BBCODE_PATTERN, options) + "</li>";
}

export function extractQuotedText(value: string, parts?: string[]): (string | string[] | undefined)[] {
    const quotes = ["\"", "'"];
    let i, quote, nextPart;

    for (i = 0; i < quotes.length; ++i) {
        quote = quotes[i];
        if (value && value[0] === quote) {
            value = value.slice(1);
            if (value[value.length - 1] !== quote) {
                while (parts && parts.length) {
                    nextPart = parts.shift();
                    if (!nextPart)
                        continue;
                    value += " " + nextPart;
                    if (nextPart[nextPart.length - 1] === quote) {
                        break;
                    }
                }
            }
            value = value.replace(new RegExp("[" + quote + "]+$"), "");
            break;
        }
    }
    return [value, parts];
}

export function parseParams(tagName: string, params?: string): Dict<string> {
    const paramMap: Dict<string> = {};

    if (!params) {
        return paramMap;
    }

    // first, collapse spaces next to equals
    params = params.replace(/\s*[=]\s*/g, "=");
    let parts = params.split(/\s+/);

    while (parts.length) {
        const part = parts.shift() ?? "";
        // check if the param itself is a valid url
        if (!URL_PATTERN.exec(part)) {
            const index = part.indexOf("=");
            if (index > 0) {
                const rv = extractQuotedText(part.slice(index + 1), parts);
                paramMap[part.slice(0, index).toLowerCase()] = rv[0] as string;
                parts = rv[1] as string[];
            }
            else {
                const rv = extractQuotedText(part, parts);
                paramMap[tagName] = rv[0] as string;
                parts = rv[1] as string[];
            }
        } else {
            const rv = extractQuotedText(part, parts);
            paramMap[tagName] = rv[0] as string;
            parts = rv[1] as string[];
        }
    }
    return paramMap;
}

const BBCODE_PATTERN = [{ e: "\\[(\\w+)(?:[= ]([^\\]]+))?]((?:.|[\r\n])*?)\\[/\\1]", func: tagReplace }];

function tagReplace(options: BBCodeConfig, fullMatch: string, tag: string, params: string | undefined, value: string) {
    let val: string;
    tag = tag.toLowerCase();
    const paramsObj = parseParams(tag, params || undefined);
    let inlineValue = paramsObj[tag];

    switch (tag) {
    case "attach":
        return "";
    case "spoiler":
        return "";
    case "center":
        return doReplace(value, BBCODE_PATTERN, options);
    case "size":
        return doReplace(value, BBCODE_PATTERN, options);
    case "quote":
        val = "<div class=\"" + options.classPrefix + "quote\"";
        for (const i in paramsObj) {
            const tmp = paramsObj[i];
            if (!inlineValue && (i === "author" || i === "name")) {
                inlineValue = tmp;
            } else if (i !== tag) {
                val += " data-" + i + "=\"" + tmp + "\"";
            }
        }
        return val + ">" + (inlineValue ? inlineValue + " wrote:" : (options.showQuotePrefix ? "Quote:" : "")) + "<blockquote>" + value + "</blockquote></div>";
    case "url":
        return "<a class=\"" + options.classPrefix + "link\" target=\"_blank\" href=\"" + (inlineValue || value) + "\">" + value + "</a>";
    case "email":
        return "<a class=\"" + options.classPrefix + "link\" target=\"_blank\" href=\"mailto:" + (inlineValue || value) + "\">" + value + "</a>";
    case "anchor":
        return "<a name=\"" + (inlineValue || paramsObj.a || value) + "\">" + value + "</a>";
    case "b":
        return "<strong>" + value + "</strong>";
    case "i":
        return "<em>" + value + "</em>";
    case "u":
        return "<span style=\"text-decoration:underline\">" + value + "</span>";
    case "s":
        return "<span style=\"text-decoration:line-through\">" + value + "</span>";
    case "indent":
        return "<blockquote>" + value + "</blockquote>";
    case "list": {
        tag = "ul";
        let className = options.classPrefix + "list";
        if (inlineValue && /[1Aa]/.test(inlineValue)) {
            tag = "ol";
            if (/1/.test(inlineValue)) {
                className += "_numeric";
            }
            else if (/A/.test(inlineValue)) {
                className += "_alpha";
            }
            else if (/a/.test(inlineValue)) {
                className += "_alpha_lower";
            }
        }
        val = "<" + tag + " class=\"" + className + "\">";
        //  parse the value
        val += doReplace(value, [{ e: "\\[([*])\\]([^\r\n]+)", func: listItemReplace }], options);
        return val + "</" + tag + ">"; 
    }
    case "code":
    case "php":
    case "java":
    case "javascript":
    case "cpp":
    case "ruby":
    case "python":
        return "<pre class=\"" + options.classPrefix + (tag === "code" ? "" : "code_") + tag + "\">" + value + "</pre>";
    case "highlight":
        return "<span class=\"" + options.classPrefix + tag + "\">" + value + "</span>";
    case "html":
        return value;
    case "mention":
        val = "<span class=\"" + options.classPrefix + "mention\"";
        if (inlineValue) {
            val += " data-mention-id=\"" + inlineValue + "\"";
        }
        return val + ">" + (options.mentionPrefix || "") + value + "</span>";
    case "span":
    case "h1":
    case "h2":
    case "h3":
    case "h4":
    case "h5":
    case "h6":
        return "<" + tag + ">" + value + "</" + tag + ">";
    case "youtube":
        return "<object class=\"" + options.classPrefix + "video\" width=\"425\" height=\"350\"><param name=\"movie\" value=\"http://www.youtube.com/v/" + value + "\"></param><embed src=\"http://www.youtube.com/v/" + value + "\" type=\"application/x-shockwave-flash\" width=\"425\" height=\"350\"></embed></object>";
    case "gvideo":
        return "<embed class=\"" + options.classPrefix + "video\" style=\"width:400px; height:325px;\" id=\"VideoPlayback\" type=\"application/x-shockwave-flash\" src=\"http://video.google.com/googleplayer.swf?docId=" + value + "&amp;hl=en\">";
    case "google":
        return "<a class=\"" + options.classPrefix + "link\" target=\"_blank\" href=\"http://www.google.com/search?q=" + (inlineValue || value) + "\">" + value + "</a>";
    case "wikipedia":
        return "<a class=\"" + options.classPrefix + "link\" target=\"_blank\" href=\"http://www.wikipedia.org/wiki/" + (inlineValue || value) + "\">" + value + "</a>";
    case "img": {
        let dims = new RegExp("^(\\d+)x(\\d+)$").exec(inlineValue || "");
        if (!dims || (dims.length !== 3)) {
            dims = new RegExp("^width=(\\d+)\\s+height=(\\d+)$").exec(inlineValue || "");
        }
        if (dims && dims.length === 3) {
            params = undefined;
        }
        val = "<img class=\"" + options.classPrefix + "image\" src=\"" + value + "\"";
        if (dims && dims.length === 3) {
            val += " width=\"" + dims[1] + "\" height=\"" + dims[2] + "\"";
        } else {
            for (let i in paramsObj) {
                const tmp = paramsObj[i];
                if (i === "img") {
                    i = "alt";
                }
                val += " " + i + "=\"" + tmp + "\"";
            }
        }
        return val + "/>";
    }
    }
    // return the original
    return fullMatch;
}

interface Replacement {
    e: string;
    func: (options: BBCodeConfig, fullMatch: string, tag: string, params: string, value: string) => string;
}

/**
 * Renders the content as html
 * @param content   the given content to render
 * @param options   optional object with control parameters
 * @returns rendered html
 */
export function render(content: string, options?: BBCodeConfig): string {
    options = options || {};

    if (!options.classPrefix)
        options.classPrefix = defaults.classPrefix;
    if (!options.mentionPrefix)
        options.mentionPrefix = defaults.mentionPrefix;
    if (!options.showQuotePrefix)
        options.showQuotePrefix = defaults.showQuotePrefix;

    // for now, only one rule
    return doReplace(content, BBCODE_PATTERN, options);
}
