'use strict'
const cheerio = require('cheerio')
const _ = require('lodash')
const esprima = require('esprima')
const escodegen = require('escodegen')
const reactDOMSupport = require('./reactDOMSupport')
const reactNativeSupport = require('./reactNativeSupport')
const reactPropTemplates = require('./reactPropTemplates')
const rtError = require('./RTCodeError')
const reactSupport = require('./reactSupport')
const templates = reactSupport.templates
const utils = require('./utils')
const validateJS = utils.validateJS
const RTCodeError = rtError.RTCodeError

const repeatTemplate = _.template('_.map(<%= collection %>,<%= repeatFunction %>.bind(<%= repeatBinds %>))')
const ifTemplate = _.template('((<%= condition %>)?(<%= body %>):null)')
const propsTemplateSimple = _.template('_.assign({}, <%= generatedProps %>, <%= rtProps %>)')
const propsTemplate = _.template('mergeProps( <%= generatedProps %>, <%= rtProps %>)')

const propsMergeFunction = `function mergeProps(inline,external) {
    var res = _.assign({},inline,external)
    if (inline.hasOwnProperty('style')) {
        res.style = _.defaults(res.style, inline.style);
    }
    if (inline.hasOwnProperty('className') && external.hasOwnProperty('className')) {
        res.className = external.className + ' ' + inline.className;
    }
    return res;
}
`

const classSetTemplate = _.template('_.transform(<%= classSet %>, function(res, value, key){ if(value){ res.push(key); } }, []).join(" ")')

function getTagTemplateString(simpleTagTemplate, shouldCreateElement) {
    if (simpleTagTemplate) {
        return shouldCreateElement ? 'React.createElement(<%= name %>,<%= props %><%= children %>)' : '<%= name %>(<%= props %><%= children %>)'
    }
    return shouldCreateElement ? 'React.createElement.apply(this, [<%= name %>,<%= props %><%= children %>])' : '<%= name %>.apply(this, [<%= props %><%= children %>])'
}


const commentTemplate = _.template(' /* <%= data %> */ ')

const repeatAttr = 'rt-repeat'
const ifAttr = 'rt-if'
const classSetAttr = 'rt-class'
const classAttr = 'class'
const scopeAttr = 'rt-scope'
const propsAttr = 'rt-props'
const templateNode = 'rt-template'
const virtualNode = 'rt-virtual'
const includeNode = 'rt-include'
const includeSrcAttr = 'src'
const requireAttr = 'rt-require'
const importAttr = 'rt-import'
const statelessAttr = 'rt-stateless'
const preAttr = 'rt-pre'

const reactTemplatesSelfClosingTags = [includeNode]

/**
 * @param {Options} options
 * @return {Options}
 */
function getOptions(options) {
    options = options || {}
    const defaultOptions = {
        version: false,
        force: false,
        format: 'stylish',
        targetVersion: reactDOMSupport.default,
        lodashImportPath: 'lodash',
        native: false,
        nativeTargetVersion: reactNativeSupport.default
    }

    const finalOptions = _.defaults({}, options, defaultOptions)
    finalOptions.reactImportPath = reactImport(finalOptions)
    finalOptions.modules = finalOptions.modules || (finalOptions.native ? 'commonjs' : 'none')

    const defaultPropTemplates = finalOptions.native ?
        reactPropTemplates.native[finalOptions.nativeTargetVersion] :
        reactPropTemplates.dom[finalOptions.targetVersion]

    finalOptions.propTemplates = _.defaults({}, options.propTemplates, defaultPropTemplates)
    return finalOptions
}

function reactImport(options) {
    if (options.native) {
        return reactNativeSupport[options.nativeTargetVersion].react.module
    }
    if (!options.reactImportPath) {
        const isNewReact = _.includes(['0.14.0', '0.15.0', '15.0.0', '15.0.1'], options.targetVersion)
        return isNewReact ? 'react' : 'react/addons'
    }
    return options.reactImportPath
}

/**
 * @param {Context} context
 * @param {string} namePrefix
 * @param {string} body
 * @param {*?} params
 * @return {string}
 */
