/*
	MIT License http://www.opensource.org/licenses/mit-license.php
	Author Tobias Koppers @sokra
*/

'use strict'

const parseJson = require('json-parse-even-better-errors')
const asyncLib = require('neo-async')
const {
  SyncHook,
  SyncBailHook,
  AsyncParallelHook,
  AsyncSeriesHook
} = require('tapable')
const { SizeOnlySource } = require('webpack-sources')
const webpack = require('./')
const Cache = require('./Cache')
const CacheFacade = require('./CacheFacade')
const ChunkGraph = require('./ChunkGraph')
const Compilation = require('./Compilation')
const ConcurrentCompilationError = require('./ConcurrentCompilationError')
const ContextModuleFactory = require('./ContextModuleFactory')
const ModuleGraph = require('./ModuleGraph')
const NormalModuleFactory = require('./NormalModuleFactory')
const RequestShortener = require('./RequestShortener')
const ResolverFactory = require('./ResolverFactory')
const Stats = require('./Stats')
const Watching = require('./Watching')
const WebpackError = require('./WebpackError')
const { Logger } = require('./logging/Logger')
const { join, dirname, mkdirp } = require('./util/fs')
const { makePathsRelative } = require('./util/identifier')
const { isSourceEqual } = require('./util/source')

/** @typedef {import("webpack-sources").Source} Source */
/** @typedef {import("../declarations/WebpackOptions").EntryNormalized} Entry */
/** @typedef {import("../declarations/WebpackOptions").OutputNormalized} OutputOptions */
/** @typedef {import("../declarations/WebpackOptions").WatchOptions} WatchOptions */
/** @typedef {import("../declarations/WebpackOptions").WebpackOptionsNormalized} WebpackOptions */
/** @typedef {import("../declarations/WebpackOptions").WebpackPluginInstance} WebpackPluginInstance */
/** @typedef {import("./Chunk")} Chunk */
/** @typedef {import("./Dependency")} Dependency */
/** @typedef {import("./FileSystemInfo").FileSystemInfoEntry} FileSystemInfoEntry */
/** @typedef {import("./Module")} Module */
/** @typedef {import("./util/WeakTupleMap")} WeakTupleMap */
/** @typedef {import("./util/fs").InputFileSystem} InputFileSystem */
/** @typedef {import("./util/fs").IntermediateFileSystem} IntermediateFileSystem */
/** @typedef {import("./util/fs").OutputFileSystem} OutputFileSystem */
/** @typedef {import("./util/fs").WatchFileSystem} WatchFileSystem */

/**
 * @typedef {Object} CompilationParams
 * @property {NormalModuleFactory} normalModuleFactory
 * @property {ContextModuleFactory} contextModuleFactory
 */

/**
 * @template T
 * @callback Callback
 * @param {(Error | null)=} err
 * @param {T=} result
 */

/**
 * @callback RunAsChildCallback
 * @param {(Error | null)=} err
 * @param {Chunk[]=} entries
 * @param {Compilation=} compilation
 */

/**
 * @typedef {Object} AssetEmittedInfo
 * @property {Buffer} content
 * @property {Source} source
 * @property {Compilation} compilation
 * @property {string} outputPath
 * @property {string} targetPath
 */

/**
 * @param {string[]} array an array
 * @returns {boolean} true, if the array is sorted
 */
const isSorted = array => {
  for (let i = 1; i < array.length; i++) {
    if (array[i - 1] > array[i]) return false
  }
  return true
}

/**
 * @param {Object} obj an object
 * @param {string[]} keys the keys of the object
 * @returns {Object} the object with properties sorted by property name
 */
const sortObject = (obj, keys) => {
  const o = {}
  for (const k of keys.sort()) {
    o[k] = obj[k]
  }
  return o
}

/**
 * @param {string} filename filename
 * @param {string | string[] | undefined} hashes list of hashes
 * @returns {boolean} true, if the filename contains any hash
 */
const includesHash = (filename, hashes) => {
  if (!hashes) return false
  if (Array.isArray(hashes)) {
    return hashes.some(hash => filename.includes(hash))
  } else {
    return filename.includes(hashes)
  }
}

