// Based on https://github.com/webpack/webpack/blob/v4.46.0/lib/node/NodeMainTemplatePlugin.js#L81-L191

import { createRequire } from 'module'
import type { Compiler } from 'webpack'
export class AsyncLoadingPlugin {
  private opts: any
  private Template: any
  constructor (opts) {
    this.opts = opts
    const _require = createRequire(import.meta.url)
    const TemplatePath = _require.resolve('webpack/lib/Template', { paths: [...this.opts.modulesDir] })
    this.Template = _require(TemplatePath)
  }

  apply (compiler: Compiler) {
    compiler.hooks.compilation.tap('AsyncLoading', (compilation) => {
      const mainTemplate = compilation.mainTemplate
      mainTemplate.hooks.requireEnsure.tap(
        'AsyncLoading',
        (_source, chunk, hash) => {
          const Template = this.Template
          const chunkFilename = mainTemplate.outputOptions.chunkFilename
          const chunkMaps = chunk.getChunkMaps()
          const insertMoreModules = [
            'var moreModules = chunk.modules, chunkIds = chunk.ids;',
            'for(var moduleId in moreModules) {',
            Template.indent(
              mainTemplate.renderAddModule(
                hash,
                chunk,
                'moduleId',
                'moreModules[moduleId]'
              )
            ),
            '}'
          ]
          return Template.asString([
            '// Async chunk loading for Nitro',
            '',
            'var installedChunkData = installedChunks[chunkId];',
            'if(installedChunkData !== 0) { // 0 means "already installed".',
            Template.indent([
              '// array of [resolve, reject, promise] means "currently loading"',
              'if(installedChunkData) {',
              Template.indent(['promises.push(installedChunkData[2]);']),
              '} else {',
              Template.indent([
                '// load the chunk and return promise to it',
                'var promise = new Promise(function(resolve, reject) {',
                Template.indent([
                  'installedChunkData = installedChunks[chunkId] = [resolve, reject];',
                  'import(' +
                  mainTemplate.getAssetPath(
                    JSON.stringify(`./${chunkFilename}`),
                    {
                      hash: `" + ${mainTemplate.renderCurrentHashCode(
                        hash
                      )} + "`,
                      hashWithLength: length =>
                        `" + ${mainTemplate.renderCurrentHashCode(
                          hash,
                          length
                        )} + "`,
                      chunk: {
                        id: '" + chunkId + "',
                        hash: `" + ${JSON.stringify(
                          chunkMaps.hash
                        )}[chunkId] + "`,
                        hashWithLength: (length) => {
                          const shortChunkHashMap = {}
                          for (const chunkId of Object.keys(chunkMaps.hash)) {
                            if (typeof chunkMaps.hash[chunkId] === 'string') {
                              shortChunkHashMap[chunkId] = chunkMaps.hash[
                                chunkId
                              ].substr(0, length)
                            }
                          }
                          return `" + ${JSON.stringify(
                            shortChunkHashMap
                          )}[chunkId] + "`
                        },
                        contentHash: {
                          javascript: `" + ${JSON.stringify(
                            chunkMaps.contentHash.javascript
                          )}[chunkId] + "`
                        },
                        contentHashWithLength: {
                          javascript: (length) => {
                            const shortContentHashMap = {}
                            const contentHash =
                              chunkMaps.contentHash.javascript
                            for (const chunkId of Object.keys(contentHash)) {
                              if (typeof contentHash[chunkId] === 'string') {
                                shortContentHashMap[chunkId] = contentHash[
                                  chunkId
                                ].substr(0, length)
                              }
                            }
                            return `" + ${JSON.stringify(
                              shortContentHashMap
                            )}[chunkId] + "`
                          }
                        },
                        name: `" + (${JSON.stringify(
                          chunkMaps.name
                        )}[chunkId]||chunkId) + "`
                      },
                      contentHashType: 'javascript'
                    }
                  ) +
                  ').then(chunk => {',
                  Template.indent(
                    insertMoreModules
                      .concat([
                        'var callbacks = [];',
                        'for(var i = 0; i < chunkIds.length; i++) {',
                        Template.indent([
                          'if(installedChunks[chunkIds[i]])',
                          Template.indent([
                            'callbacks = callbacks.concat(installedChunks[chunkIds[i]][0]);'
                          ]),
                          'installedChunks[chunkIds[i]] = 0;'
                        ]),
                        '}',
                        'for(i = 0; i < callbacks.length; i++)',
                        Template.indent('callbacks[i]();')
                      ])
                  ),
                  '});'
                ]),
                '});',
                'promises.push(installedChunkData[2] = promise);'
              ]),
              '}'
            ]),
            '}'
          ])
        })
    })
  }
}