function generateInjectedFunc(context, namePrefix, body, params) {
    params = params || context.boundParams
    const funcName = namePrefix.replace(',', '') + (context.injectedFunctions.length + 1)
    const funcText = `function ${funcName}(${params.join(',')}) {
        ${body}
        }
        `
    context.injectedFunctions.push(funcText)
    return funcName
}

function generateTemplateProps(node, context) {
    let templatePropCount = 0
    const propTemplateDefinition = context.options.propTemplates[node.name]
    const propertiesTemplates = _(node.children)
        .map((child, index) => {
            let templateProp = null
            if (child.name === templateNode) { // Generic explicit template tag
                if (!_.has(child.attribs, 'prop')) {
                    throw RTCodeError.build(context, child, 'rt-template must have a prop attribute')
                }
                if (_.filter(child.children, {type: 'tag'}).length !== 1) {
                    throw RTCodeError.build(context, child, "'rt-template' should have a single non-text element as direct child")
                }

                const childTemplate = _.find(context.options.propTemplates, {prop: child.attribs.prop}) || {arguments: []}
                templateProp = {
                    prop: child.attribs.prop,
                    arguments: (child.attribs.arguments ? child.attribs.arguments.split(',') : childTemplate.arguments) || []
                }
            } else if (propTemplateDefinition && propTemplateDefinition[child.name]) { // Implicit child template from configuration
                templateProp = {
                    prop: propTemplateDefinition[child.name].prop,
                    arguments: child.attribs.arguments ? child.attribs.arguments.split(',') : propTemplateDefinition[child.name].arguments
                }
            }

            if (templateProp) {
                _.assign(templateProp, {childIndex: index - templatePropCount++, content: _.find(child.children, {type: 'tag'})})
            }

            return templateProp
        })
        .compact()
        .value()

    return _.transform(propertiesTemplates, (props, templateProp) => {
        const functionParams = _.values(context.boundParams).concat(templateProp.arguments)

        const oldBoundParams = context.boundParams
        context.boundParams = context.boundParams.concat(templateProp.arguments)

        const functionBody = 'return ' + convertHtmlToReact(templateProp.content, context)
        context.boundParams = oldBoundParams

        const generatedFuncName = generateInjectedFunc(context, templateProp.prop, functionBody, functionParams)
        props[templateProp.prop] = genBind(generatedFuncName, _.values(context.boundParams))

        // Remove the template child from the children definition.
        node.children.splice(templateProp.childIndex, 1)
    }, {})
}

/**
 * @param node
 * @param {Context} context
 * @return {string}
 */
function generateProps(node, context) {
    const props = {}
    _.forOwn(node.attribs, (val, key) => {
        const propKey = reactSupport.attributesMapping[key.toLowerCase()] || key
        if (props.hasOwnProperty(propKey) && propKey !== reactSupport.classNameProp) {
            throw RTCodeError.build(context, node, `duplicate definition of ${propKey} ${JSON.stringify(node.attribs)}`)
        }
        if (_.startsWith(key, 'on') && !utils.isStringOnlyCode(val)) {
            props[propKey] = handleEventHandler(val, context, node, key)
        } else if (key === 'style' && !utils.isStringOnlyCode(val)) {
            props[propKey] = handleStyleProp(val, node, context)
        } else if (propKey === reactSupport.classNameProp) {
            // Processing for both class and rt-class conveniently return strings that
            // represent JS expressions, each evaluating to a space-separated set of class names.
            // We can just join them with another space here.
            const existing = props[propKey] ? `${props[propKey]} + " " + ` : ''
            if (key === classSetAttr) {
                props[propKey] = existing + classSetTemplate({classSet: val})
            } else if (key === classAttr || key === reactSupport.classNameProp) {
                props[propKey] = existing + utils.convertText(node, context, val.trim())
            }
        } else if (!_.startsWith(key, 'rt-')) {
            props[propKey] = utils.convertText(node, context, val.trim())
        }
    })
    _.assign(props, generateTemplateProps(node, context))

    // map 'className' back into 'class' for custom elements
    if (props[reactSupport.classNameProp] && isCustomElement(node.name)) {
        props[classAttr] = props[reactSupport.classNameProp]
        delete props[reactSupport.classNameProp]
    }

    const propStr = _.map(props, (v, k) => `${JSON.stringify(k)} : ${v}`).join(',')
    return `{${propStr}}`
}

