import _componentsLintJsx from "./components/lint.jsx";
import _utilJs from "./util.js";
import _reactComponentsTexJsx from "react-components/tex.jsx";
import _simpleMarkdown from "simple-markdown";
import _underscore from "underscore";
import React from "react";

var _module_ = {
    exports: {}
};

var exports = _module_.exports;
/* eslint-disable no-var, object-curly-spacing */
/* TODO(csilvers): fix these lint errors (http://eslint.org/docs/rules): */
/* To fix, remove an entry above, run ka-lint, and fix errors. */

/* globals KA */
var _ = _underscore;

var SimpleMarkdown = _simpleMarkdown;
var TeX = _reactComponentsTexJsx;
var Util = _utilJs;
const Lint = _componentsLintJsx;

/**
 * This match function matches math in `$`s, such as:
 *
 * $y = x + 1$
 *
 * It functions roughly like the following regex:
 * /\$([^\$]*)\$/
 *
 * Unfortunately, math may have other `$`s inside it, as
 * long as they are inside `{` braces `}`, mostly for
 * `\text{ $math$ }`.
 *
 * To parse this, we can't use a regex, since we
 * should support arbitrary nesting (even though
 * MathJax actually only supports two levels of nesting
 * here, which we *could* parse with a regex).
 *
 * Non-regex matchers like this are now a first-class
 * concept in simple-markdown. Yay!
 *
 * This can also match block-math, which is math alone in a paragraph.
 */
var mathMatcher = (source, state, isBlock) => {
    var length = source.length;
    var index = 0;

    // When looking for blocks, skip over leading spaces
    if (isBlock) {
        if (state.inline) {
            return null;
        }
        while (index < length && source[index] === " ") {
            index++;
        }
    }

    // Our source must start with a "$"
    if (!(index < length && source[index] === "$")) {
        return null;
    }

    index++;
    var startIndex = index;
    var braceLevel = 0;

    // Loop through the source, looking for a closing '$'
    // closing '$'s only count if they are not escaped with
    // a `\`, and we are not in nested `{}` braces.
    while (index < length) {
        var character = source[index];

        if (character === "\\") {
            // Consume both the `\` and the escaped char as a single
            // token.
            // This is so that the second `$` in `$\\$` closes
            // the math expression, since the first `\` is escaping
            // the second `\`, but the second `\` is not escaping
            // the second `$`.
            // This also handles the case of escaping `$`s or
            // braces `\{`
            index++;
        } else if (braceLevel <= 0 && character === "$") {
            var endIndex = index + 1;
            if (isBlock) {
                // Look for two trailing newlines after the closing `$`
                var match = /^(?: *\n){2,}/.exec(source.slice(endIndex));
                endIndex = match ? endIndex + match[0].length : null;
            }

            // Return an array that looks like the results of a
            // regex's .exec function:
            // capture[0] is the whole string
            // capture[1] is the first "paren" match, which is the
            //   content of the math here, as if we wrote the regex
            //   /\$([^\$]*)\$/
            if (endIndex) {
                return [
                    source.substring(0, endIndex),
                    source.substring(startIndex, index),
                ];
            }
            return null;
        } else if (character === "{") {
            braceLevel++;
        } else if (character === "}") {
            braceLevel--;
        } else if (character === "\n" && source[index - 1] === "\n") {
            // This is a weird case we supported in the old
            // math implementation--double newlines break
            // math. I'm preserving it for now because content
            // creators might have questions with single '$'s
            // in paragraphs...
            return null;
        }

        index++;
    }

    // we didn't find a closing `$`
    return null;
};
var mathMatch = (source, state) => mathMatcher(source, state, false);
var blockMathMatch = (source, state) => mathMatcher(source, state, true);

var TITLED_TABLE_REGEX = new RegExp(
    "^\\|\\| +(.*) +\\|\\| *\\n" +
        "(" +
        // The simple-markdown nptable regex, without
        // the leading `^`
        SimpleMarkdown.defaultRules.nptable.match.regex.source.substring(1) +
        ")"
);

var crowdinJiptMatcher = SimpleMarkdown.blockRegex(/^(crwdns.*)\n\s*\n/);

