const qs = require('querystring')
const loaderUtils = require('loader-utils')
const hash = require('hash-sum')
const selfPath = require.resolve('../index')
const templateLoaderPath = require.resolve('./templateLoader')
const stylePostLoaderPath = require.resolve('./stylePostLoader')
const { resolveCompiler } = require('../compiler')
const { testWebpack5 } = require('../codegen/utils')

const isESLintLoader = (l) => /(\/|\\|@)eslint-loader/.test(l.path)
const isNullLoader = (l) => /(\/|\\|@)null-loader/.test(l.path)
const isCSSLoader = (l) => /(\/|\\|@)css-loader/.test(l.path)
const isCacheLoader = (l) => /(\/|\\|@)cache-loader/.test(l.path)
const isPitcher = (l) => l.path !== __filename
const isPreLoader = (l) => !l.pitchExecuted
const isPostLoader = (l) => l.pitchExecuted

const dedupeESLintLoader = (loaders) => {
  const res = []
  let seen = false
  loaders.forEach((l) => {
    if (!isESLintLoader(l)) {
      res.push(l)
    } else if (!seen) {
      seen = true
      res.push(l)
    }
  })
  return res
}

const shouldIgnoreCustomBlock = (loaders) => {
  const actualLoaders = loaders.filter((loader) => {
    // vue-loader
    if (loader.path === selfPath) {
      return false
    }

    // cache-loader
    if (isCacheLoader(loader)) {
      return false
    }

    return true
  })
  return actualLoaders.length === 0
}

module.exports = (code) => code

// This pitching loader is responsible for intercepting all vue block requests
// and transform it into appropriate requests.
module.exports.pitch = function (remainingRequest) {
  const options = loaderUtils.getOptions(this)
  const { cacheDirectory, cacheIdentifier } = options
  const query = qs.parse(this.resourceQuery.slice(1))
  const isWebpack5 = testWebpack5(this._compiler)

  let loaders = this.loaders

  // if this is a language block request, eslint-loader may get matched
  // multiple times
  if (query.type) {
    // if this is an inline block, since the whole file itself is being linted,
    // remove eslint-loader to avoid duplicate linting.
    if (/\.vue$/.test(this.resourcePath)) {
      loaders = loaders.filter((l) => !isESLintLoader(l))
    } else {
      // This is a src import. Just make sure there's not more than 1 instance
      // of eslint present.
      loaders = dedupeESLintLoader(loaders)
    }
  }

  // remove self
  loaders = loaders.filter(isPitcher)

  // do not inject if user uses null-loader to void the type (#1239)
  if (loaders.some(isNullLoader)) {
    return
  }

  const genRequest = (loaders, lang) => {
    // Important: dedupe since both the original rule
    // and the cloned rule would match a source import request.
    // also make sure to dedupe based on loader path.
    // assumes you'd probably never want to apply the same loader on the same
    // file twice.
    // Exception: in Vue CLI we do need two instances of postcss-loader
    // for user config and inline minification. So we need to dedupe baesd on
    // path AND query to be safe.
    const seen = new Map()
    const loaderStrings = []
    const enableInlineMatchResource =
      isWebpack5 && options.experimentalInlineMatchResource

    loaders.forEach((loader) => {
      const identifier =
        typeof loader === 'string' ? loader : loader.path + loader.query
      const request = typeof loader === 'string' ? loader : loader.request
      if (!seen.has(identifier)) {
        seen.set(identifier, true)
        // loader.request contains both the resolved loader path and its options
        // query (e.g. ??ref-0)
        loaderStrings.push(request)
      }
    })
    if (enableInlineMatchResource) {
      return loaderUtils.stringifyRequest(
        this,
        `${this.resourcePath}${lang ? `.${lang}` : ''}${
          this.resourceQuery
        }!=!-!${[...loaderStrings, this.resourcePath + this.resourceQuery].join('!')}`
      )
    }

    return loaderUtils.stringifyRequest(
      this,
      '-!' +
        [...loaderStrings, this.resourcePath + this.resourceQuery].join('!')
    )
  }

  // Inject style-post-loader before css-loader for scoped CSS and trimming
  if (query.type === `style`) {
    if (isWebpack5 && this._compiler.options.experiments && this._compiler.options.experiments.css) {
      // If user enables `experiments.css`, then we are trying to emit css code directly.
      // Although we can target requests like `xxx.vue?type=style` to match `type: "css"`,
      // it will make the plugin a mess.
      if (!options.experimentalInlineMatchResource) {
        this.emitError(
          new Error(
            '`experimentalInlineMatchResource` should be enabled if `experiments.css` enabled currently'
          )
        )
        return ''
      }

      if (query.inline || query.module) {
        this.emitError(
          new Error(
            '`inline` or `module` is currently not supported with `experiments.css` enabled'
          )
        )
        return ''
      }

      const loaderString = [stylePostLoaderPath, ...loaders]
        .map((loader) => {
          return typeof loader === 'string' ? loader : loader.request
        })
        .join('!')

        const styleRequest = loaderUtils.stringifyRequest(
          this,
          `${this.resourcePath}${query.lang ? `.${query.lang}` : ''}${
            this.resourceQuery
          }!=!-!${loaderString}!${this.resourcePath + this.resourceQuery}`
        )
        return `@import ${styleRequest};`
    }

    const cssLoaderIndex = loaders.findIndex(isCSSLoader)
    if (cssLoaderIndex > -1) {
      const afterLoaders = loaders.slice(0, cssLoaderIndex + 1)
      const beforeLoaders = loaders.slice(cssLoaderIndex + 1)
      const request = genRequest(
        [...afterLoaders, stylePostLoaderPath, ...beforeLoaders],
        query.lang || 'css'
      )
      // console.log(request)
      return query.module
        ? `export { default } from  ${request}; export * from ${request}`
        : `export * from ${request}`
    }
  }

  // for templates: inject the template compiler & optional cache
  if (query.type === `template`) {
    const path = require('path')
    const cacheLoader =
      cacheDirectory && cacheIdentifier
        ? [
            `${require.resolve('cache-loader')}?${JSON.stringify({
              // For some reason, webpack fails to generate consistent hash if we
              // use absolute paths here, even though the path is only used in a
              // comment. For now we have to ensure cacheDirectory is a relative path.
              cacheDirectory: (path.isAbsolute(cacheDirectory)
                ? path.relative(process.cwd(), cacheDirectory)
                : cacheDirectory
              ).replace(/\\/g, '/'),
              cacheIdentifier: hash(cacheIdentifier) + '-vue-loader-template'
            })}`
          ]
        : []

    const preLoaders = loaders.filter(isPreLoader)
    const postLoaders = loaders.filter(isPostLoader)
    const { is27 } = resolveCompiler(this.rootContext, this)

    const request = genRequest([
      ...cacheLoader,
      ...postLoaders,
      ...(is27 ? [] : [templateLoaderPath + `??vue-loader-options`]),
      ...preLoaders
    ])

    // the template compiler uses esm exports
    return `export * from ${request}`
  }

  // if a custom block has no other matching loader other than vue-loader itself
  // or cache-loader, we should ignore it
  if (query.type === `custom` && shouldIgnoreCustomBlock(loaders)) {
    return ``
  }

  // When the user defines a rule that has only resourceQuery but no test,
  // both that rule and the cloned rule will match, resulting in duplicated
  // loaders. Therefore it is necessary to perform a dedupe here.
  const request = genRequest(loaders)
  return `import mod from ${request}; export default mod; export * from ${request}`
}