function handleEventHandler(val, context, node, key) {
    let handlerString
    if (_.startsWith(val, 'this.')) {
        if (context.options.autobind) {
            handlerString = `${val}.bind(this)`
        } else {
            throw RTCodeError.build(context, node, "'this.handler' syntax allowed only when the --autobind is on, use {} to return a callback function.")
        }
    } else {
        const funcParts = val.split('=>')
        if (funcParts.length !== 2) {
            throw RTCodeError.build(context, node, `when using 'on' events, use lambda '(p1,p2)=>body' notation or 'this.handler'; otherwise use {} to return a callback function. error: [${key}='${val}']`)
        }
        const evtParams = funcParts[0].replace('(', '').replace(')', '').trim()
        const funcBody = funcParts[1].trim()
        let params = context.boundParams
        if (evtParams.trim() !== '') {
            params = params.concat([evtParams.trim()])
        }
        const generatedFuncName = generateInjectedFunc(context, key, funcBody, params)
        handlerString = genBind(generatedFuncName, context.boundParams)
    }
    return handlerString
}

function genBind(func, args) {
    const bindArgs = ['this'].concat(args)
    return `${func}.bind(${bindArgs.join(',')})`
}

function handleStyleProp(val, node, context) {
    const styleStr = _(val)
        .split(';')
        .map(_.trim)
        .filter(i => _.includes(i, ':'))
        .map(i => {
            const pair = i.split(':')
            const key = pair[0].trim()
            if (/\{|}/g.test(key)) {
                throw RTCodeError.build(context, node, 'style attribute keys cannot contain { } expressions')
            }
            const value = pair.slice(1).join(':').trim()
            const parsedKey = /(^-moz-)|(^-o-)|(^-webkit-)/ig.test(key) ? _.upperFirst(_.camelCase(key)) : _.camelCase(key)
            return parsedKey + ' : ' + utils.convertText(node, context, value.trim())
        })
        .join(',')
    return `{${styleStr}}`
}

/**
 * @param {string} tagName
 * @param context
 * @return {string}
 */
function convertTagNameToConstructor(tagName, context) {
    if (context.options.native) {
        const targetSupport = reactNativeSupport[context.options.nativeTargetVersion]
        return _.includes(targetSupport.components, tagName) ? `${targetSupport.reactNative.name}.${tagName}` : tagName
    }
    let isHtmlTag = _.includes(reactDOMSupport[context.options.targetVersion], tagName) || isCustomElement(tagName)
    if (reactSupport.shouldUseCreateElement(context)) {
        isHtmlTag = isHtmlTag || tagName.match(/^\w+(-\w+)+$/)
        return isHtmlTag ? `'${tagName}'` : tagName
    }
    return isHtmlTag ? `React.DOM.${tagName}` : tagName
}

function isCustomElement(tagName) {
    return tagName.match(/^\w+(-\w+)+$/)
}

/**
 * @param {string} html
 * @param options
 * @param reportContext
 * @return {Context}
 */
function defaultContext(html, options, reportContext) {
    const defaultDefines = [
        {moduleName: options.reactImportPath, alias: 'React', member: '*'},
        {moduleName: options.lodashImportPath, alias: '_', member: '*'}
    ]
    if (options.native) {
        const targetSupport = reactNativeSupport[options.nativeTargetVersion]
        if (targetSupport.reactNative.module !== targetSupport.react.module) {
            defaultDefines.splice(0, 0, {moduleName: targetSupport.reactNative.module, alias: targetSupport.reactNative.name, member: '*'})
        }
    }
    return {
        boundParams: [],
        injectedFunctions: [],
        html,
        options,
        defines: options.defines ? _.clone(options.defines) : defaultDefines,
        reportContext
    }
}

/**
 * @param node
 * @return {boolean}
 */
function hasNonSimpleChildren(node) {
    return _.some(node.children, child => child.type === 'tag' && child.attribs[repeatAttr])
}

/**
 * Trims a string the same way as String.prototype.trim(), but preserving all non breaking spaces ('\xA0')
 * @param {string} text
 * @return {string}
 */