class Compiler {
  /**
	 * @param {string} context the compilation path
	 * @param {WebpackOptions} options options
	 */
  constructor (context, options = /** @type {WebpackOptions} */ ({})) {
    this.hooks = Object.freeze({
      /** @type {SyncHook<[]>} */
      initialize: new SyncHook([]),

      /** @type {SyncBailHook<[Compilation], boolean | undefined>} */
      shouldEmit: new SyncBailHook(['compilation']),
      /** @type {AsyncSeriesHook<[Stats]>} */
      done: new AsyncSeriesHook(['stats']),
      /** @type {SyncHook<[Stats]>} */
      afterDone: new SyncHook(['stats']),
      /** @type {AsyncSeriesHook<[]>} */
      additionalPass: new AsyncSeriesHook([]),
      /** @type {AsyncSeriesHook<[Compiler]>} */
      beforeRun: new AsyncSeriesHook(['compiler']),
      /** @type {AsyncSeriesHook<[Compiler]>} */
      run: new AsyncSeriesHook(['compiler']),
      /** @type {AsyncSeriesHook<[Compilation]>} */
      emit: new AsyncSeriesHook(['compilation']),
      /** @type {AsyncSeriesHook<[string, AssetEmittedInfo]>} */
      assetEmitted: new AsyncSeriesHook(['file', 'info']),
      /** @type {AsyncSeriesHook<[Compilation]>} */
      afterEmit: new AsyncSeriesHook(['compilation']),

      /** @type {SyncHook<[Compilation, CompilationParams]>} */
      thisCompilation: new SyncHook(['compilation', 'params']),
      /** @type {SyncHook<[Compilation, CompilationParams]>} */
      compilation: new SyncHook(['compilation', 'params']),
      /** @type {SyncHook<[NormalModuleFactory]>} */
      normalModuleFactory: new SyncHook(['normalModuleFactory']),
      /** @type {SyncHook<[ContextModuleFactory]>}  */
      contextModuleFactory: new SyncHook(['contextModuleFactory']),

      /** @type {AsyncSeriesHook<[CompilationParams]>} */
      beforeCompile: new AsyncSeriesHook(['params']),
      /** @type {SyncHook<[CompilationParams]>} */
      compile: new SyncHook(['params']),
      /** @type {AsyncParallelHook<[Compilation]>} */
      make: new AsyncParallelHook(['compilation']),
      /** @type {AsyncParallelHook<[Compilation]>} */
      finishMake: new AsyncSeriesHook(['compilation']),
      /** @type {AsyncSeriesHook<[Compilation]>} */
      afterCompile: new AsyncSeriesHook(['compilation']),

      /** @type {AsyncSeriesHook<[]>} */
      readRecords: new AsyncSeriesHook([]),
      /** @type {AsyncSeriesHook<[]>} */
      emitRecords: new AsyncSeriesHook([]),

      /** @type {AsyncSeriesHook<[Compiler]>} */
      watchRun: new AsyncSeriesHook(['compiler']),
      /** @type {SyncHook<[Error]>} */
      failed: new SyncHook(['error']),
      /** @type {SyncHook<[string | null, number]>} */
      invalid: new SyncHook(['filename', 'changeTime']),
      /** @type {SyncHook<[]>} */
      watchClose: new SyncHook([]),
      /** @type {AsyncSeriesHook<[]>} */
      shutdown: new AsyncSeriesHook([]),

      /** @type {SyncBailHook<[string, string, any[]], true>} */
      infrastructureLog: new SyncBailHook(['origin', 'type', 'args']),

      // TODO the following hooks are weirdly located here
      // TODO move them for webpack 5
      /** @type {SyncHook<[]>} */
      environment: new SyncHook([]),
      /** @type {SyncHook<[]>} */
      afterEnvironment: new SyncHook([]),
      /** @type {SyncHook<[Compiler]>} */
      afterPlugins: new SyncHook(['compiler']),
      /** @type {SyncHook<[Compiler]>} */
      afterResolvers: new SyncHook(['compiler']),
      /** @type {SyncBailHook<[string, Entry], boolean>} */
      entryOption: new SyncBailHook(['context', 'entry'])
    })

    this.webpack = webpack

    /** @type {string=} */
    this.name = undefined
    /** @type {Compilation=} */
    this.parentCompilation = undefined
    /** @type {Compiler} */
    this.root = this
    /** @type {string} */
    this.outputPath = ''
    /** @type {Watching | undefined} */
    this.watching = undefined

    /** @type {OutputFileSystem} */
    this.outputFileSystem = null
    /** @type {IntermediateFileSystem} */
    this.intermediateFileSystem = null
    /** @type {InputFileSystem} */
    this.inputFileSystem = null
    /** @type {WatchFileSystem} */
    this.watchFileSystem = null

    /** @type {string|null} */
    this.recordsInputPath = null
    /** @type {string|null} */
    this.recordsOutputPath = null
    this.records = {}
    /** @type {Set<string | RegExp>} */
    this.managedPaths = new Set()
    /** @type {Set<string | RegExp>} */
    this.immutablePaths = new Set()

    /** @type {ReadonlySet<string> | undefined} */
    this.modifiedFiles = undefined
    /** @type {ReadonlySet<string> | undefined} */
    this.removedFiles = undefined
    /** @type {ReadonlyMap<string, FileSystemInfoEntry | "ignore" | null> | undefined} */
    this.fileTimestamps = undefined
    /** @type {ReadonlyMap<string, FileSystemInfoEntry | "ignore" | null> | undefined} */
    this.contextTimestamps = undefined
    /** @type {number | undefined} */
    this.fsStartTime = undefined

    /** @type {ResolverFactory} */
    this.resolverFactory = new ResolverFactory()

    this.infrastructureLogger = undefined

    this.options = options

    this.context = context

    this.requestShortener = new RequestShortener(context, this.root)

    this.cache = new Cache()

    /** @type {Map<Module, { buildInfo: object, references: WeakMap<Dependency, Module>, memCache: WeakTupleMap }> | undefined} */
    this.moduleMemCaches = undefined

    this.compilerPath = ''

    /** @type {boolean} */
    this.running = false

    /** @type {boolean} */
    this.idle = false

    /** @type {boolean} */
    this.watchMode = false

    this._backCompat = this.options.experiments.backCompat !== false

    /** @type {Compilation} */
    this._lastCompilation = undefined
    /** @type {NormalModuleFactory} */
    this._lastNormalModuleFactory = undefined

    /** @private @type {WeakMap<Source, { sizeOnlySource: SizeOnlySource, writtenTo: Map<string, number> }>} */
    this._assetEmittingSourceCache = new WeakMap()
    /** @private @type {Map<string, number>} */
    this._assetEmittingWrittenFiles = new Map()
    /** @private @type {Set<string>} */
    this._assetEmittingPreviousFiles = new Set()
  }