var rules = _.extend({}, SimpleMarkdown.defaultRules, {
    // NOTE: basically ignored by JIPT. wraps everything at the outer layer
    columns: {
        order: -2,
        match: SimpleMarkdown.blockRegex(/^([\s\S]*\n\n)={5,}\n\n([\s\S]*)/),
        parse: (capture, parse, state) => {
            return {
                col1: parse(capture[1], state),
                col2: parse(capture[2], state),
            };
        },
        react: (node, output, state) => {
            return (
                <div className="perseus-two-columns" key={state.key}>
                    <div className="perseus-column">
                        <div className="perseus-column-content">
                            {output(node.col1, state)}
                        </div>
                    </div>
                    <div className="perseus-column">
                        {/* HACK(#sat) This is a cheap way to allow a custom
                      * header to be displayed in two-column items in the SAT
                      * mission.
                      * The header will be rendered into this div. Do not write
                      * code outside of the SAT mission that relies on this
                      * because this will be cleaned up with other SAT
                      * technical debt. */}
                        <div className="sat-header-grafting-area" />

                        <div className="perseus-column-content">
                            {/* HACK(#sat) This is a cheap way to allow a
                          * custom review-mode skill box to be displayed in
                          * SAT.
                          * Don't write code outside of the SAT mission that
                          * relies on this div because we are still telling
                          * ourselves this will be cleaned up along with other
                          * SAT technical debt. */}
                            <div className="sat-skill-subscore-grafting-area" />
                            {output(node.col2, state)}
                            {/* HACK(#sat) This is a cheap way to allow hints
                          * to be displayed in two-column items in the SAT
                          * mission.
                          * The hint renderer will be rendered into this div.
                          * Do not write code outside of the SAT mission that
                          * relies on this because this will be cleaned up with
                          * other SAT technical debt. */}
                            <div className="sat-grafting-area" />
                        </div>
                    </div>
                </div>
            );
        },
    },
    // Match paragraphs consisting solely of crowdin IDs
    // (they look roughly like crwdns9238932:0), which means that
    // crowdin is going to take the DOM node that ID is rendered into
    // and count it as the top-level translation node. They mutate this
    // node, so we need to make sure it is an outer node, not an inner
    // span. So here we parse this separately and just output the
    // raw string, which becomes the body of the <QuestionParagraph>
    // created by the Renderer.
    // This currently (2015-09-01) affects only articles, since
    // for exercises the renderer just renders the crowdin id to the
    // renderer div.
    crowdinId: {
        order: -1,
        match: (source, state, prevCapture) => {
            // Only match on the just-in-place translation site
            if (state.isJipt) {
                return crowdinJiptMatcher(source, state, prevCapture);
            } else {
                return null;
            }
        },
        parse: (capture, parse, state) => ({
            id: capture[1]
        }),
        react: (node, output, state) => node.id,
    },
    // This is pretty much horrible, but we have a regex here to capture an
    // entire table + a title. capture[1] is the title. capture[2] of the
    // regex is a copy of the simple-markdown nptable regex. Then we turn
    // our capture[2] into tableCapture[0], and any further captures in
    // our table regex into tableCapture[1..], and we pass tableCapture to
    // our nptable regex
    titledTable: {
        // process immediately before nptables
        order: SimpleMarkdown.defaultRules.nptable.order - 0.5,
        match: SimpleMarkdown.blockRegex(TITLED_TABLE_REGEX),
        parse: (capture, parse, state) => {
            var title = SimpleMarkdown.parseInline(parse, capture[1], state);

            // Remove our [0] and [1] captures, and pass the rest to
            // the nptable parser
            var tableCapture = _.rest(capture, 2);
            var table = SimpleMarkdown.defaultRules.nptable.parse(
                tableCapture,
                parse,
                state
            );
            return {
                title: title,
                table: table,
            };
        },
        react: (node, output, state) => {
            let contents;
            if (!node.table) {
                contents = "//invalid table//";
            } else {
                const tableOutput = SimpleMarkdown.defaultRules.table.react(
                    node.table,
                    output,
                    state
                );

                const caption = (
                    <caption key="caption" className="perseus-table-title">
                        {output(node.title, state)}
                    </caption>
                );

                // Splice the caption into the table's children with the
                // caption as the first child.
                contents = React.cloneElement(tableOutput, null, [
                    caption,
                    ...tableOutput.props.children,
                ]);
            }

            // Note: if the DOM structure changes, edit the Zoomable wrapper
            // in src/renderer.jsx.
            return (
                <div className="perseus-titled-table" key={state.key}>
                    {contents}
                </div>
            );
        },
    },
    widget: {
        order: SimpleMarkdown.defaultRules.link.order - 0.75,
        match: SimpleMarkdown.inlineRegex(Util.rWidgetRule),
        parse: (capture, parse, state) => {
            return {
                id: capture[1],
                widgetType: capture[2],
            };
        },
        react: (node, output, state) => {
            // The actual output is handled in the renderer, where
            // we know the current widget props/state. This is
            // just a stub for testing.
            return (
                <em key={state.key}>
                    [Widget: {node.id}]
                </em>
            );
        },
    },
    blockMath: {
        order: SimpleMarkdown.defaultRules.codeBlock.order + 0.5,
        match: blockMathMatch,
        parse: (capture, parse, state) => {
            return {
                content: capture[1],
            };
        },
        react: (node, output, state) => {
            // The actual output is handled in the renderer, because
            // it needs to pass in an `onRender` callback prop. This
            // is just a stub for testing.
            return (
                <TeX key={state.key}>
                    {node.content}
                </TeX>
            );
        },
    },
    math: {
        order: SimpleMarkdown.defaultRules.link.order - 0.25,
        match: mathMatch,
        parse: (capture, parse, state) => {
            return {
                content: capture[1],
            };
        },
        react: (node, output, state) => {
            // The actual output is handled in the renderer, because
            // it needs to pass in an `onRender` callback prop. This
            // is just a stub for testing.
            return (
                <TeX key={state.key}>
                    {node.content}
                </TeX>
            );
        },
    },
    unescapedDollar: {
        order: SimpleMarkdown.defaultRules.link.order - 0.24,
        match: SimpleMarkdown.inlineRegex(/^(?!\\)\$/),
        parse: (capture, parse, state) => {
            return {};
        },
        react: (node, output, state) => {
            // Unescaped dollar signs render correctly, but result in
            // untranslatable text after the i18n python linter flags it
            return "$";
        },
    },
    fence: _.extend({}, SimpleMarkdown.defaultRules.fence, {
        parse: (capture, parse, state) => {
            var node = SimpleMarkdown.defaultRules.fence.parse(
                capture,
                parse,
                state
            );

            // support screenreader-only text with ```alt
            if (node.lang === "alt") {
                return {
                    type: "codeBlock",
                    lang: "alt",
                    // default codeBlock parsing doesn't parse the contents.
                    // We need to parse the contents for things like table
                    // support :).
                    // The \n\n is because the inside of the codeblock might
                    // not end in double newlines for block rules, because
                    // ordinarily we don't parse this :).
                    content: parse(node.content + "\n\n", state),
                };
            } else {
                return node;
            }
        },
    }),
    // Extend the SimpleMarkdown link parser to make the link
    // zero-rating-friendly if necessary. No changes will be made for
    // non-zero-rated requests, but zero-rated requests will be re-pointed at
    // either the zero-rated version of khanacademy.org or the external link
    // warning interstitial. We also replace the default <a /> tag with a custom
    // element, if necessary.
    link: _.extend({}, SimpleMarkdown.defaultRules.link, {
        react: function(node, output, state) {
            const link = SimpleMarkdown.defaultRules.link.react(
                node,
                output,
                state
            );

            let href = link.props.href;

            // TODO(charlie): Move this logic out of Perseus and into webapp via
            // the <Link /> component that is now injected as a dependency.
            if (typeof KA !== "undefined" && KA.isZeroRated) {
                if (href.match(/https?:\/\/[^\/]*khanacademy.org/)) {
                    href = href.replace(
                        "khanacademy.org",
                        "zero.khanacademy.org"
                    );
                } else {
                    href =
                        "/zero/external-link?url=" + encodeURIComponent(href);
                }
            }

            const newProps = {...link.props, href};

            if (state.baseElements && state.baseElements.Link) {
                return state.baseElements.Link(newProps);
            } else {
                return React.cloneElement(link, newProps);
            }
        },
    }),
    codeBlock: _.extend({}, SimpleMarkdown.defaultRules.codeBlock, {
        react: (node, output, state) => {
            // ideally this should be a different rule, with only an
            // output function, but right now that breaks the parser.
            if (node.lang === "alt") {
                return (
                    <div
                        key={state.key}
                        className="perseus-markdown-alt perseus-sr-only"
                    >
                        {output(node.content, state)}
                    </div>
                );
            } else {
                return SimpleMarkdown.defaultRules.codeBlock.react(
                    node,
                    output,
                    state
                );
            }
        },
    }),
    list: _.extend({}, SimpleMarkdown.defaultRules.list, {
        match: (source, state, prevCapture) => {
            // Since lists can contain double newlines and we have special
            // handling of double newlines while parsing jipt content, just
            // disable the list parser.
            if (state.isJipt) {
                return null;
            } else {
                return SimpleMarkdown.defaultRules.list.match(
                    source,
                    state,
                    prevCapture
                );
            }
        },
    }),
    // The lint rule never actually matches anything.
    // We check for lint after parsing, and, if we find any, we
    // transform the tree to add lint nodes. This rule is here
    // just for the react() function
    lint: {
        order: 1000,
        match: s => null,
        parse: (capture, parse, state) => ({}),
        react: (node, output, state) => {
            return (
                <Lint
                    message={node.message}
                    ruleName={node.ruleName}
                    inline={isInline(node.content)}
                    insideTable={node.insideTable}
                    severity={node.severity}
                >
                    {output(node.content, state)}
                </Lint>
            );
        },
    },
});

