const NormalModule = require('webpack/lib/NormalModule');
const Module = require('webpack/lib/Module');

const logMessages = require('./util/log-messages');
const {
  relateNormalPath,
  relateNormalRequest,
  relateNormalPathSet,
  relateNormalLoaders
} = require('./util/relate-context');
const pluginCompat = require('./util/plugin-compat');
const serial = require('./util/serial');

const serialNormalModule4 = serial.serial('NormalModule', {
  constructor: serial.constructed(NormalModule, {
    data: serial.pipe(
      ({freeze: (arg, module) => module, thaw: arg => arg}),
      serial.created({
        type: serial.identity,
        request: serial.request,
        userRequest: serial.request,
        rawRequest: serial.request,
        loaders: serial.loaders,
        resource: serial.path,
        parser: serial.parser,
        generator: serial.generator,
        resolveOptions: serial.identity,
      })
    ),
  }),

  setModuleExtra: ({
    freeze() {},
    thaw(arg, frozen, extra, methods) {
      extra.module = arg;
      return arg;
    },
  }),

  identifier: ({
    freeze(arg, module, extra, methods) {
      return serial.request.freeze(module.identifier(), null, extra, methods);
    },
    thaw(arg) {return arg;},
  }),

  assigned: serial.assigned({
    factoryMeta: serial.identity,
    issuer: serial.pipe(
      ({
        freeze(arg, module) {
          return module.issuer && typeof module.issuer === 'object' ?
            module.issuer.identifier() :
            module.issuer;
        },
        thaw(arg, frozen, extra) {return arg;},
      }),
      serial.request,
      ({
        freeze(arg) {return arg;},
        thaw(arg, frozen, extra) {
          if (extra.compilation.modules) {
            for (const module of extra.compilation.modules) {
              if (module && typeof module.identifier === 'function' && module.identifier() === arg) {
                return module;
              }
            }
            for (const cacheId in extra.compilation.cache) {
              const module = extra.compilation.cache[cacheId];
              if (module && typeof module.identifier === 'function' && module.identifier() === arg) {
                return module;
              }
            }
          }
          return arg;
        },
      })
    ),
    useSourceMap: serial.identity,
    lineToLine: serial.identity,
  }),

  setOriginExtra: ({
    freeze() {},
    thaw(arg, frozen, extra) {
      if (typeof arg.issuer === 'object') {
        extra.origin = arg.issuer;
      }
      return arg;
    },
  }),

  build: serial.assigned({
    built: serial.identity,
    buildTimestamp: serial.identity,
    buildMeta: serial.identity,
    buildInfo: serial.created({
      assets: serial.moduleAssets,
      cacheable: serial.identity,
      contextDependencies: serial.pathSet,
      exportsArgument: serial.identity,
      fileDependencies: serial.pathSet,
      harmonyModule: serial.identity,
      jsonData: serial.identity,
      strict: serial.identity,
    }),
    warnings: serial.moduleWarning,
    errors: serial.moduleError,
    _source: serial.source,
    _buildHash: serial.identity,
    hash: serial.identity,
    _lastSuccessfulBuildMeta: serial.identity,
  }),

  dependencyBlock: serial.dependencyBlock,

  setError: ({
    freeze() {},
    thaw(arg, module, extra) {
      arg.error = arg.errors[0] || null;
      return arg;
    },
  }),

  setSourceExtra: ({
    freeze() {},
    thaw(arg, module, extra) {
      extra.source = arg._source;
      return arg;
    },
  }),

  source: serial.assigned({
    _cachedSource: serial.source,
    _cachedSourceHash: serial.identity,
    renderedHash: serial.identity,
  }),
});

const needRebuild4 = function() {
  if (this.error) {
    this.cacheItem.invalid = true;
    this.cacheItem.invalidReason = 'error building';
    return true;
  }
  const fileHashes = this.__hardSourceFileMd5s;
  const cachedHashes = this.__hardSourceCachedMd5s;
  for (const file of this.buildInfo.fileDependencies) {
    if (!cachedHashes[file] || fileHashes[file] !== cachedHashes[file]) {
      this.cacheItem.invalid = true;
      this.cacheItem.invalidReason = 'md5 mismatch';
      return true;
    }
  }
  for (const dir of this.buildInfo.contextDependencies) {
    if (!cachedHashes[dir] || fileHashes[dir] !== cachedHashes[dir]) {
      this.cacheItem.invalid = true;
      this.cacheItem.invalidReason = 'md5 mismatch';
      return true;
    }
  }
  return false;
};