  /**
	 * @param {string} name cache name
	 * @returns {CacheFacade} the cache facade instance
	 */
  getCache (name) {
    return new CacheFacade(
      this.cache,
      `${this.compilerPath}${name}`,
      this.options.output.hashFunction
    )
  }

  /**
	 * @param {string | (function(): string)} name name of the logger, or function called once to get the logger name
	 * @returns {Logger} a logger with that name
	 */
  getInfrastructureLogger (name) {
    if (!name) {
      throw new TypeError(
        'Compiler.getInfrastructureLogger(name) called without a name'
      )
    }
    return new Logger(
      (type, args) => {
        if (typeof name === 'function') {
          name = name()
          if (!name) {
            throw new TypeError(
              'Compiler.getInfrastructureLogger(name) called with a function not returning a name'
            )
          }
        }
        if (this.hooks.infrastructureLog.call(name, type, args) === undefined) {
          if (this.infrastructureLogger !== undefined) {
            this.infrastructureLogger(name, type, args)
          }
        }
      },
      childName => {
        if (typeof name === 'function') {
          if (typeof childName === 'function') {
            return this.getInfrastructureLogger(() => {
              if (typeof name === 'function') {
                name = name()
                if (!name) {
                  throw new TypeError(
                    'Compiler.getInfrastructureLogger(name) called with a function not returning a name'
                  )
                }
              }
              if (typeof childName === 'function') {
                childName = childName()
                if (!childName) {
                  throw new TypeError(
                    'Logger.getChildLogger(name) called with a function not returning a name'
                  )
                }
              }
              return `${name}/${childName}`
            })
          } else {
            return this.getInfrastructureLogger(() => {
              if (typeof name === 'function') {
                name = name()
                if (!name) {
                  throw new TypeError(
                    'Compiler.getInfrastructureLogger(name) called with a function not returning a name'
                  )
                }
              }
              return `${name}/${childName}`
            })
          }
        } else {
          if (typeof childName === 'function') {
            return this.getInfrastructureLogger(() => {
              if (typeof childName === 'function') {
                childName = childName()
                if (!childName) {
                  throw new TypeError(
                    'Logger.getChildLogger(name) called with a function not returning a name'
                  )
                }
              }
              return `${name}/${childName}`
            })
          } else {
            return this.getInfrastructureLogger(`${name}/${childName}`)
          }
        }
      }
    )
  }

  // TODO webpack 6: solve this in a better way
  // e.g. move compilation specific info from Modules into ModuleGraph
  _cleanupLastCompilation () {
    if (this._lastCompilation !== undefined) {
      for (const module of this._lastCompilation.modules) {
        ChunkGraph.clearChunkGraphForModule(module)
        ModuleGraph.clearModuleGraphForModule(module)
        module.cleanupForCache()
      }
      for (const chunk of this._lastCompilation.chunks) {
        ChunkGraph.clearChunkGraphForChunk(chunk)
      }
      this._lastCompilation = undefined
    }
  }