function trimHtmlText(text) {
    return text.replace(/^[ \f\n\r\t\v\u1680\u180e\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff]+|[ \f\n\r\t\v\u1680\u180e\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff]+$/g, '')
}

/**
 * @param node
 * @param {Context} context
 * @return {string}
 */
function convertHtmlToReact(node, context) {
    if (node.type === 'tag' || node.type === 'style') {
        context = _.defaults({
            boundParams: _.clone(context.boundParams)
        }, context)

        if (node.type === 'tag' && node.name === importAttr) {
            throw RTCodeError.build(context, node, "'rt-import' must be a toplevel node")
        }

        if (node.type === 'tag' && node.name === includeNode) {
            const srcFile = node.attribs[includeSrcAttr]
            if (!srcFile) {
                throw RTCodeError.build(context, node, 'rt-include must supply a source attribute')
            }
            if (!context.options.readFileSync) {
                throw RTCodeError.build(context, node, 'rt-include needs a readFileSync polyfill on options')
            }
            try {
                context.html = context.options.readFileSync(srcFile)
            } catch (e) {
                console.error(e)
                throw RTCodeError.build(context, node, `rt-include failed to read file '${srcFile}'`)
            }
            return parseAndConvertHtmlToReact(context.html, context)
        }

        const data = {name: convertTagNameToConstructor(node.name, context)}

        // Order matters. We need to add the item and itemIndex to context.boundParams before
        // the rt-scope directive is processed, lest they are not passed to the child scopes
        if (node.attribs[repeatAttr]) {
            const arr = node.attribs[repeatAttr].split(' in ')
            if (arr.length !== 2) {
                throw RTCodeError.build(context, node, `rt-repeat invalid 'in' expression '${node.attribs[repeatAttr]}'`)
            }
            const repeaterParams = arr[0].split(',').map(s => s.trim())
            data.item = repeaterParams[0]
            data.index = repeaterParams[1] || `${data.item}Index`
            data.collection = arr[1].trim()
            const bindParams = [data.item, data.index]
            _.forEach(bindParams, param => {
                validateJS(param, node, context)
            })
            validateJS(`(${data.collection})`, node, context)
            _.forEach(bindParams, param => {
                if (!_.includes(context.boundParams, param)) {
                    context.boundParams.push(param)
                }
            })
        }

        if (node.attribs[scopeAttr]) {
            handleScopeAttribute(node, context, data)
        }

        if (node.attribs[ifAttr]) {
            validateIfAttribute(node, context, data)
            data.condition = node.attribs[ifAttr].trim()
            if (!node.attribs.key && node.name !== virtualNode) {
                _.set(node, ['attribs', 'key'], `${node.startIndex}`)
            }
        }

        data.props = generateProps(node, context)
        if (node.attribs[propsAttr]) {
            if (data.props === '{}') {
                data.props = node.attribs[propsAttr]
            } else if (!node.attribs.style && !node.attribs.class) {
                data.props = propsTemplateSimple({generatedProps: data.props, rtProps: node.attribs[propsAttr]})
            } else {
                data.props = propsTemplate({generatedProps: data.props, rtProps: node.attribs[propsAttr]})
                if (!_.includes(context.injectedFunctions, propsMergeFunction)) {
                    context.injectedFunctions.push(propsMergeFunction)
                }
            }
        }

        if (node.name === virtualNode) {
            const invalidAttributes = _.without(_.keys(node.attribs), scopeAttr, ifAttr, repeatAttr)
            if (invalidAttributes.length > 0) {
                throw RTCodeError.build(context, node, "<rt-virtual> may not contain attributes other than 'rt-scope', 'rt-if' and 'rt-repeat'")
            }

            // provide a key to virtual node children if missing
            if (node.children.length > 1) {
                _(node.children)
                    .reject('attribs.key')
                    .forEach((child, i) => {
                        if (child.type === 'tag' && child.name !== virtualNode) {
                            _.set(child, ['attribs', 'key'], `${node.startIndex}${i}`)
                        }
                    })
            }
        }

        const children = _.map(node.children, child => {
            const code = convertHtmlToReact(child, context)
            validateJS(code, child, context)
            return code
        })

        data.children = utils.concatChildren(children)

        if (node.name === virtualNode) { //eslint-disable-line wix-editor/prefer-ternary
            data.body = `[${_.compact(children).join(',')}]`
        } else {
            data.body = _.template(getTagTemplateString(!hasNonSimpleChildren(node), reactSupport.shouldUseCreateElement(context)))(data)
        }

        if (node.attribs[scopeAttr]) {
            const functionBody = _.values(data.innerScope.innerMapping).join('\n') + `return ${data.body}`
            const generatedFuncName = generateInjectedFunc(context, 'scope' + data.innerScope.scopeName, functionBody, _.keys(data.innerScope.outerMapping))
            data.body = `${generatedFuncName}.apply(this, [${_.values(data.innerScope.outerMapping).join(',')}])`
        }

        // Order matters here. Each rt-repeat iteration wraps over the rt-scope, so
        // the scope variables are evaluated in context of the current iteration.
        if (node.attribs[repeatAttr]) {
            data.repeatFunction = generateInjectedFunc(context, 'repeat' + _.upperFirst(data.item), 'return ' + data.body)
            data.repeatBinds = ['this'].concat(_.reject(context.boundParams, p => p === data.item || p === data.index || data.innerScope && p in data.innerScope.innerMapping))
            data.body = repeatTemplate(data)
        }
        if (node.attribs[ifAttr]) {
            data.body = ifTemplate(data)
        }
        return data.body
    } else if (node.type === 'comment') {
        const sanitizedComment = node.data.split('*/').join('* /')
        return commentTemplate({data: sanitizedComment})
    } else if (node.type === 'text') {
        const parentNode = node.parent
        const overrideNormalize = parentNode !== undefined && (parentNode.name === 'pre' || parentNode.name === 'textarea' || _.has(parentNode.attribs, preAttr))
        const normalizeWhitespaces = context.options.normalizeHtmlWhitespace && !overrideNormalize
        const text = node.data
        return trimHtmlText(text) ? utils.convertText(node, context, text, normalizeWhitespaces) : ''
    }
}