const serialNormalModule3 = serial.serial('NormalModule', {
  constructor: serial.constructed(NormalModule, {
    request: serial.request,
    userRequest: serial.request,
    rawRequest: serial.request,
    loaders: serial.loaders,
    resource: serial.path,
    parser: serial.parser,
  }),

  setModuleExtra: ({
    freeze() {},
    thaw(arg, frozen, extra, methods) {
      extra.module = arg;
      return arg;
    },
  }),

  // Used internally by HardSource
  identifier: ({
    freeze(arg, module, extra, methods) {
      return serial.request.freeze(module.identifier(), null, extra, methods);
    },
    thaw(arg) {return arg;},
  }),

  assigned: serial.assigned({
    issuer: serial.pipe(
      ({
        freeze(arg, module) {
          return module.issuer && typeof module.issuer === 'object' ?
            module.issuer.identifier() :
            module.issuer;
        },
        thaw(arg, frozen, extra) {return arg;},
      }),
      serial.request,
      ({
        freeze(arg) {return arg;},
        thaw(arg, frozen, extra) {
          if (extra.compilation.modules) {
            for (const module of extra.compilation.modules) {
              if (module && typeof module.identifier === 'function' && module.identifier() === arg) {
                return module;
              }
            }
            for (const cacheId in extra.compilation.cache) {
              const module = extra.compilation.cache[cacheId];
              if (module && typeof module.identifier === 'function' && module.identifier() === arg) {
                return module;
              }
            }
          }
          return arg;
        },
      })
    ),
    useSourceMap: serial.identity,
    lineToLine: serial.identity,
  }),

  setOriginExtra: ({
    freeze() {},
    thaw(arg, frozen, extra) {
      if (typeof arg.issuer === 'object') {
        extra.origin = arg.issuer;
      }
      return arg;
    },
  }),

  build: serial.assigned({
    built: serial.identity,
    buildTimestamp: serial.identity,
    cacheable: serial.identity,
    meta: serial.identity,
    assets: serial.moduleAssets,
    fileDependencies: serial.pathArray,
    contextDependencies: serial.pathArray,
    harmonyModule: serial.identity,
    strict: serial.identity,
    exportsArgument: serial.identity,
    warnings: serial.moduleWarning,
    errors: serial.moduleError,
    _source: serial.source,
  }),

  hash: ({
    freeze(arg, module, extra, methods) {
      return module.getHashDigest(extra.compilation.dependencyTemplates);
    },
    thaw(arg) {return arg;},
  }),

  dependencyBlock: serial.dependencyBlock,

  setError: ({
    freeze() {},
    thaw(arg, module, extra) {
      arg.error = arg.errors[0] || null;
      return arg;
    },
  }),

  setSourceExtra: ({
    freeze() {},
    thaw(arg, module, extra) {
      extra.source = arg._source;
      return arg;
    },
  }),

  source: serial.assigned({
    _cachedSource: serial.created({
      source: serial.source,
      hash: serial.identity,
    }),
  }),
});

const needRebuild3 = function() {
  if (this.error) {
    this.cacheItem.invalid = true;
    this.cacheItem.invalidReason = 'error building';
    return true;
  }
  const fileHashes = this.__hardSourceFileMd5s;
  const cachedHashes = this.__hardSourceCachedMd5s;
  for (const file of this.fileDependencies) {
    if (!cachedHashes[file] || fileHashes[file] !== cachedHashes[file]) {
      this.cacheItem.invalid = true;
      this.cacheItem.invalidReason = 'md5 mismatch';
      return true;
    }
  }
  for (const dir of this.contextDependencies) {
    if (!cachedHashes[dir] || fileHashes[dir] !== cachedHashes[dir]) {
      this.cacheItem.invalid = true;
      this.cacheItem.invalidReason = 'md5 mismatch';
      return true;
    }
  }
  return false;
}

const cacheable = module => module.buildInfo ? module.buildInfo.cacheable : module.cacheable;

function HardNormalModulePlugin(options) {
  this.options = options || {};
}

