import fs from 'fs'
import path from 'path'
import sourceMapSupport from 'source-map-support'
import MemoryFileSystem from 'memory-fs'
import { Compiler, Stats } from 'webpack'
import registerRequireHook from '../../util/registerRequireHook'
import { ensureAbsolutePath } from '../../util/paths'
import { MOCHAPACK_NAME } from '../../util/constants'

type VoidFunction = () => void

export default function registerInMemoryCompiler(
  compiler: Compiler
): VoidFunction {
  // register memory fs to webpack
  const memoryFs = new MemoryFileSystem()
  // the flag is used for integration tests that need to read compiled files
  if (!process.env.MOCHAPACK_WRITE_TO_DISK) {
    compiler.outputFileSystem = memoryFs // eslint-disable-line no-param-reassign
  }

  // build asset map to allow fast checks for file existence
  const assetMap = new Map()
  compiler.hooks.done.tap(MOCHAPACK_NAME, (stats: Stats) => {
    assetMap.clear()

    if (!stats.hasErrors()) {
      Object.keys(stats.compilation.assets).forEach(assetPath =>
        assetMap.set(
          ensureAbsolutePath(assetPath, compiler.options.output.path),
          true
        )
      )
    }
  })

  // provide file reader to read from memory fs
  let readFile = filePath => {
    if (assetMap.has(filePath)) {
      try {
        const code = memoryFs.readFileSync(filePath, 'utf8')
        return code
      } catch (e) {
        return null
      }
    }
    return null
  }

  // module resolver for require calls from memory fs
  const resolveFile = (filePath, requireCaller) => {
    // try to read file from memory-fs as it is
    let code = readFile(filePath)
    let resolvedPath = filePath

    if (code === null && requireCaller != null) {
      const { filename } = requireCaller
      if (filename != null) {
        // if that didn't work, resolve the file relative to it's parent
        resolvedPath = path.resolve(path.dirname(filename), filePath)
        code = readFile(resolvedPath)
      }
    }
    return { path: code !== null ? resolvedPath : null, source: code }
  }

  // install require hook to be able to require webpack bundles from memory
  const unmountHook = registerRequireHook('.js', resolveFile)

  // install source map support to read source map from memory
  sourceMapSupport.install({
    emptyCacheBetweenOperations: true,
    handleUncaughtExceptions: false,
    environment: 'node',
    retrieveFile: f => readFile(f) // wrapper function to fake an unmount function
  })

  return function unmount() {
    unmountHook()
    readFile = filePath => null // eslint-disable-line no-unused-vars
  }
}