// Return true if the specified parse tree node represents inline content
// and false otherwise. We need this so that lint nodes can figure out whether
// they should behave as an inline wrapper or a block wrapper
function isInline(node) {
    return !!(node && node.type && inlineNodeTypes.hasOwnProperty(node.type));
}
const inlineNodeTypes = {
    text: true,
    math: true,
    unescapedDollar: true,
    link: true,
    img: true,
    strong: true,
    u: true,
    em: true,
    del: true,
    code: true,
};

var builtParser = SimpleMarkdown.parserFor(rules);
var parse = (source, state) => {
    var paragraphedSource = source + "\n\n";

    return builtParser(paragraphedSource, _.extend({inline: false}, state));
};
var inlineParser = (source, state) => {
    return builtParser(source, _.extend({inline: true}, state));
};

/**
 * Traverse all of the nodes in the Perseus Markdown AST. The callback is
 * called for each node in the AST.
 */
var traverseContent = (ast, cb) => {
    if (_.isArray(ast)) {
        _.each(ast, node => traverseContent(node, cb));
    } else if (_.isObject(ast)) {
        cb(ast);
        if (ast.type === "table") {
            traverseContent(ast.header, cb);
            traverseContent(ast.cells, cb);
        } else if (ast.type === "list") {
            traverseContent(ast.items, cb);
        } else if (ast.type === "titledTable") {
            traverseContent(ast.table, cb);
        } else if (ast.type === "columns") {
            traverseContent(ast.col1, cb);
            traverseContent(ast.col2, cb);
        } else if (_.isArray(ast.content)) {
            traverseContent(ast.content, cb);
        }
    }
};