/**
 * Parses the rt-scope attribute returning an array of parsed sections
 *
 * @param {String} scope The scope attribute to parse
 * @returns {Array} an array of {expression,identifier}
 * @throws {String} the part of the string that failed to parse
 */
function parseScopeSyntax(text) {
    // the regex below was built using the following pseudo-code:
    // double_quoted_string = `"[^"\\\\]*(?:\\\\.[^"\\\\]*)*"`
    // single_quoted_string = `'[^'\\\\]*(?:\\\\.[^'\\\\]*)*'`
    // text_out_of_quotes = `[^"']*?`
    // expr_parts = double_quoted_string + "|" + single_quoted_string + "|" + text_out_of_quotes
    // expression = zeroOrMore(nonCapture(expr_parts)) + "?"
    // id = "[$_a-zA-Z]+[$_a-zA-Z0-9]*"
    // as = " as" + OneOrMore(" ")
    // optional_spaces = zeroOrMore(" ")
    // semicolon = nonCapture(or(text(";"), "$"))
    //
    // regex = capture(expression) + as + capture(id) + optional_spaces + semicolon + optional_spaces

    const regex = RegExp("((?:(?:\"[^\"\\\\]*(?:\\\\.[^\"\\\\]*)*\"|'[^'\\\\]*(?:\\\\.[^'\\\\]*)*'|[^\"']*?))*?) as(?: )+([$_a-zA-Z]+[$_a-zA-Z0-9]*)(?: )*(?:;|$)(?: )*", 'g')
    const res = []
    do {
        const idx = regex.lastIndex
        const match = regex.exec(text)
        if (regex.lastIndex === idx || match === null) {
            throw text.substr(idx)
        }
        if (match.index === regex.lastIndex) {
            regex.lastIndex++
        }
        res.push({expression: match[1].trim(), identifier: match[2]})
    } while (regex.lastIndex < text.length)

    return res
}