  // TODO webpack 6: solve this in a better way
  _cleanupLastNormalModuleFactory () {
    if (this._lastNormalModuleFactory !== undefined) {
      this._lastNormalModuleFactory.cleanupForCache()
      this._lastNormalModuleFactory = undefined
    }
  }

  /**
	 * @param {WatchOptions} watchOptions the watcher's options
	 * @param {Callback<Stats>} handler signals when the call finishes
	 * @returns {Watching} a compiler watcher
	 */
  watch (watchOptions, handler) {
    if (this.running) {
      return handler(new ConcurrentCompilationError())
    }

    this.running = true
    this.watchMode = true
    this.watching = new Watching(this, watchOptions, handler)
    return this.watching
  }

  /**
	 * @param {Callback<Stats>} callback signals when the call finishes
	 * @returns {void}
	 */
  run (callback) {
    if (this.running) {
      return callback(new ConcurrentCompilationError())
    }

    let logger

    const finalCallback = (err, stats) => {
      if (logger) logger.time('beginIdle')
      this.idle = true
      this.cache.beginIdle()
      this.idle = true
      if (logger) logger.timeEnd('beginIdle')
      this.running = false
      if (err) {
        this.hooks.failed.call(err)
      }
      if (callback !== undefined) callback(err, stats)
      this.hooks.afterDone.call(stats)
    }

    const startTime = Date.now()

    this.running = true

    const onCompiled = (err, compilation) => {
      if (err) return finalCallback(err)

      if (this.hooks.shouldEmit.call(compilation) === false) {
        compilation.startTime = startTime
        compilation.endTime = Date.now()
        const stats = new Stats(compilation)
        this.hooks.done.callAsync(stats, err => {
          if (err) return finalCallback(err)
          return finalCallback(null, stats)
        })
        return
      }

      process.nextTick(() => {
        logger = compilation.getLogger('webpack.Compiler')
        logger.time('emitAssets')
        this.emitAssets(compilation, err => {
          logger.timeEnd('emitAssets')
          if (err) return finalCallback(err)

          if (compilation.hooks.needAdditionalPass.call()) {
            compilation.needAdditionalPass = true

            compilation.startTime = startTime
            compilation.endTime = Date.now()
            logger.time('done hook')
            const stats = new Stats(compilation)
            this.hooks.done.callAsync(stats, err => {
              logger.timeEnd('done hook')
              if (err) return finalCallback(err)

              this.hooks.additionalPass.callAsync(err => {
                if (err) return finalCallback(err)
                this.compile(onCompiled)
              })
            })
            return
          }

          logger.time('emitRecords')
          this.emitRecords(err => {
            logger.timeEnd('emitRecords')
            if (err) return finalCallback(err)

            compilation.startTime = startTime
            compilation.endTime = Date.now()
            logger.time('done hook')
            const stats = new Stats(compilation)
            this.hooks.done.callAsync(stats, err => {
              logger.timeEnd('done hook')
              if (err) return finalCallback(err)
              this.cache.storeBuildDependencies(
                compilation.buildDependencies,
                err => {
                  if (err) return finalCallback(err)
                  return finalCallback(null, stats)
                }
              )
            })
          })
        })
      })
    }

    const run = () => {
      this.hooks.beforeRun.callAsync(this, err => {
        if (err) return finalCallback(err)

        this.hooks.run.callAsync(this, err => {
          if (err) return finalCallback(err)

          this.readRecords(err => {
            if (err) return finalCallback(err)

            this.compile(onCompiled)
          })
        })
      })
    }

    if (this.idle) {
      this.cache.endIdle(err => {
        if (err) return finalCallback(err)

        this.idle = false
        run()
      })
    } else {
      run()
    }
  }

  /**
	 * @param {RunAsChildCallback} callback signals when the call finishes
	 * @returns {void}
	 */
  runAsChild (callback) {
    const startTime = Date.now()

    const finalCallback = (err, entries, compilation) => {
      try {
        callback(err, entries, compilation)
      } catch (e) {
        const err = new WebpackError(
          `compiler.runAsChild callback error: ${e}`
        )
        err.details = e.stack
        this.parentCompilation.errors.push(err)
      }
    }

    this.compile((err, compilation) => {
      if (err) return finalCallback(err)

      this.parentCompilation.children.push(compilation)
      for (const { name, source, info } of compilation.getAssets()) {
        this.parentCompilation.emitAsset(name, source, info)
      }

      const entries = []
      for (const ep of compilation.entrypoints.values()) {
        entries.push(...ep.chunks)
      }

      compilation.startTime = startTime
      compilation.endTime = Date.now()

      return finalCallback(null, entries, compilation)
    })
  }

