const { existsSync, readFileSync, statSync } = require('fs')
const { brotliCompress, constants } = require('zlib')
const { promisify } = require('util')
const { load: loadYaml } = require('js-yaml')

const brotliCompressAsync = promisify(brotliCompress)

class ProcessLocalesPlugin {
  constructor(options = {}) {
    this.compress = !!options.compress
    this.isIncrementalBuild = false

    if (typeof options.inputDir !== 'string') {
      throw new Error('ProcessLocalesPlugin: no input directory `inputDir` specified.')
    } else if (!existsSync(options.inputDir)) {
      throw new Error('ProcessLocalesPlugin: the specified input directory does not exist.')
    }
    this.inputDir = options.inputDir

    if (typeof options.outputDir !== 'string') {
      throw new Error('ProcessLocalesPlugin: no output directory `outputDir` specified.')
    }
    this.outputDir = options.outputDir

    this.locales = {}
    this.localeNames = []
    this.activeLocales = []

    this.cache = {}

    this.loadLocales()
  }

  apply(compiler) {
    compiler.hooks.thisCompilation.tap('ProcessLocalesPlugin', (compilation) => {

      const IS_DEV_SERVER = !!compiler.watching
      const { CachedSource, RawSource } = compiler.webpack.sources;

      compilation.hooks.additionalAssets.tapPromise('process-locales-plugin', async (_assets) => {

        // While running in the webpack dev server, this hook gets called for every incremental build.
        // For incremental builds we can return the already processed versions, which saves time
        // and makes webpack treat them as cached
        const promises = []
        // Prevents `loadLocales` called twice on first time (e.g. release build)
        if (this.isIncrementalBuild) {
          this.loadLocales(true)
        } else {
          this.isIncrementalBuild = true
        }

        Object.values(this.locales).forEach((localeEntry) => {
          const { locale, data, mtimeMs } = localeEntry

          promises.push(new Promise(async (resolve) => {
            if (IS_DEV_SERVER) {
              const cacheEntry = this.cache[locale]

              if (cacheEntry != null) {
                const { filename, source, mtimeMs: cachedMtimeMs } = cacheEntry

                if (cachedMtimeMs === mtimeMs) {
                  compilation.emitAsset(filename, source, { minimized: true })
                  resolve()
                  return
                }
              }
            }

            this.removeEmptyValues(data)

            let filename = `${this.outputDir}/${locale}.json`
            let output = JSON.stringify(data)

            if (this.compress) {
              filename += '.br'
              output = await this.compressLocale(output)
            }

            let source = new RawSource(output)

            if (IS_DEV_SERVER) {
              source = new CachedSource(source)
              this.cache[locale] = { filename, source, mtimeMs }
            }

            compilation.emitAsset(filename, source, { minimized: true })

            resolve()
          }))

          if (IS_DEV_SERVER) {
            // we don't need the unmodified sources anymore, as we use the cache `this.cache`
            // so we can clear this to free some memory
            delete localeEntry.data
          }
        })

        await Promise.all(promises)
      })
    })
  }

  loadLocales(loadModifiedFilesOnly = false) {
    if (this.activeLocales.length === 0) {
      this.activeLocales = JSON.parse(readFileSync(`${this.inputDir}/activeLocales.json`))
    }

    for (const locale of this.activeLocales) {
      const filePath = `${this.inputDir}/${locale}.yaml`
      // Cannot use `mtime` since values never equal
      const mtimeMsFromStats = statSync(filePath).mtimeMs
      if (loadModifiedFilesOnly) {
        // Skip reading files where mtime (modified time) same as last read
        // (stored in mtime)
        const existingMtime = this.locales[locale]?.mtimeMs
        if (existingMtime != null && existingMtime === mtimeMsFromStats) {
          continue
        }
      }
      const contents = readFileSync(filePath, 'utf-8')
      const data = loadYaml(contents)
      this.locales[locale] = { locale, data, mtimeMs: mtimeMsFromStats }

      const localeName = data['Locale Name'] ?? locale
      if (!loadModifiedFilesOnly) {
        this.localeNames.push(localeName)
      }
    }
  }

  async compressLocale(data) {
    const buffer = Buffer.from(data, 'utf-8')

    return await brotliCompressAsync(buffer, {
      params: {
        [constants.BROTLI_PARAM_MODE]: constants.BROTLI_MODE_TEXT,
        [constants.BROTLI_PARAM_QUALITY]: constants.BROTLI_MAX_QUALITY,
        [constants.BROTLI_PARAM_SIZE_HINT]: buffer.byteLength
      }
    })
  }

  /**
   * vue-i18n doesn't fallback if the translation is an empty string
   * so we want to get rid of them and also remove the empty objects that can get left behind
   * if we've removed all the keys and values in them
   * @param {object|string} data
   */
  removeEmptyValues(data) {
    for (const key of Object.keys(data)) {
      const value = data[key]
      if (typeof value === 'object') {
        this.removeEmptyValues(value)
      }

      if (!value || (typeof value === 'object' && Object.keys(value).length === 0)) {
        delete data[key]
      }
    }
  }
}

module.exports = ProcessLocalesPlugin