function handleScopeAttribute(node, context, data) {
    data.innerScope = {
        scopeName: '',
        innerMapping: {},
        outerMapping: {}
    }

    data.innerScope.outerMapping = _.zipObject(context.boundParams, context.boundParams)

    let scopes
    try {
        scopes = parseScopeSyntax(node.attribs[scopeAttr])
    } catch (scopePart) {
        throw RTCodeError.build(context, node, `invalid scope part '${scopePart}'`)
    }

    scopes.forEach(({expression, identifier}) => {
        validateJS(identifier, node, context)

        // this adds both parameters to the list of parameters passed further down
        // the scope chain, as well as variables that are locally bound before any
        // function call, as with the ones we generate for rt-scope.
        if (!_.includes(context.boundParams, identifier)) {
            context.boundParams.push(identifier)
        }

        data.innerScope.scopeName += _.upperFirst(identifier)
        data.innerScope.innerMapping[identifier] = `var ${identifier} = ${expression};`
        validateJS(data.innerScope.innerMapping[identifier], node, context)
    })
}

function validateIfAttribute(node, context, data) {
    const innerMappingKeys = _.keys(data.innerScope && data.innerScope.innerMapping || {})
    let ifAttributeTree = null
    try {
        ifAttributeTree = esprima.parse(node.attribs[ifAttr])
    } catch (e) {
        throw new RTCodeError(e.message, e.index, -1)
    }
    if (ifAttributeTree && ifAttributeTree.body && ifAttributeTree.body.length === 1 && ifAttributeTree.body[0].type === 'ExpressionStatement') {
        // make sure that rt-if does not use an inner mapping
        if (ifAttributeTree.body[0].expression && utils.usesScopeName(innerMappingKeys, ifAttributeTree.body[0].expression)) {
            throw RTCodeError.buildFormat(context, node, "invalid scope mapping used in if part '%s'", node.attribs[ifAttr])
        }
    } else {
        throw RTCodeError.buildFormat(context, node, "invalid if part '%s'", node.attribs[ifAttr])
    }
}

function handleSelfClosingHtmlTags(nodes) {
    return _.flatMap(nodes, node => {
        let externalNodes = []
        node.children = handleSelfClosingHtmlTags(node.children)
        if (node.type === 'tag' && (_.includes(reactSupport.htmlSelfClosingTags, node.name) ||
            _.includes(reactTemplatesSelfClosingTags, node.name))) {
            externalNodes = _.filter(node.children, {type: 'tag'})
            _.forEach(externalNodes, i => {i.parent = node})
            node.children = _.reject(node.children, {type: 'tag'})
        }
        return [node].concat(externalNodes)
    })
}

function handleRequire(tag, context) {
    let moduleName
    let alias
    let member
    if (tag.children.length) {
        throw RTCodeError.build(context, tag, `'${requireAttr}' may have no children`)
    } else if (tag.attribs.dependency && tag.attribs.as) {
        moduleName = tag.attribs.dependency
        member = '*'
        alias = tag.attribs.as
    }
    if (!moduleName) {
        throw RTCodeError.build(context, tag, `'${requireAttr}' needs 'dependency' and 'as' attributes`)
    }
    context.defines.push({moduleName, member, alias})
}

function handleImport(tag, context) {
    let moduleName
    let alias
    let member
    if (tag.children.length) {
        throw RTCodeError.build(context, tag, `'${importAttr}' may have no children`)
    } else if (tag.attribs.name && tag.attribs.from) {
        moduleName = tag.attribs.from
        member = tag.attribs.name
        alias = tag.attribs.as
        if (!alias) {
            if (member === '*') {
                throw RTCodeError.build(context, tag, "'*' imports must have an 'as' attribute")
            } else if (member === 'default') {
                throw RTCodeError.build(context, tag, "default imports must have an 'as' attribute")
            }
            alias = member
        }
    }
    if (!moduleName) {
        throw RTCodeError.build(context, tag, `'${importAttr}' needs 'name' and 'from' attributes`)
    }
    context.defines.push({moduleName, member, alias})
}

function convertTemplateToReact(html, options) {
    const context = require('./context')
    return convertRT(html, context, options)
}