  purgeInputFileSystem () {
    if (this.inputFileSystem && this.inputFileSystem.purge) {
      this.inputFileSystem.purge()
    }
  }

  /**
	 * @param {Compilation} compilation the compilation
	 * @param {Callback<void>} callback signals when the assets are emitted
	 * @returns {void}
	 */
  emitAssets (compilation, callback) {
    let outputPath

    const emitFiles = err => {
      if (err) return callback(err)

      const assets = compilation.getAssets()
      compilation.assets = { ...compilation.assets }
      /** @type {Map<string, { path: string, source: Source, size: number, waiting: { cacheEntry: any, file: string }[] }>} */
      const caseInsensitiveMap = new Map()
      /** @type {Set<string>} */
      const allTargetPaths = new Set()
      asyncLib.forEachLimit(
        assets,
        15,
        ({ name: file, source, info }, callback) => {
          let targetFile = file
          let immutable = info.immutable
          const queryStringIdx = targetFile.indexOf('?')
          if (queryStringIdx >= 0) {
            targetFile = targetFile.slice(0, queryStringIdx)
            // We may remove the hash, which is in the query string
            // So we recheck if the file is immutable
            // This doesn't cover all cases, but immutable is only a performance optimization anyway
            immutable =
							immutable &&
							(includesHash(targetFile, info.contenthash) ||
								includesHash(targetFile, info.chunkhash) ||
								includesHash(targetFile, info.modulehash) ||
								includesHash(targetFile, info.fullhash))
          }

          const writeOut = err => {
            if (err) return callback(err)
            const targetPath = join(
              this.outputFileSystem,
              outputPath,
              targetFile
            )
            allTargetPaths.add(targetPath)

            // check if the target file has already been written by this Compiler
            const targetFileGeneration =
							this._assetEmittingWrittenFiles.get(targetPath)

            // create an cache entry for this Source if not already existing
            let cacheEntry = this._assetEmittingSourceCache.get(source)
            if (cacheEntry === undefined) {
              cacheEntry = {
                sizeOnlySource: undefined,
                writtenTo: new Map()
              }
              this._assetEmittingSourceCache.set(source, cacheEntry)
            }

            let similarEntry

            const checkSimilarFile = () => {
              const caseInsensitiveTargetPath = targetPath.toLowerCase()
              similarEntry = caseInsensitiveMap.get(caseInsensitiveTargetPath)
              // todo 去除掉相似性校验，cocos反馈 Windows上打包有问题
              // 							if (similarEntry !== undefined) {
              // 								const { path: other, source: otherSource } = similarEntry;
              // 								if (isSourceEqual(otherSource, source)) {
              // 									// Size may or may not be available at this point.
              // 									// If it's not available add to "waiting" list and it will be updated once available
              // 									if (similarEntry.size !== undefined) {
              // 										updateWithReplacementSource(similarEntry.size);
              // 									} else {
              // 										if (!similarEntry.waiting) similarEntry.waiting = [];
              // 										similarEntry.waiting.push({ file, cacheEntry });
              // 									}
              // 									alreadyWritten();
              // 								} else {
              // 									const err =
              // 										new WebpackError(`Prevent writing to file that only differs in casing or query string from already written file.
              // This will lead to a race-condition and corrupted files on case-insensitive file systems.
              // ${targetPath}
              // ${other}`);
              // 									err.file = file;
              // 									callback(err);
              // 								}
              // 								return true;
              // 							} else {
              caseInsensitiveMap.set(
                caseInsensitiveTargetPath,
                (similarEntry = {
                  path: targetPath,
                  source,
                  size: undefined,
                  waiting: undefined
                })
              )
              return false
              // }
            }

            /**
						 * get the binary (Buffer) content from the Source
						 * @returns {Buffer} content for the source
						 */
            const getContent = () => {
              if (typeof source.buffer === 'function') {
                return source.buffer()
              } else {
                const bufferOrString = source.source()
                if (Buffer.isBuffer(bufferOrString)) {
                  return bufferOrString
                } else {
                  return Buffer.from(bufferOrString, 'utf8')
                }
              }
            }

            const alreadyWritten = () => {
              // cache the information that the Source has been already been written to that location
              if (targetFileGeneration === undefined) {
                const newGeneration = 1
                this._assetEmittingWrittenFiles.set(targetPath, newGeneration)
                cacheEntry.writtenTo.set(targetPath, newGeneration)
              } else {
                cacheEntry.writtenTo.set(targetPath, targetFileGeneration)
              }
              callback()
            }

            /**
						 * Write the file to output file system
						 * @param {Buffer} content content to be written
						 * @returns {void}
						 */
            const doWrite = content => {
              this.outputFileSystem.writeFile(targetPath, content, err => {
                if (err) return callback(err)

                // information marker that the asset has been emitted
                compilation.emittedAssets.add(file)

                // cache the information that the Source has been written to that location
                const newGeneration =
									targetFileGeneration === undefined
									  ? 1
									  : targetFileGeneration + 1
                cacheEntry.writtenTo.set(targetPath, newGeneration)
                this._assetEmittingWrittenFiles.set(targetPath, newGeneration)
                this.hooks.assetEmitted.callAsync(
                  file,
                  {
                    content,
                    source,
                    outputPath,
                    compilation,
                    targetPath
                  },
                  callback
                )
              })
            }

            const updateWithReplacementSource = size => {
              updateFileWithReplacementSource(file, cacheEntry, size)
              similarEntry.size = size
              if (similarEntry.waiting !== undefined) {
                for (const { file, cacheEntry } of similarEntry.waiting) {
                  updateFileWithReplacementSource(file, cacheEntry, size)
                }
              }
            }

            const updateFileWithReplacementSource = (
              file,
              cacheEntry,
              size
            ) => {
              // Create a replacement resource which only allows to ask for size
              // This allows to GC all memory allocated by the Source
              // (expect when the Source is stored in any other cache)
              if (!cacheEntry.sizeOnlySource) {
                cacheEntry.sizeOnlySource = new SizeOnlySource(size)
              }
              compilation.updateAsset(file, cacheEntry.sizeOnlySource, {
                size
              })
            }

            const processExistingFile = stats => {
              // skip emitting if it's already there and an immutable file
              if (immutable) {
                updateWithReplacementSource(stats.size)
                return alreadyWritten()
              }

              const content = getContent()

              updateWithReplacementSource(content.length)

              // if it exists and content on disk matches content
              // skip writing the same content again
              // (to keep mtime and don't trigger watchers)
              // for a fast negative match file size is compared first
              if (content.length === stats.size) {
                compilation.comparedForEmitAssets.add(file)
                return this.outputFileSystem.readFile(
                  targetPath,
                  (err, existingContent) => {
                    if (
                      err ||
											!content.equals(/** @type {Buffer} */ (existingContent))
                    ) {
                      return doWrite(content)
                    } else {
                      return alreadyWritten()
                    }
                  }
                )
              }

              return doWrite(content)
            }

            const processMissingFile = () => {
              const content = getContent()

              updateWithReplacementSource(content.length)

              return doWrite(content)
            }

            // if the target file has already been written
            if (targetFileGeneration !== undefined) {
              // check if the Source has been written to this target file
              const writtenGeneration = cacheEntry.writtenTo.get(targetPath)
              if (writtenGeneration === targetFileGeneration) {
                // if yes, we may skip writing the file
                // if it's already there
                // (we assume one doesn't modify files while the Compiler is running, other then removing them)

                if (this._assetEmittingPreviousFiles.has(targetPath)) {
                  // We assume that assets from the last compilation say intact on disk (they are not removed)
                  compilation.updateAsset(file, cacheEntry.sizeOnlySource, {
                    size: cacheEntry.sizeOnlySource.size()
                  })

                  return callback()
                } else {
                  // Settings immutable will make it accept file content without comparing when file exist
                  immutable = true
                }
              } else if (!immutable) {
                if (checkSimilarFile()) return
                // We wrote to this file before which has very likely a different content
                // skip comparing and assume content is different for performance
                // This case happens often during watch mode.
                return processMissingFile()
              }
            }

            if (checkSimilarFile()) return
            if (this.options.output.compareBeforeEmit) {
              this.outputFileSystem.stat(targetPath, (err, stats) => {
                const exists = !err && stats.isFile()

                if (exists) {
                  processExistingFile(stats)
                } else {
                  processMissingFile()
                }
              })
            } else {
              processMissingFile()
            }
          }

          if (targetFile.match(/\/|\\/)) {
            const fs = this.outputFileSystem
            const dir = dirname(fs, join(fs, outputPath, targetFile))
            mkdirp(fs, dir, writeOut)
          } else {
            writeOut()
          }
        },
        err => {
          // Clear map to free up memory
          caseInsensitiveMap.clear()
          if (err) {
            this._assetEmittingPreviousFiles.clear()
            return callback(err)
          }

          this._assetEmittingPreviousFiles = allTargetPaths

          this.hooks.afterEmit.callAsync(compilation, err => {
            if (err) return callback(err)

            return callback()
          })
        }
      )
    }

    this.hooks.emit.callAsync(compilation, err => {
      if (err) return callback(err)
      outputPath = compilation.getPath(this.outputPath, {})
      mkdirp(this.outputFileSystem, outputPath, emitFiles)
    })
  }