/**
 * Pull out text content from a Perseus Markdown AST.
 * Returns an array of strings.
 */
var getContent = ast => {
    // Simplify logic by dealing with a single AST node at a time
    if (_.isArray(ast)) {
        return _.flatten(_.map(ast, getContent));
    }

    // Base case: This is where we actually extract text content
    if (ast.content && _.isString(ast.content)) {
        // Collapse whitespace within content unless it is code
        if (ast.type.toLowerCase().indexOf("code") !== -1) {
            // In case this is the sole child of a paragraph,
            // prevent whitespace from being trimmed later
            return ["", ast.content, ""];
        } else {
            return [ast.content.replace(/\s+/g, " ")];
        }
    }

    // Recurse through child AST nodes
    // Assumptions made:
    // 1) Child AST nodes are either direct properties or inside
    //    arbitrarily nested lists that are direct properties.
    // 2) Only AST nodes have a 'type' property.
    var children = _.chain(ast)
        .values()
        .flatten()
        .filter(object => object != null && _.has(object, "type"))
        .value();

    if (!children.length) {
        return [];
    } else {
        var nestedContent = getContent(children);
        if (ast.type === "paragraph" && nestedContent.length) {
            // Trim whitespace before or after a paragraph
            nestedContent[0] = nestedContent[0].replace(/^\s+/, "");
            var last = nestedContent.length - 1;
            nestedContent[last] = nestedContent[last].replace(/\s+$/, "");
        }
        return nestedContent;
    }
};

/**
 * Count the number of characters in Perseus Markdown source.
 * Markdown markup and widget references are ignored.
 */
var characterCount = source => {
    var ast = parse(source);
    var content = getContent(ast).join("");
    return content.length;
};

_module_.exports = {
    characterCount: characterCount,
    traverseContent: traverseContent,
    parse: parse,
    parseInline: inlineParser,
    reactFor: SimpleMarkdown.reactFor,
    ruleOutput: SimpleMarkdown.ruleOutput(rules, "react"),
    basicOutput: SimpleMarkdown.reactFor(
        SimpleMarkdown.ruleOutput(rules, "react")
    ),
    sanitizeUrl: SimpleMarkdown.sanitizeUrl,
};
export default _module_.exports;