HardNormalModulePlugin.prototype.apply = function(compiler) {
  const schema = this.options.schema;

  let serialNormalModule = serialNormalModule4;
  let needRebuild = needRebuild4;
  if (schema < 4) {
    serialNormalModule = serialNormalModule3;
    needRebuild = needRebuild3;
  }

  let createHash;
  if (schema >= 4) {
    createHash = require("webpack/lib/util/createHash");
  }

  var freeze, mapFreeze;
  var _methods;

  pluginCompat.tap(compiler, '_hardSourceMethods', 'HardNormalModulePlugin', function(methods) {
    _methods = methods;

    // store = methods.store;
    // fetch = methods.fetch;
    freeze = methods.freeze;
    // thaw = methods.thaw;
    mapFreeze = methods.mapFreeze;
    // mapThaw = methods.mapThaw;
  });

  pluginCompat.tap(compiler, 'compilation', 'HardNormalModulePlugin', compilation => {
    pluginCompat.tap(compilation, 'succeedModule', 'HardNormalModulePlugin', module => {
      if (module instanceof NormalModule) {
        try {
          module._dependencyBlock = freeze('DependencyBlock', null, module, {
            module: module,
            parent: module,
            compilation: compilation,
          });
        }
        catch (e) {
          logMessages.moduleFreezeError(compilation, module, e);
        }
      }
    });
  });

  pluginCompat.tap(compiler, '_hardSourceFreezeModule', 'HardNormalModulePlugin', function(frozen, module, extra) {
    // Set hash if it was not set.
    if (schema === 4 && module instanceof NormalModule && !module.hash) {
      const outputOptions = extra.compilation.outputOptions;
      const hashFunction = outputOptions.hashFunction;
      const hashDigest = outputOptions.hashDigest;
      const hashDigestLength = outputOptions.hashDigestLength;

      if (module._initBuildHash) {
        module._initBuildHash(extra.compilation);
      }

      const moduleHash = createHash(hashFunction);
      module.updateHash(moduleHash);
      module.hash = moduleHash.digest(hashDigest);
      module.renderedHash = module.hash.substr(0, hashDigestLength);
      if (module._cachedSource) {
        module._cachedSourceHash = module.getHashDigest(extra.compilation.dependencyTemplates);
      }
    }

    if (
      module.request &&
      cacheable(module) &&
      module instanceof NormalModule &&
      (
        !frozen ||
        schema >= 4 &&
        module.hash !== frozen.build.hash ||
        schema < 4 &&
        module.getHashDigest(extra.compilation.dependencyTemplates) !== frozen.hash
      )
    ) {
      var compilation = extra.compilation;

      if (module.cacheItem) {
        module.cacheItem.invalid = false;
        module.cacheItem.invalidReason = null;
      }
      const f = serialNormalModule.freeze(null, module, {
        module: module,
        compilation: compilation,
      }, _methods);
      // The saved dependencies may not be the ones derived in the hash. This is
      // alright, in such a case the dependencies were altered before the source
      // was rendered. The dependencies should be modified a second time, if
      // they are in the same way they'll match. If they are not modified in the
      // same way, then it'll correctly rerender.
      if (module._dependencyBlock) {
        f.dependencyBlock = module._dependencyBlock;
      }
      return f;
    }

    return frozen;
  });

  pluginCompat.tap(compiler, '_hardSourceThawModule', 'HardNormalModulePlugin thaw', function(module, frozen, _extra) {
    if (frozen.type === 'NormalModule') {
      const m = serialNormalModule.thaw(null, frozen, {
        state: {imports: {}},
        compilation: _extra.compilation,
        normalModuleFactory: _extra.normalModuleFactory,
      }, _methods);

      m.cacheItem = frozen;
      m.__hardSourceFileMd5s = _extra.compilation.__hardSourceFileMd5s;
      m.__hardSourceCachedMd5s = _extra.compilation.__hardSourceCachedMd5s;
      m.needRebuild = needRebuild;

      // Unbuild if there is no cache. The module will be rebuilt. Not
      // unbuilding will lead to double dependencies.
      if (schema === 4 && !_extra.compilation.cache) {
        m.unbuild();
      }
      // Side load into the cache if something for this identifier isn't already
      // there.
      else if (_extra.compilation.cache && !_extra.compilation.cache['m' + m.identifier()]) {
        _extra.compilation.cache['m' + m.identifier()] = m;
      }

      return m;
    }
    return module;
  });
};

module.exports = HardNormalModulePlugin;