  /**
	 * @param {Callback<void>} callback signals when the call finishes
	 * @returns {void}
	 */
  emitRecords (callback) {
    if (this.hooks.emitRecords.isUsed()) {
      if (this.recordsOutputPath) {
        asyncLib.parallel(
          [
            cb => this.hooks.emitRecords.callAsync(cb),
            this._emitRecords.bind(this)
          ],
          err => callback(err)
        )
      } else {
        this.hooks.emitRecords.callAsync(callback)
      }
    } else {
      if (this.recordsOutputPath) {
        this._emitRecords(callback)
      } else {
        callback()
      }
    }
  }

  /**
	 * @param {Callback<void>} callback signals when the call finishes
	 * @returns {void}
	 */
  _emitRecords (callback) {
    const writeFile = () => {
      this.outputFileSystem.writeFile(
        this.recordsOutputPath,
        JSON.stringify(
          this.records,
          (n, value) => {
            if (
              typeof value === 'object' &&
							value !== null &&
							!Array.isArray(value)
            ) {
              const keys = Object.keys(value)
              if (!isSorted(keys)) {
                return sortObject(value, keys)
              }
            }
            return value
          },
          2
        ),
        callback
      )
    }

    const recordsOutputPathDirectory = dirname(
      this.outputFileSystem,
      this.recordsOutputPath
    )
    if (!recordsOutputPathDirectory) {
      return writeFile()
    }
    mkdirp(this.outputFileSystem, recordsOutputPathDirectory, err => {
      if (err) return callback(err)
      writeFile()
    })
  }

