const qs = require('querystring')
const loaderUtils = require('loader-utils')
const { resolveCompiler } = require('../compiler')
const { getDescriptor } = require('../descriptorCache')
const { resolveScript } = require('../resolveScript')

// Loader that compiles raw template into JavaScript functions.
// This is injected by the global pitcher (../pitch) for template
// selection requests initiated from vue files.
module.exports = function (source) {
  const loaderContext = this
  const filename = this.resourcePath
  const ctx = this.rootContext
  const query = qs.parse(this.resourceQuery.slice(1))

  // although this is not the main vue-loader, we can get access to the same
  // vue-loader options because we've set an ident in the plugin and used that
  // ident to create the request for this loader in the pitcher.
  const options = loaderUtils.getOptions(loaderContext) || {}
  const { id } = query
  const isServer = loaderContext.target === 'node'
  const isProduction =
    options.productionMode ||
    loaderContext.minimize ||
    process.env.NODE_ENV === 'production'
  const isFunctional = query.functional

  const compilerOptions = Object.assign(
    {
      outputSourceRange: true
    },
    options.compilerOptions,
    {
      scopeId: query.scoped ? `data-v-${id}` : null,
      comments: query.comments
    }
  )

  const { compiler, templateCompiler } = resolveCompiler(ctx, loaderContext)

  const descriptor = getDescriptor(filename, options, loaderContext)
  const script = resolveScript(descriptor, id, options, loaderContext)

  // for vue/compiler-sfc OR @vue/component-compiler-utils
  const finalOptions = {
    source,
    filename: this.resourcePath,
    compiler: options.compiler || templateCompiler,
    compilerOptions,
    // allow customizing behavior of vue-template-es2015-compiler
    transpileOptions: options.transpileOptions,
    transformAssetUrls: options.transformAssetUrls || true,
    isProduction,
    isFunctional,
    optimizeSSR: isServer && options.optimizeSSR !== false,
    prettify: options.prettify,
    bindings: script ? script.bindings : undefined
  }

  const compiled = compiler.compileTemplate(finalOptions)

  // tips
  if (compiled.tips && compiled.tips.length) {
    compiled.tips.forEach((tip) => {
      loaderContext.emitWarning(typeof tip === 'object' ? tip.msg : tip)
    })
  }

  // errors
  if (compiled.errors && compiled.errors.length) {
    const generateCodeFrame =
      (templateCompiler && templateCompiler.generateCodeFrame) ||
      compiler.generateCodeFrame
    // 2.6 compiler outputs errors as objects with range
    if (generateCodeFrame && finalOptions.compilerOptions.outputSourceRange) {
      // TODO account for line offset in case template isn't placed at top
      // of the file
      loaderContext.emitError(
        `\n\n  Errors compiling template:\n\n` +
          compiled.errors
            .map(({ msg, start, end }) => {
              const frame = generateCodeFrame(source, start, end)
              return `  ${msg}\n\n${pad(frame)}`
            })
            .join(`\n\n`) +
          '\n'
      )
    } else {
      loaderContext.emitError(
        `\n  Error compiling template:\n${pad(compiled.source)}\n` +
          compiled.errors.map((e) => `  - ${e}`).join('\n') +
          '\n'
      )
    }
  }

  const { code } = compiled

  // finish with ESM exports
  return code + `\nexport { render, staticRenderFns }`
}

function pad(source) {
  return source
    .split(/\r?\n/)
    .map((line) => `  ${line}`)
    .join('\n')
}