function parseAndConvertHtmlToReact(html, context) {
    const rootNode = cheerio.load(html, {
        lowerCaseTags: false,
        lowerCaseAttributeNames: false,
        xmlMode: true,
        withStartIndices: true
    })
    utils.validate(context.options, context, context.reportContext, rootNode.root()[0])
    let rootTags = _.filter(rootNode.root()[0].children, {type: 'tag'})
    rootTags = handleSelfClosingHtmlTags(rootTags)
    if (!rootTags || rootTags.length === 0) {
        throw new RTCodeError('Document should have a root element')
    }
    let firstTag = null
    _.forEach(rootTags, tag => {
        if (tag.name === requireAttr) {
            handleRequire(tag, context)
        } else if (tag.name === importAttr) {
            handleImport(tag, context)
        } else if (firstTag === null) {
            firstTag = tag
            if (_.hasIn(tag, ['attribs', statelessAttr])) {
                context.stateless = true
            }
        } else {
            throw RTCodeError.build(context, tag, 'Document should have no more than a single root element')
        }
    })
    if (firstTag === null) {
        throw RTCodeError.build(context, rootNode.root()[0], 'Document should have a single root element')
    } else if (firstTag.name === virtualNode) {
        throw RTCodeError.build(context, firstTag, `Document should not have <${virtualNode}> as root element`)
    } else if (_.includes(_.keys(firstTag.attribs), repeatAttr)) {
        throw RTCodeError.build(context, firstTag, "root element may not have a 'rt-repeat' attribute")
    }
    return convertHtmlToReact(firstTag, context)
}

/**
 * @param {string} html
 * @param {CONTEXT} reportContext
 * @param {Options?} options
 * @return {string}
 */
function convertRT(html, reportContext, options) {
    options = getOptions(options)

    const context = defaultContext(html, options, reportContext)
    const body = parseAndConvertHtmlToReact(html, context)
    const injectedFunctions = context.injectedFunctions.join('\n')
    const statelessParams = context.stateless ? 'props, context' : ''
    const renderFunction = `function(${statelessParams}) { ${injectedFunctions}return ${body} }`

    const requirePaths = _.map(context.defines, d => `"${d.moduleName}"`).join(',')
    const requireNames = _.map(context.defines, d => `${d.alias}`).join(',')
    const AMDArguments = _.map(context.defines, (d, i) => d.member === '*' ? `${d.alias}` : `$${i}`).join(',') //eslint-disable-line no-confusing-arrow
    const AMDSubstitutions = _.map(context.defines, (d, i) => d.member === '*' ? null : `var ${d.alias} = $${i}.${d.member};`).join('\n') //eslint-disable-line no-confusing-arrow
    const buildImport = reactSupport.buildImport[options.modules] || reactSupport.buildImport.commonjs
    const requires = _.map(context.defines, buildImport).join('\n')
    const header = options.flow ? '/* @flow */\n' : ''
    const vars = header + requires
    const data = {
        renderFunction,
        requireNames,
        requirePaths,
        AMDArguments,
        AMDSubstitutions,
        vars,
        name: options.name
    }
    let code = templates[options.modules](data)
    if (options.modules !== 'typescript' && options.modules !== 'jsrt') {
        code = parseJS(code, options)
    }
    return code
}

function parseJS(code, options) {
    try {
        let tree = esprima.parse(code, {range: true, tokens: true, comment: true, sourceType: 'module'})
        // fix for https://github.com/wix/react-templates/issues/157
        // do not include comments for es6 modules due to bug in dependency "escodegen"
        // to be removed when https://github.com/estools/escodegen/issues/263 will be fixed
        // remove also its test case "test/data/comment.rt.es6.js"
        if (options.modules !== 'es6') {
            tree = escodegen.attachComments(tree, tree.comments, tree.tokens)
        }
        return escodegen.generate(tree, {comment: true})
    } catch (e) {
        throw new RTCodeError(e.message, e.index, -1)
    }
}

function convertJSRTToJS(text, reportContext, options) {
    options = getOptions(options)
    options.modules = 'jsrt'
    const templateMatcherJSRT = /<template>([^]*?)<\/template>/gm
    const code = text.replace(templateMatcherJSRT, (template, html) => convertRT(html, reportContext, options).replace(/;$/, ''))

    return parseJS(code, options)
}

module.exports = {
    convertTemplateToReact,
    convertRT,
    convertJSRTToJS,
    RTCodeError,
    normalizeName: utils.normalizeName
}