  /**
	 * @param {Callback<void>} callback signals when the call finishes
	 * @returns {void}
	 */
  readRecords (callback) {
    if (this.hooks.readRecords.isUsed()) {
      if (this.recordsInputPath) {
        asyncLib.parallel(
          [
            cb => this.hooks.readRecords.callAsync(cb),
            this._readRecords.bind(this)
          ],
          err => callback(err)
        )
      } else {
        this.records = {}
        this.hooks.readRecords.callAsync(callback)
      }
    } else {
      if (this.recordsInputPath) {
        this._readRecords(callback)
      } else {
        this.records = {}
        callback()
      }
    }
  }

  /**
	 * @param {Callback<void>} callback signals when the call finishes
	 * @returns {void}
	 */
  _readRecords (callback) {
    if (!this.recordsInputPath) {
      this.records = {}
      return callback()
    }
    this.inputFileSystem.stat(this.recordsInputPath, err => {
      // It doesn't exist
      // We can ignore this.
      if (err) return callback()

      this.inputFileSystem.readFile(this.recordsInputPath, (err, content) => {
        if (err) return callback(err)

        try {
          this.records = parseJson(content.toString('utf-8'))
        } catch (e) {
          return callback(new Error(`Cannot parse records: ${e.message}`))
        }

        return callback()
      })
    })
  }

  /**
	 * @param {Compilation} compilation the compilation
	 * @param {string} compilerName the compiler's name
	 * @param {number} compilerIndex the compiler's index
	 * @param {OutputOptions=} outputOptions the output options
	 * @param {WebpackPluginInstance[]=} plugins the plugins to apply
	 * @returns {Compiler} a child compiler
	 */
  createChildCompiler (
    compilation,
    compilerName,
    compilerIndex,
    outputOptions,
    plugins
  ) {
    const childCompiler = new Compiler(this.context, {
      ...this.options,
      output: {
        ...this.options.output,
        ...outputOptions
      }
    })
    childCompiler.name = compilerName
    childCompiler.outputPath = this.outputPath
    childCompiler.inputFileSystem = this.inputFileSystem
    childCompiler.outputFileSystem = null
    childCompiler.resolverFactory = this.resolverFactory
    childCompiler.modifiedFiles = this.modifiedFiles
    childCompiler.removedFiles = this.removedFiles
    childCompiler.fileTimestamps = this.fileTimestamps
    childCompiler.contextTimestamps = this.contextTimestamps
    childCompiler.fsStartTime = this.fsStartTime
    childCompiler.cache = this.cache
    childCompiler.compilerPath = `${this.compilerPath}${compilerName}|${compilerIndex}|`
    childCompiler._backCompat = this._backCompat

    const relativeCompilerName = makePathsRelative(
      this.context,
      compilerName,
      this.root
    )
    if (!this.records[relativeCompilerName]) {
      this.records[relativeCompilerName] = []
    }
    if (this.records[relativeCompilerName][compilerIndex]) {
      childCompiler.records = this.records[relativeCompilerName][compilerIndex]
    } else {
      this.records[relativeCompilerName].push((childCompiler.records = {}))
    }

    childCompiler.parentCompilation = compilation
    childCompiler.root = this.root
    if (Array.isArray(plugins)) {
      for (const plugin of plugins) {
        if (plugin) {
          plugin.apply(childCompiler)
        }
      }
    }
    for (const name in this.hooks) {
      if (
        ![
          'make',
          'compile',
          'emit',
          'afterEmit',
          'invalid',
          'done',
          'thisCompilation'
        ].includes(name)
      ) {
        if (childCompiler.hooks[name]) {
          childCompiler.hooks[name].taps = this.hooks[name].taps.slice()
        }
      }
    }

    compilation.hooks.childCompiler.call(
      childCompiler,
      compilerName,
      compilerIndex
    )

    return childCompiler
  }

