text: const crypto = require('crypto');
  const path = require('path');
  
  const lodash = require('lodash');
  
  const bulkFsTask = require('./util/bulk-fs-task');
  const pluginCompat = require('./util/plugin-compat');
  const promisify = require('./util/promisify');
  const values = require('./util/Object.values');
  const { parityCacheFromCache, pushParityWriteOps } = require('./util/parity');
  
  class Md5Cache {
    apply(compiler) {
      let md5Cache = {};
      let parityCache = {};
  
      const fileMd5s = {};
      const cachedMd5s = {};
      let fileTimestamps = {};
      const contextMd5s = {};
      let contextTimestamps = {};
  
      let md5CacheSerializer;
  
      let latestStats = {};
      let latestMd5s = {};
      let unbuildMd5s = {};
  
      let fileDependencies = [];
      let contextDependencies = [];
  
      let stat;
      let readdir;
      let readFile;
      let mtime;
      let md5;
      let fileStamp;
      let contextStamp;
      let contextStamps;
  
      function bindFS() {
        stat = promisify(compiler.inputFileSystem.stat, {
          context: compiler.inputFileSystem,
        });
        // stat = promisify(fs.stat, {context: fs});
  
        readdir = promisify(compiler.inputFileSystem.readdir, {
          context: compiler.inputFileSystem,
        });
  
        readFile = promisify(compiler.inputFileSystem.readFile, {
          context: compiler.inputFileSystem,
        });
  
        mtime = file =>
          stat(file)
            .then(stat => +stat.mtime)
            .catch(() => 0);
  
        md5 = file =>
          readFile(file)
            .then(contents =>
              crypto
                .createHash('md5')
                .update(contents, 'utf8')
                .digest('hex'),
            )
            .catch(() => '');
  
        fileStamp = (file, stats) => {
          if (compiler.__hardSource_fileTimestamps[file]) {
            return compiler.__hardSource_fileTimestamps[file];
          } else {
            if (!stats[file]) {
              stats[file] = stat(file);
            }
            return stats[file].then(stat => {
              const mtime = +stat.mtime;
              compiler.__hardSource_fileTimestamps[file] = mtime;
              return mtime;
            });
          }
        };
  
        contextStamp = (dir, stats) => {
          const context = {};
  
          let selfTime = 0;
  
          function walk(dir) {
            return readdir(dir)
              .then(items =>
                Promise.all(
                  items.map(item => {
                    const file = path.join(dir, item);
                    if (!stats[file]) {
                      stats[file] = stat(file);
                    }
                    return stats[file].then(
                      stat => {
                        if (stat.isDirectory()) {
                          return walk(path.join(dir, item)).then(items2 =>
                            items2.map(item2 => path.join(item, item2)),
                          );
                        }
                        if (+stat.mtime > selfTime) {
                          selfTime = +stat.mtime;
                        }
                        return item;
                      },
                      () => {
                        return;
                      },
                    );
                  }),
                ),
              )
              .catch(() => [])
              .then(items =>
                items
                  .reduce((carry, item) => carry.concat(item), [])
                  .filter(Boolean),
              );
          }
  
          return walk(dir).then(items => {
            items.sort();
            const selfHash = crypto.createHash('md5');
            items.forEach(item => {
              selfHash.update(item);
            });
            context.mtime = selfTime;
            context.hash = selfHash.digest('hex');
            return context;
          });
        };
  
        contextStamps = (contextDependencies, stats) => {
          stats = stats || {};
          const contexts = {};
          contextDependencies.forEach(context => {
            contexts[context] = { files: [], mtime: 0, hash: '' };
          });
  
          const compilerContextTs = compiler.contextTimestamps;
  
          contextDependencies.forEach(contextPath => {
            const _context = contextStamp(contextPath, stats);
            if (!_context.then) {
              contexts[contextPath] = _context;
            } else {
              contexts[contextPath] = _context.then(context => {
                contexts[contextPath] = context;
                return context;
              });
            }
          });
          return contexts;
        };
      }
  
      if (compiler.inputFileSystem) {
        bindFS();
      } else {
        pluginCompat.tap(
          compiler,
          'afterEnvironment',
          'HardSource - Md5Cache',
          bindFS,
        );
      }
  
      pluginCompat.tap(
        compiler,
        '_hardSourceCreateSerializer',
        'HardSource - Md5Cache',
        (cacheSerializerFactory, cacheDirPath) => {
          md5CacheSerializer = cacheSerializerFactory.create({
            name: 'md5',
            type: 'data',
            autoParse: true,
            cacheDirPath,
          });
        },
      );
  
      pluginCompat.tap(
        compiler,
        '_hardSourceResetCache',
        'HardSource - Md5Cache',
        () => {
          md5Cache = {};
          parityCache = {};
          fileTimestamps = {};
          contextTimestamps = {};
        },
      );
  
      pluginCompat.tapPromise(
        compiler,
        '_hardSourceReadCache',
        'HardSource - Md5Cache',
        ({ contextKeys, contextNormalPath }) =>
          md5CacheSerializer
            .read()
            .then(_md5Cache => {
              Object.keys(_md5Cache).forEach(key => {
                if (key.startsWith('__hardSource_parityToken')) {
                  parityCache[key] = _md5Cache[key];
                  delete _md5Cache[key];
                }
              });
              return _md5Cache;
            })
            .then(contextKeys(compiler, contextNormalPath))
            .then(_md5Cache => {
              Object.keys(_md5Cache).forEach(key => {
                if (typeof _md5Cache[key] === 'string') {
                  _md5Cache[key] = JSON.parse(_md5Cache[key]);
                }
  
                if (_md5Cache[key] && _md5Cache[key].hash) {
                  cachedMd5s[key] = _md5Cache[key].hash;
                }
              });
              md5Cache = _md5Cache;
            })
            .then(() => {
              const dependencies = Object.keys(md5Cache);
              fileDependencies = dependencies.filter(
                file => md5Cache[file].isFile,
              );
              contextDependencies = dependencies.filter(
                file => md5Cache[file].isDirectory,
              );
            }),
      );
  
      pluginCompat.tap(
        compiler,
        '_hardSourceParityCache',
        'HardSource - Md5Cache',
        parityRoot => {
          parityCacheFromCache('Md5', parityRoot, parityCache);
        },
      );
  
      pluginCompat.tapPromise(
        compiler,
        '_hardSourceVerifyCache',
        'HardSource - Md5Cache',
        () => {
          latestStats = {};
          latestMd5s = {};
          unbuildMd5s = {};
  
          const stats = {};
          // var md5s = latestMd5s;
  
          // Prepare objects to mark md5s to delete if they are not used.
          for (const key in cachedMd5s) {
            unbuildMd5s[key] = null;
          }
  
          return Promise.all([
            (() => {
              const compilerFileTs = (compiler.__hardSource_fileTimestamps = {});
              const fileTs = (fileTimestamps = {});
  
              return bulkFsTask(fileDependencies, (file, task) => {
                if (compiler.__hardSource_fileTimestamps[file]) {
                  return compiler.__hardSource_fileTimestamps[file];
                } else {
                  compiler.inputFileSystem.stat(
                    file,
                    task((err, value) => {
                      if (err) {
                        return 0;
                      }
  
                      const mtime = +value.mtime;
                      compiler.__hardSource_fileTimestamps[file] = mtime;
                      return mtime;
                    }),
                  );
                }
              }).then(mtimes => {
                const bulk = lodash.zip(fileDependencies, mtimes);
                return bulkFsTask(bulk, (item, task) => {
                  const file = item[0];
                  const mtime = item[1];
  
                  fileTs[file] = mtime || 0;
                  if (!compiler.__hardSource_fileTimestamps[file]) {
                    compiler.__hardSource_fileTimestamps[file] = mtime;
                  }
  
                  compiler.inputFileSystem.readFile(
                    file,
                    task(function(err, body) {
                      if (err) {
                        fileMd5s[file] = '';
                        return;
                      }
  
                      const hash = crypto
                        .createHash('md5')
                        .update(body, 'utf8')
                        .digest('hex');
  
                      fileMd5s[file] = hash;
                    }),
                  );
                });
              });
            })(),
            (() => {
              compiler.contextTimestamps = compiler.contextTimestamps || {};
              const contextTs = (contextTimestamps = {});
              const contexts = contextStamps(contextDependencies, stats);
              return Promise.all(values(contexts)).then(function() {
                for (var contextPath in contexts) {
                  var context = contexts[contextPath];
  
                  if (!compiler.contextTimestamps[contextPath]) {
                    compiler.contextTimestamps[contextPath] = context.mtime;
                  }
                  contextTimestamps[contextPath] = context.mtime;
                  fileMd5s[contextPath] = context.hash;
                }
              });
            })(),
          ]);
        },
      );
  
      pluginCompat.tap(
        compiler,
        'compilation',
        'HardSource - Md5Cache',
        compilation => {
          compilation.__hardSourceFileMd5s = fileMd5s;
          compilation.__hardSourceCachedMd5s = cachedMd5s;
          compilation.__hardSourceFileTimestamps = fileTimestamps;
        },
      );
  
      pluginCompat.tapPromise(
        compiler,
        '_hardSourceWriteCache',
        'HardSource - Md5Cache',
        (compilation, { relateNormalPath, contextNormalPath }) => {
          const moduleOps = [];
          const dataOps = [];
          const md5Ops = [];
          const assetOps = [];
          const moduleResolveOps = [];
          const missingOps = [];
          const resolverOps = [];
  
          let buildingMd5s = {};
  
          function buildMd5Ops(dependencies) {
            dependencies.forEach(file => {
              function updateMd5CacheItem(value) {
                if (
                  !md5Cache[file] ||
                  (md5Cache[file] && md5Cache[file].hash !== value.hash)
                ) {
                  md5Cache[file] = value;
                  cachedMd5s[file] = value.hash;
  
                  md5Ops.push({
                    key: relateNormalPath(compiler, file),
                    value: value,
                  });
                } else if (
                  !value.mtime &&
                  md5Cache[file] &&
                  md5Cache[file].mtime !== value.mtime
                ) {
                  md5Cache[file] = value;
                  cachedMd5s[file] = value.hash;
                }
                
                return value;
              }
  
              const building = buildingMd5s[file];
              if (!building.then) {
                updateMd5CacheItem(building);
              } else {
                buildingMd5s[file] = building.then(updateMd5CacheItem);
              }
            });
          }
  
          const fileDependencies = Array.from(compilation.fileDependencies).map(
            file => contextNormalPath(compiler, file),
          );
  
          const MD5_TIME_PRECISION_BUFFER = 2000;
  
          fileDependencies.forEach(file => {
            if (buildingMd5s[file]) {
              return;
            }
            delete unbuildMd5s[file];
  
            if (fileMd5s[file]) {
              buildingMd5s[file] = {
                // Subtract a small buffer from now for file systems that record
                // lower precision mtimes.
                mtime: Date.now() - MD5_TIME_PRECISION_BUFFER,
                hash: fileMd5s[file],
                isFile: true,
                isDirectory: false,
              };
            } else {
              buildingMd5s[file] = md5(file).then(hash => ({
                mtime: Date.now() - MD5_TIME_PRECISION_BUFFER,
                hash,
                isFile: true,
                isDirectory: false,
              }));
            }
          });
  
          buildMd5Ops(fileDependencies);
  
          const contextDependencies = Array.from(
            compilation.contextDependencies,
          ).map(file => contextNormalPath(compiler, file));
  
          const contexts = contextStamps(contextDependencies);
          contextDependencies.forEach(file => {
            if (buildingMd5s[file]) {
              return;
            }
            delete unbuildMd5s[file];
  
            let context = contexts[file];
            if (!context.then) {
              // Subtract a small buffer from now for file systems that record lower
              // precision mtimes.
              context.mtime = Date.now() - MD5_TIME_PRECISION_BUFFER;
              context.isFile = false;
              context.isDirectory = true;
            } else {
              context = context.then(context => {
                context.mtime = Date.now() - MD5_TIME_PRECISION_BUFFER;
                context.isFile = false;
                context.isDirectory = true;
                return context;
              });
            }
            buildingMd5s[file] = context;
          });
  
          buildMd5Ops(contextDependencies);
  
          const writeMd5Ops = Promise.all(
            Object.keys(buildingMd5s).map(key => buildingMd5s[key]),
          ).then(() => {
            if (!compilation.compiler.parentCompilation) {
              for (const key in unbuildMd5s) {
                md5Ops.push({
                  key: relateNormalPath(compiler, key),
                  value: unbuildMd5s[key],
                });
              }
            }
  
            pushParityWriteOps(compilation, md5Ops);
          });
  
          return writeMd5Ops.then(() => md5CacheSerializer.write(md5Ops));
        },
      );
    }
  }
  
  module.exports = Md5Cache;
  