'use strict'

module.exports = jsTemplates
jsTemplates.displayName = 'jsTemplates'
jsTemplates.aliases = []
function jsTemplates(Prism) {
  ;(function (Prism) {
    var templateString = Prism.languages.javascript['template-string'] // see the pattern in prism-javascript.js
    var templateLiteralPattern = templateString.pattern.source
    var interpolationObject = templateString.inside['interpolation']
    var interpolationPunctuationObject =
      interpolationObject.inside['interpolation-punctuation']
    var interpolationPattern = interpolationObject.pattern.source
    /**
     * Creates a new pattern to match a template string with a special tag.
     *
     * This will return `undefined` if there is no grammar with the given language id.
     *
     * @param {string} language The language id of the embedded language. E.g. `markdown`.
     * @param {string} tag The regex pattern to match the tag.
     * @returns {object | undefined}
     * @example
     * createTemplate('css', /\bcss/.source);
     */
    function createTemplate(language, tag) {
      if (!Prism.languages[language]) {
        return undefined
      }
      return {
        pattern: RegExp('((?:' + tag + ')\\s*)' + templateLiteralPattern),
        lookbehind: true,
        greedy: true,
        inside: {
          'template-punctuation': {
            pattern: /^`|`$/,
            alias: 'string'
          },
          'embedded-code': {
            pattern: /[\s\S]+/,
            alias: language
          }
        }
      }
    }
    Prism.languages.javascript['template-string'] = [
      // styled-jsx:
      //   css`a { color: #25F; }`
      // styled-components:
      //   styled.h1`color: red;`
      createTemplate(
        'css',
        /\b(?:styled(?:\([^)]*\))?(?:\s*\.\s*\w+(?:\([^)]*\))*)*|css(?:\s*\.\s*(?:global|resolve))?|createGlobalStyle|keyframes)/
          .source
      ), // html`<p></p>`
      // div.innerHTML = `<p></p>`
      createTemplate('html', /\bhtml|\.\s*(?:inner|outer)HTML\s*\+?=/.source), // svg`<path fill="#fff" d="M55.37 ..."/>`
      createTemplate('svg', /\bsvg/.source), // md`# h1`, markdown`## h2`
      createTemplate('markdown', /\b(?:markdown|md)/.source), // gql`...`, graphql`...`, graphql.experimental`...`
      createTemplate(
        'graphql',
        /\b(?:gql|graphql(?:\s*\.\s*experimental)?)/.source
      ), // sql`...`
      createTemplate('sql', /\bsql/.source), // vanilla template string
      templateString
    ].filter(Boolean)
    /**
     * Returns a specific placeholder literal for the given language.
     *
     * @param {number} counter
     * @param {string} language
     * @returns {string}
     */
    function getPlaceholder(counter, language) {
      return '___' + language.toUpperCase() + '_' + counter + '___'
    }
    /**
     * Returns the tokens of `Prism.tokenize` but also runs the `before-tokenize` and `after-tokenize` hooks.
     *
     * @param {string} code
     * @param {any} grammar
     * @param {string} language
     * @returns {(string|Token)[]}
     */
    function tokenizeWithHooks(code, grammar, language) {
      var env = {
        code: code,
        grammar: grammar,
        language: language
      }
      Prism.hooks.run('before-tokenize', env)
      env.tokens = Prism.tokenize(env.code, env.grammar)
      Prism.hooks.run('after-tokenize', env)
      return env.tokens
    }
    /**
     * Returns the token of the given JavaScript interpolation expression.
     *
     * @param {string} expression The code of the expression. E.g. `"${42}"`
     * @returns {Token}
     */
    function tokenizeInterpolationExpression(expression) {
      var tempGrammar = {}
      tempGrammar['interpolation-punctuation'] = interpolationPunctuationObject
      /** @type {Array} */
      var tokens = Prism.tokenize(expression, tempGrammar)
      if (tokens.length === 3) {
        /**
         * The token array will look like this
         * [
         *     ["interpolation-punctuation", "${"]
         *     "..." // JavaScript expression of the interpolation
         *     ["interpolation-punctuation", "}"]
         * ]
         */
        var args = [1, 1]
        args.push.apply(
          args,
          tokenizeWithHooks(tokens[1], Prism.languages.javascript, 'javascript')
        )
        tokens.splice.apply(tokens, args)
      }
      return new Prism.Token(
        'interpolation',
        tokens,
        interpolationObject.alias,
        expression
      )
    }
    /**
     * Tokenizes the given code with support for JavaScript interpolation expressions mixed in.
     *
     * This function has 3 phases:
     *
     * 1. Replace all JavaScript interpolation expression with a placeholder.
     *    The placeholder will have the syntax of a identify of the target language.
     * 2. Tokenize the code with placeholders.
     * 3. Tokenize the interpolation expressions and re-insert them into the tokenize code.
     *    The insertion only works if a placeholder hasn't been "ripped apart" meaning that the placeholder has been
     *    tokenized as two tokens by the grammar of the embedded language.
     *
     * @param {string} code
     * @param {object} grammar
     * @param {string} language
     * @returns {Token}
     */
    function tokenizeEmbedded(code, grammar, language) {
      // 1. First filter out all interpolations
      // because they might be escaped, we need a lookbehind, so we use Prism
      /** @type {(Token|string)[]} */
      var _tokens = Prism.tokenize(code, {
        interpolation: {
          pattern: RegExp(interpolationPattern),
          lookbehind: true
        }
      }) // replace all interpolations with a placeholder which is not in the code already
      var placeholderCounter = 0
      /** @type {Object<string, string>} */
      var placeholderMap = {}
      var embeddedCode = _tokens
        .map(function (token) {
          if (typeof token === 'string') {
            return token
          } else {
            var interpolationExpression = token.content
            var placeholder
            while (
              code.indexOf(
                (placeholder = getPlaceholder(placeholderCounter++, language))
              ) !== -1
            ) {
              /* noop */
            }
            placeholderMap[placeholder] = interpolationExpression
            return placeholder
          }
        })
        .join('') // 2. Tokenize the embedded code
      var embeddedTokens = tokenizeWithHooks(embeddedCode, grammar, language) // 3. Re-insert the interpolation
      var placeholders = Object.keys(placeholderMap)
      placeholderCounter = 0
      /**
       *
       * @param {(Token|string)[]} tokens
       * @returns {void}
       */
      function walkTokens(tokens) {
        for (var i = 0; i < tokens.length; i++) {
          if (placeholderCounter >= placeholders.length) {
            return
          }
          var token = tokens[i]
          if (typeof token === 'string' || typeof token.content === 'string') {
            var placeholder = placeholders[placeholderCounter]
            var s =
              typeof token === 'string'
                ? token
                : /** @type {string} */
                  token.content
            var index = s.indexOf(placeholder)
            if (index !== -1) {
              ++placeholderCounter
              var before = s.substring(0, index)
              var middle = tokenizeInterpolationExpression(
                placeholderMap[placeholder]
              )
              var after = s.substring(index + placeholder.length)
              var replacement = []
              if (before) {
                replacement.push(before)
              }
              replacement.push(middle)
              if (after) {
                var afterTokens = [after]
                walkTokens(afterTokens)
                replacement.push.apply(replacement, afterTokens)
              }
              if (typeof token === 'string') {
                tokens.splice.apply(tokens, [i, 1].concat(replacement))
                i += replacement.length - 1
              } else {
                token.content = replacement
              }
            }
          } else {
            var content = token.content
            if (Array.isArray(content)) {
              walkTokens(content)
            } else {
              walkTokens([content])
            }
          }
        }
      }
      walkTokens(embeddedTokens)
      return new Prism.Token(
        language,
        embeddedTokens,
        'language-' + language,
        code
      )
    }
    /**
     * The languages for which JS templating will handle tagged template literals.
     *
     * JS templating isn't active for only JavaScript but also related languages like TypeScript, JSX, and TSX.
     */
    var supportedLanguages = {
      javascript: true,
      js: true,
      typescript: true,
      ts: true,
      jsx: true,
      tsx: true
    }
    Prism.hooks.add('after-tokenize', function (env) {
      if (!(env.language in supportedLanguages)) {
        return
      }
      /**
       * Finds and tokenizes all template strings with an embedded languages.
       *
       * @param {(Token | string)[]} tokens
       * @returns {void}
       */
      function findTemplateStrings(tokens) {
        for (var i = 0, l = tokens.length; i < l; i++) {
          var token = tokens[i]
          if (typeof token === 'string') {
            continue
          }
          var content = token.content
          if (!Array.isArray(content)) {
            if (typeof content !== 'string') {
              findTemplateStrings([content])
            }
            continue
          }
          if (token.type === 'template-string') {
            /**
             * A JavaScript template-string token will look like this:
             *
             * ["template-string", [
             *     ["template-punctuation", "`"],
             *     (
             *         An array of "string" and "interpolation" tokens. This is the simple string case.
             *         or
             *         ["embedded-code", "..."] This is the token containing the embedded code.
             *                                  It also has an alias which is the language of the embedded code.
             *     ),
             *     ["template-punctuation", "`"]
             * ]]
             */
            var embedded = content[1]
            if (
              content.length === 3 &&
              typeof embedded !== 'string' &&
              embedded.type === 'embedded-code'
            ) {
              // get string content
              var code = stringContent(embedded)
              var alias = embedded.alias
              var language = Array.isArray(alias) ? alias[0] : alias
              var grammar = Prism.languages[language]
              if (!grammar) {
                // the embedded language isn't registered.
                continue
              }
              content[1] = tokenizeEmbedded(code, grammar, language)
            }
          } else {
            findTemplateStrings(content)
          }
        }
      }
      findTemplateStrings(env.tokens)
    })
    /**
     * Returns the string content of a token or token stream.
     *
     * @param {string | Token | (string | Token)[]} value
     * @returns {string}
     */
    function stringContent(value) {
      if (typeof value === 'string') {
        return value
      } else if (Array.isArray(value)) {
        return value.map(stringContent).join('')
      } else {
        return stringContent(value.content)
      }
    }
  })(Prism)
}