  isChild () {
    return !!this.parentCompilation
  }

  createCompilation (params) {
    this._cleanupLastCompilation()
    return (this._lastCompilation = new Compilation(this, params))
  }

  /**
	 * @param {CompilationParams} params the compilation parameters
	 * @returns {Compilation} the created compilation
	 */
  newCompilation (params) {
    const compilation = this.createCompilation(params)
    compilation.name = this.name
    compilation.records = this.records
    this.hooks.thisCompilation.call(compilation, params)
    this.hooks.compilation.call(compilation, params)
    return compilation
  }

  createNormalModuleFactory () {
    this._cleanupLastNormalModuleFactory()
    const normalModuleFactory = new NormalModuleFactory({
      context: this.options.context,
      fs: this.inputFileSystem,
      resolverFactory: this.resolverFactory,
      options: this.options.module,
      associatedObjectForCache: this.root,
      layers: this.options.experiments.layers
    })
    this._lastNormalModuleFactory = normalModuleFactory
    this.hooks.normalModuleFactory.call(normalModuleFactory)
    return normalModuleFactory
  }

  createContextModuleFactory () {
    const contextModuleFactory = new ContextModuleFactory(this.resolverFactory)
    this.hooks.contextModuleFactory.call(contextModuleFactory)
    return contextModuleFactory
  }

  newCompilationParams () {
    const params = {
      normalModuleFactory: this.createNormalModuleFactory(),
      contextModuleFactory: this.createContextModuleFactory()
    }
    return params
  }

  /**
	 * @param {Callback<Compilation>} callback signals when the compilation finishes
	 * @returns {void}
	 */
  compile (callback) {
    const params = this.newCompilationParams()
    this.hooks.beforeCompile.callAsync(params, err => {
      if (err) return callback(err)

      this.hooks.compile.call(params)

      const compilation = this.newCompilation(params)

      const logger = compilation.getLogger('webpack.Compiler')

      logger.time('make hook')
      this.hooks.make.callAsync(compilation, err => {
        logger.timeEnd('make hook')
        if (err) return callback(err)

        logger.time('finish make hook')
        this.hooks.finishMake.callAsync(compilation, err => {
          logger.timeEnd('finish make hook')
          if (err) return callback(err)

          process.nextTick(() => {
            logger.time('finish compilation')
            compilation.finish(err => {
              logger.timeEnd('finish compilation')
              if (err) return callback(err)

              logger.time('seal compilation')
              compilation.seal(err => {
                logger.timeEnd('seal compilation')
                if (err) return callback(err)

                logger.time('afterCompile hook')
                this.hooks.afterCompile.callAsync(compilation, err => {
                  logger.timeEnd('afterCompile hook')
                  if (err) return callback(err)

                  return callback(null, compilation)
                })
              })
            })
          })
        })
      })
    })
  }

  /**
	 * @param {Callback<void>} callback signals when the compiler closes
	 * @returns {void}
	 */
  close (callback) {
    if (this.watching) {
      // When there is still an active watching, close this first
      this.watching.close(err => {
        this.close(callback)
      })
      return
    }
    this.hooks.shutdown.callAsync(err => {
      if (err) return callback(err)
      // Get rid of reference to last compilation to avoid leaking memory
      // We can't run this._cleanupLastCompilation() as the Stats to this compilation
      // might be still in use. We try to get rid of the reference to the cache instead.
      this._lastCompilation = undefined
      this._lastNormalModuleFactory = undefined
      this.cache.shutdown(callback)
    })
  }
}

module.exports = Compiler
