import { promises as fs } from 'fs';
import originalFs from 'original-fs';
import fse from 'fs-extra';
import { extractFull } from 'node-7z';
import jimp from 'jimp/es';
import makeDir from 'make-dir';
import { promisify } from 'util';
import { ipcRenderer } from 'electron';
import path from 'path';
import crypto from 'crypto';
import { exec, spawn } from 'child_process';
import {
  MC_LIBRARIES_URL,
  FABRIC,
  FORGE,
  LATEST_JAVA_VERSION
} from '../../../common/utils/constants';

import {
  addQuotes,
  removeDuplicates,
  sortByForgeVersionDesc
} from '../../../common/utils';
import { getAddon, getAddonFile, mcGetPlayerSkin } from '../../../common/api';
import { downloadFile } from './downloader';
import browserDownload from '../../../common/utils/browserDownload';
import { REQUIRED_JAVA_ARGS } from './constants';

export const isDirectory = source =>
  fs.lstat(source).then(r => r.isDirectory());

export const getDirectories = async source => {
  const dirs = await fs.readdir(source);
  return Promise.all(
    dirs
      .map(name => path.join(source, name))
      .filter(isDirectory)
      .map(dir => path.basename(dir))
  );
};

export const mavenToArray = (s, nativeString, forceExt) => {
  const pathSplit = s.split(':');
  const fileName = pathSplit[3]
    ? `${pathSplit[2]}-${pathSplit[3]}`
    : pathSplit[2];
  const finalFileName = fileName.includes('@')
    ? fileName.replace('@', '.')
    : `${fileName}${nativeString || ''}${forceExt || '.jar'}`;
  const initPath = pathSplit[0]
    .split('.')
    .concat(pathSplit[1])
    .concat(pathSplit[2].split('@')[0])
    .concat(`${pathSplit[1]}-${finalFileName}`);
  return initPath;
};

export const convertOSToMCFormat = ElectronFormat => {
  switch (ElectronFormat) {
    case 'win32':
      return 'windows';
    case 'darwin':
      return 'osx';
    case 'linux':
      return 'linux';
    default:
      return false;
  }
};

export const convertOSToJavaFormat = ElectronFormat => {
  switch (ElectronFormat) {
    case 'win32':
      return 'windows';
    case 'darwin':
      return 'mac';
    case 'linux':
      return 'linux';
    default:
      return false;
  }
};

export const convertArchToJavaFormat = ElectronFormat => {
  switch (ElectronFormat) {
    case 'x64':
      return 'x64';
    case 'ia32':
      return 'x32';
    case 'arm':
      return 'arm';
    case 'arm64':
      return 'aarch64';
    default:
      return false;
  }
};

export const skipLibrary = lib => {
  let skip = false;
  if (lib.rules) {
    skip = true;
    lib.rules.forEach(({ action, os, features }) => {
      if (features) return true;
      if (
        action === 'allow' &&
        ((os && os.name === convertOSToMCFormat(process.platform)) || !os)
      ) {
        skip = false;
      }
      if (
        action === 'disallow' &&
        ((os && os.name === convertOSToMCFormat(process.platform)) || !os)
      ) {
        skip = true;
      }
    });
  }
  return skip;
};

export const librariesMapper = (libraries, librariesPath) => {
  return removeDuplicates(
    libraries
      .filter(v => !skipLibrary(v))
      .reduce((acc, lib) => {
        const tempArr = [];
        // Forge libs
        if (lib.downloads && lib.downloads.artifact) {
          let { url } = lib.downloads.artifact;
          // Handle special case for forge universal where the url is "".
          if (lib.downloads.artifact.url === '') {
            url = `https://files.minecraftforge.net/${mavenToArray(
              lib.name
            ).join('/')}`;
          }
          tempArr.push({
            url,
            path: path.join(librariesPath, lib.downloads.artifact.path),
            sha1: lib.downloads.artifact.sha1,
            name: lib.name
          });
        }

        const native = (
          (lib?.natives &&
            lib?.natives[convertOSToMCFormat(process.platform)]) ||
          ''
        ).replace(
          '${arch}', // eslint-disable-line no-template-curly-in-string
          '64'
        );

        // Vanilla native libs
        if (native && lib?.downloads?.classifiers[native]) {
          tempArr.push({
            url: lib.downloads.classifiers[native].url,
            path: path.join(
              librariesPath,
              lib.downloads.classifiers[native].path
            ),
            sha1: lib.downloads.classifiers[native].sha1,
            natives: true,
            name: lib.name
          });
        }
        if (tempArr.length === 0) {
          tempArr.push({
            url: `${lib.url || `${MC_LIBRARIES_URL}/`}${mavenToArray(
              lib.name,
              native && `-${native}`
            ).join('/')}`,
            path: path.join(librariesPath, ...mavenToArray(lib.name, native)),
            ...(native && { natives: true }),
            name: lib.name
          });
        }
        // Patch log4j versions https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2021-44228
        for (const k in tempArr) {
          if (tempArr[k]?.url?.includes('log4j')) {
            // Get rid of all log4j aside from official
            if (!tempArr[k].url.includes('libraries.minecraft.net')) {
              tempArr[k] = null;
              continue;
            }

            if (tempArr[k].url.includes('2.0-beta9')) {
              tempArr[k] = {
                url: tempArr[k].url
                  .replace(
                    'libraries.minecraft.net',
                    'cdn.gdlauncher.com/maven'
                  )
                  .replace(/2.0-beta9/g, '2.0-beta9-fixed'),
                path: tempArr[k].path.replace(/2.0-beta9/g, '2.0-beta9-fixed'),
                sha1: tempArr[k].url.includes('log4j-api')
                  ? 'b61eaf2e64d8b0277e188262a8b771bbfa1502b3'
                  : '677991ea2d7426f76309a73739cecf609679492c',
                name: tempArr[k].name
              };
            } else {
              const splitName = tempArr[k].name.split(':');
              splitName[splitName.length - 1] = '2.15.0';
              const patchedName = splitName.join(':');

              // Assuming we can use 2.15
              tempArr[k] = {
                url: `https://cdn.gdlauncher.com/maven/${mavenToArray(
                  patchedName,
                  native
                ).join(path.sep)}`,
                path: path.join(
                  librariesPath,
                  ...mavenToArray(patchedName, native)
                ),
                sha1: tempArr[k].url.includes('log4j-api')
                  ? '42319af9991a86b4475ab3316633a3d03e2d29e1'
                  : '9bd89149d5083a2a3ab64dcc88b0227da14152ec'
              };
            }
          }
        }

        return acc.concat(tempArr.filter(_ => _));
      }, []),
    'url'
  );
};

export const getFilteredVersions = (
  vanillaManifest,
  forgeManifest,
  fabricManifest
) => {
  const versions = [
    {
      value: 'vanilla',
      label: 'Vanilla',
      children: [
        {
          value: 'release',
          label: 'Releases',
          children: vanillaManifest.versions
            .filter(v => v.type === 'release')
            .map(v => ({
              value: v.id,
              label: v.id
            }))
        },
        {
          value: 'snapshot',
          label: 'Snapshots',
          children: vanillaManifest.versions
            .filter(v => v.type === 'snapshot')
            .map(v => ({
              value: v.id,
              label: v.id
            }))
        },
        {
          value: 'old_beta',
          label: 'Old Beta',
          children: vanillaManifest.versions
            .filter(v => v.type === 'old_beta')
            .map(v => ({
              value: v.id,
              label: v.id
            }))
        },
        {
          value: 'old_alpha',
          label: 'Old Alpha',
          children: vanillaManifest.versions
            .filter(v => v.type === 'old_alpha')
            .map(v => ({
              value: v.id,
              label: v.id
            }))
        }
      ]
    },
    {
      value: 'forge',
      label: 'Forge',
      children: Object.entries(forgeManifest).map(([k, v]) => ({
        value: k,
        label: k,
        children: v.sort(sortByForgeVersionDesc).map(child => ({
          value: child,
          label: child.split('-')[1]
        }))
      }))
    },
    {
      value: 'fabric',
      label: 'Fabric',
      children: [
        {
          value: 'release',
          label: 'Releases',
          children: fabricManifest.game
            .filter(v => v.stable)
            .map(v => ({
              value: v.version,
              label: v.version,
              children: fabricManifest.loader.map(c => ({
                value: c.version,
                label: c.version
              }))
            }))
        },
        {
          value: 'snapshot',
          label: 'Snapshots',
          children: fabricManifest.game
            .filter(v => !v.stable)
            .map(v => ({
              value: v.version,
              label: v.version,
              children: fabricManifest.loader.map(c => ({
                value: c.version,
                label: c.version
              }))
            }))
        }
      ]
    }
  ];
  return versions;
};

export const isLatestJavaDownloaded = async (
  meta,
  userData,
  retry,
  version = 8
) => {
  const javaOs = convertOSToJavaFormat(process.platform);
  const javaArch = convertArchToJavaFormat(process.arch);
  let log = null;

  const isJavaLatest = version === LATEST_JAVA_VERSION;

  const manifest = isJavaLatest ? meta.javaLatest : meta.java;

  const javaMeta = manifest.find(
    v =>
      v.os === javaOs &&
      v.architecture === javaArch &&
      (v.binary_type === 'jre' || v.binary_type === 'jdk')
  );
  const javaFolder = path.join(
    userData,
    'java',
    javaMeta.version_data.openjdk_version
  );
  // Check if it's downloaded, if it's latest version and if it's a valid download
  let isValid = true;

  const javaExecutable = path.join(
    javaFolder,
    'bin',
    `java${javaOs === 'windows' ? '.exe' : ''}`
  );
  try {
    await fs.access(javaFolder);
    log = await promisify(exec)(`"${javaExecutable}" -version`);
  } catch (err) {
    if (retry) {
      if (process.platform !== 'win32') {
        try {
          await promisify(exec)(`chmod +x "${javaExecutable}"`);
          await promisify(exec)(`chmod 755 "${javaExecutable}"`);
        } catch {
          // swallow error
        }
      }

      return isLatestJavaDownloaded(meta, userData, null, version);
    }

    isValid = false;
  }
  // Return stderr because that garbage of a language which is java
  // outputs the result of the version command to the error stream
  // https://stackoverflow.com/questions/13483443/why-does-java-version-go-to-stderr
  return { isValid, log: log?.stderr };
};

export const get7zPath = async () => {
  // Get userData from ipc because we can't always get this from redux
  let baseDir = await ipcRenderer.invoke('getExecutablePath');
  const isDev = process.env.NODE_ENV === 'development';
  if (isDev) {
    baseDir = path.resolve(baseDir, '../../');
    if (process.platform === 'win32') {
      baseDir = path.join(baseDir, '7zip-bin/win/x64');
    } else if (process.platform === 'linux') {
      baseDir = path.join(baseDir, '7zip-bin/linux/x64');
    } else if (process.platform === 'darwin') {
      baseDir = path.resolve(baseDir, '../../../', '7zip-bin/mac/x64');
    }
  }
  if (process.platform === 'linux') {
    return path.join(baseDir, '7za');
  }
  if (process.platform === 'darwin') {
    return path.resolve(baseDir, isDev ? '' : '../', '7za');
  }
  return path.join(baseDir, '7za.exe');
};

get7zPath();

export const extractAll = async (
  source,
  destination,
  args = {},
  funcs = {}
) => {
  const sevenZipPath = await get7zPath();
  const extraction = extractFull(source, destination, {
    ...args,
    yes: true,
    $bin: sevenZipPath,
    $spawnOptions: { shell: true }
  });
  let extractedParentDir = null;
  await new Promise((resolve, reject) => {
    if (funcs.progress) {
      extraction.on('progress', ({ percent }) => {
        funcs.progress(percent);
      });
    }
    extraction.on('data', data => {
      if (!extractedParentDir) {
        [extractedParentDir] = data.file.split('/');
      }
    });
    extraction.on('end', () => {
      funcs.end?.();
      resolve(extractedParentDir);
    });
    extraction.on('error', err => {
      funcs.error?.();
      reject(err);
    });
  });
  return { extraction, extractedParentDir };
};

export const extractNatives = async (libraries, instancePath) => {
  const extractLocation = path.join(instancePath, 'natives');
  await Promise.all(
    libraries
      .filter(l => l.natives)
      .map(async l => {
        await extractAll(l.path, extractLocation, {
          $raw: ['-xr!META-INF']
        });
      })
  );
};

export const copyAssetsToResources = async assets => {
  await Promise.all(
    assets.map(async asset => {
      try {
        await fs.access(asset.resourcesPath);
      } catch {
        await makeDir(path.dirname(asset.resourcesPath));
        await fs.copyFile(asset.path, asset.resourcesPath);
      }
    })
  );
};

export const copyAssetsToLegacy = async assets => {
  await Promise.all(
    assets.map(async asset => {
      try {
        await fs.access(asset.legacyPath);
      } catch {
        await makeDir(path.dirname(asset.legacyPath));
        await fs.copyFile(asset.path, asset.legacyPath);
      }
    })
  );
};

const hiddenToken = '__HIDDEN_TOKEN__';
export const getJVMArguments112 = (
  libraries,
  mcjar,
  instancePath,
  assetsPath,
  mcJson,
  account,
  memory,
  resolution,
  hideAccessToken,
  jvmOptions = []
) => {
  const needsQuote = process.platform !== 'win32';
  const args = [];
  args.push('-cp');

  args.push(
    [...libraries, mcjar]
      .filter(l => !l.natives)
      .map(l => `${addQuotes(needsQuote, l.path)}`)
      .join(process.platform === 'win32' ? ';' : ':')
  );

  const javaArgs = jvmOptions.filter(Boolean);

  // if (process.platform === "darwin") {
  //   args.push("-Xdock:name=instancename");
  //   args.push("-Xdock:icon=instanceicon");
  // }

  args.push(`-Xmx${memory}m`);
  args.push(`-Xms${memory}m`);
  args.push(...REQUIRED_JAVA_ARGS.split(' '));
  if (javaArgs.length > 0) args.push(...javaArgs);
  args.push(
    `-Djava.library.path=${addQuotes(
      needsQuote,
      path.join(instancePath, 'natives')
    )}`
  );
  args.push(
    `-Dminecraft.applet.TargetDirectory=${addQuotes(needsQuote, instancePath)}`
  );
  if (mcJson.logging) {
    args.push(mcJson?.logging?.client?.argument || '');
  }

  args.push(mcJson.mainClass);

  const mcArgs = mcJson.minecraftArguments.split(' ');
  const argDiscovery = /\${*(.*)}/;

  for (let i = 0; i < mcArgs.length; i += 1) {
    if (argDiscovery.test(mcArgs[i])) {
      const identifier = mcArgs[i].match(argDiscovery)[1];
      let val = null;
      switch (identifier) {
        case 'auth_player_name':
          val = account.selectedProfile.name.trim();
          break;
        case 'version_name':
          val = mcJson.id;
          break;
        case 'game_directory':
          val = `${addQuotes(needsQuote, instancePath)}`;
          break;
        case 'assets_root':
          val = `${addQuotes(needsQuote, assetsPath)}`;
          break;
        case 'game_assets':
          val = `${path.join(assetsPath, 'virtual', 'legacy')}`;
          break;
        case 'assets_index_name':
          val = mcJson.assets;
          break;
        case 'auth_uuid':
          val = account.selectedProfile.id.trim();
          break;
        case 'auth_access_token':
          val = hideAccessToken ? hiddenToken : account.accessToken;
          break;
        case 'auth_session':
          val = hideAccessToken ? hiddenToken : account.accessToken;
          break;
        case 'user_type':
          val = 'mojang';
          break;
        case 'version_type':
          val = mcJson.type;
          break;
        case 'user_properties':
          val = '{}';
          break;
        default:
          break;
      }
      if (val != null) {
        mcArgs[i] = val;
      }
      if (typeof args[i] === 'string' && !needsQuote) {
        args[i] = args[i].replaceAll('"', '');
      }
    }
  }

  args.push(...mcArgs);

  if (resolution) {
    args.push(`--width=${resolution.width}`);
    args.push(`--height=${resolution.height}`);
  }

  return args;
};

export const getJVMArguments113 = (
  libraries,
  mcjar,
  instancePath,
  assetsPath,
  mcJson,
  account,
  memory,
  resolution,
  hideAccessToken,
  jvmOptions = []
) => {
  const argDiscovery = /\${*(.*)}/;
  let args = mcJson.arguments.jvm.filter(v => !skipLibrary(v));
  const needsQuote = process.platform !== 'win32';

  const javaArgs = jvmOptions.filter(Boolean);
  // if (process.platform === "darwin") {
  //   args.push("-Xdock:name=instancename");
  //   args.push("-Xdock:icon=instanceicon");
  // }
  args.push(`-Xmx${memory}m`);
  args.push(`-Xms${memory}m`);
  args.push(
    `-Dminecraft.applet.TargetDirectory=${addQuotes(needsQuote, instancePath)}`
  );
  if (mcJson.logging) {
    args.push(mcJson?.logging?.client?.argument || '');
  }

  args.push(...REQUIRED_JAVA_ARGS.split(' '));
  if (javaArgs.length > 0) args.push(...javaArgs);

  // Eventually inject additional arguments (from 1.17 (?))
  if (mcJson?.forge?.arguments?.jvm) {
    args.push(...mcJson.forge.arguments.jvm);
  }

  args.push(mcJson.mainClass);

  args.push(...mcJson.arguments.game.filter(v => !skipLibrary(v)));

  for (let i = 0; i < args.length; i += 1) {
    if (typeof args[i] === 'object' && args[i].rules) {
      if (typeof args[i].value === 'string') {
        args[i] = `${addQuotes(needsQuote, args[i].value)}`;
      } else if (typeof args[i].value === 'object') {
        args.splice(i, 1, ...args[i].value.map(v => `${v}`));
      }
      i -= 1;
    } else if (typeof args[i] === 'string') {
      if (argDiscovery.test(args[i])) {
        const identifier = args[i].match(argDiscovery)[1];
        let val = null;
        switch (identifier) {
          case 'auth_player_name':
            val = account.selectedProfile.name.trim();
            break;
          case 'version_name':
            val = mcJson.id;
            break;
          case 'game_directory':
            val = `${addQuotes(needsQuote, instancePath)}`;
            break;
          case 'assets_root':
            val = `${addQuotes(needsQuote, assetsPath)}`;
            break;
          case 'assets_index_name':
            val = mcJson.assets;
            break;
          case 'auth_uuid':
            val = account.selectedProfile.id.trim();
            break;
          case 'auth_access_token':
            val = hideAccessToken ? hiddenToken : account.accessToken;
            break;
          case 'user_type':
            val = 'mojang';
            break;
          case 'version_type':
            val = mcJson.type;
            break;
          case 'resolution_width':
            val = 800;
            break;
          case 'resolution_height':
            val = 600;
            break;
          case 'natives_directory':
            val = args[i].replace(
              argDiscovery,
              `${addQuotes(needsQuote, path.join(instancePath, 'natives'))}`
            );
            break;
          case 'launcher_name':
            val = args[i].replace(argDiscovery, 'GDLauncher');
            break;
          case 'launcher_version':
            val = args[i].replace(argDiscovery, '1.0');
            break;
          case 'classpath':
            val = [...libraries, mcjar]
              .filter(l => !l.natives)
              .map(l => `${addQuotes(needsQuote, l.path)}`)
              .join(process.platform === 'win32' ? ';' : ':');
            break;
          default:
            break;
        }
        if (val !== null) {
          args[i] = val;
        }
      }
      if (!needsQuote) args[i] = args[i].replaceAll('"', '');
    }
  }

  if (resolution) {
    args.push(`--width=${resolution.width}`);
    args.push(`--height=${resolution.height}`);
  }

  args = args.filter(arg => {
    return arg != null;
  });

  return args;
};

export const readJarManifest = async (jarPath, property) => {
  const { extraction: list } = await extractAll(jarPath, '.', {
    toStdout: true,
    $cherryPick: 'META-INF/MANIFEST.MF'
  });

  if (list.info.has(property)) return list.info.get(property);
  return null;
};

export const patchForge113 = async (
  forgeJson,
  mainJar,
  librariesPath,
  installerPath,
  mcJsonPath,
  universalPath,
  javaPath,
  updatePercentage
) => {
  const { processors } = forgeJson;
  const replaceIfPossible = arg => {
    const finalArg = arg.replace('{', '').replace('}', '');
    if (forgeJson.data[finalArg]) {
      // Handle special case
      if (finalArg === 'BINPATCH') {
        return `"${path
          .join(librariesPath, ...mavenToArray(forgeJson.path || universalPath))
          .replace('.jar', '-clientdata.lzma')}"`;
      }
      // Return replaced string
      return forgeJson.data[finalArg].client;
    }
    // Fix forge madness
    return arg
      .replace('{SIDE}', `client`)
      .replace('{ROOT}', `"${path.dirname(installerPath)}"`)
      .replace('{MINECRAFT_JAR}', `"${mainJar}"`)
      .replace('{MINECRAFT_VERSION}', `"${mcJsonPath}"`)
      .replace('{INSTALLER}', `"${installerPath}"`)
      .replace('{LIBRARY_DIR}', `"${librariesPath}"`);
  };
  const computePathIfPossible = arg => {
    if (arg[0] === '[') {
      return `"${path.join(
        librariesPath,
        ...mavenToArray(arg.replace('[', '').replace(']', ''))
      )}"`;
    }
    return arg;
  };

  let counter = 1;
  /* eslint-disable no-await-in-loop, no-restricted-syntax */
  for (const key in processors) {
    if (Object.prototype.hasOwnProperty.call(processors, key)) {
      const p = processors[key];
      if (p?.sides && !(p?.sides || []).includes('client')) {
        continue;
      }
      const filePath = path.join(librariesPath, ...mavenToArray(p.jar));
      const args = p.args
        .map(arg => replaceIfPossible(arg))
        .map(arg => computePathIfPossible(arg));

      const classPaths = p.classpath.map(
        cp => `"${path.join(librariesPath, ...mavenToArray(cp))}"`
      );

      const mainClass = await readJarManifest(filePath, 'Main-Class');

      await new Promise(resolve => {
        const ps = spawn(
          `"${javaPath}"`,
          [
            '-classpath',
            [`"${filePath}"`, ...classPaths].join(path.delimiter),
            mainClass,
            ...args
          ],
          { shell: true }
        );

        ps.stdout.on('data', data => {
          console.log(data.toString());
        });

        ps.stderr.on('data', data => {
          console.error(`ps stderr: ${data}`);
        });

        ps.on('close', code => {
          if (code !== 0) {
            console.log(`process exited with code ${code}`);
            resolve();
          }
          resolve();
        });
      });
      updatePercentage(counter, processors.length);
      counter += 1;
    }
  }
  /* eslint-enable no-await-in-loop, no-restricted-syntax */
};

export const importAddonZip = async (
  zipPath,
  instancePath,
  instanceTempPath,
  tempPath
) => {
  const tempZipFile = path.join(instanceTempPath, 'addon.zip');
  await makeDir(instanceTempPath);
  if (zipPath.includes(tempPath)) {
    await fse.move(zipPath, tempZipFile);
  } else {
    await fse.copyFile(zipPath, tempZipFile);
  }
  const instanceManifest = path.join(instancePath, 'manifest.json');
  // Wait 500ms to avoid `The process cannot access the file because it is being used by another process.`
  await new Promise(resolve => {
    setTimeout(() => resolve(), 500);
  });

  await extractAll(tempZipFile, instancePath, {
    yes: true,
    $cherryPick: 'manifest.json'
  });
  const manifest = await fse.readJson(instanceManifest);
  return manifest;
};

export const downloadAddonZip = async (id, fileID, instancePath, tempPath) => {
  const data = await getAddonFile(id, fileID);
  const instanceManifest = path.join(instancePath, 'manifest.json');
  const zipFile = path.join(tempPath, 'addon.zip');
  if (data.downloadUrl) {
    await downloadFile(zipFile, data.downloadUrl);
  } else {
    const addon = await getAddon(id);

    const addonZipUrl = `https://www.curseforge.com/minecraft/modpacks/${addon.slug}/download/${fileID}`;
    await browserDownload(addonZipUrl, zipFile);
  }
  // Wait 500ms to avoid `The process cannot access the file because it is being used by another process.`
  await new Promise(resolve => {
    setTimeout(() => resolve(), 500);
  });

  await extractAll(zipFile, instancePath, {
    yes: true,
    $cherryPick: 'manifest.json'
  });
  const manifest = await fse.readJson(instanceManifest);
  return manifest;
};

export const getPlayerSkin = async uuid => {
  const playerSkin = await mcGetPlayerSkin(uuid);
  const { data } = playerSkin;
  const base64 = data.properties[0].value;
  const decoded = JSON.parse(Buffer.from(base64, 'base64').toString());
  return decoded?.textures?.SKIN?.url;
};

export const extractFace = async buffer => {
  const face = await jimp.read(buffer);
  const hat = await jimp.read(buffer);
  face.crop(8, 8, 8, 8);
  hat.crop(40, 8, 8, 8);
  face.scale(10, jimp.RESIZE_NEAREST_NEIGHBOR);
  hat.scale(10, jimp.RESIZE_NEAREST_NEIGHBOR);
  face.composite(hat, 0, 0);
  const ImageBuffer = await face.getBufferAsync(jimp.MIME_PNG);
  return ImageBuffer.toString('base64');
};

export const normalizeModData = (data, projectID, modName) => {
  const temp = data;
  temp.name = modName;
  if (data.fileFingerprint) {
    temp.packageFingerprint = data.fileFingerprint;
  }
  if (data.projectID && data.fileID) return temp;
  if (data.id) {
    temp.projectID = projectID;
    temp.fileID = data?.file?.id || data.id;
  }
  return temp;
};

export const reflect = p =>
  p.then(
    v => ({ v, status: true }),
    e => ({ e, status: false })
  );

export const convertCompletePathToInstance = (f, instancesPath) => {
  const escapeRegExp = stringToGoIntoTheRegex => {
    return stringToGoIntoTheRegex.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&');
  };

  return f.replace(new RegExp(escapeRegExp(instancesPath), 'gi'), '');
};

export const isMod = (fileName, instancesPath) =>
  /^(\\|\/)([\w\d\-.{}()[\]@#$%^&!\s])+((\\|\/)mods((\\|\/)(([^\\/])*))(\.jar|\.disabled))$/.test(
    convertCompletePathToInstance(fileName, instancesPath)
  );

export const isInstanceFolderPath = (f, instancesPath) =>
  /^(\\|\/)([\w\d-.{}()[\]@#$%^&!\s])+$/.test(
    convertCompletePathToInstance(f, instancesPath)
  );

export const isFileModFabric = file => {
  return (
    (file.gameVersions.includes('Fabric') ||
      file.modules.find(v => v.foldername === 'fabric.mod.json')) &&
    !file.gameVersions.includes('Forge')
  );
};

export const filterFabricFilesByVersion = (files, version) => {
  return files.filter(v => {
    if (Array.isArray(v.gameVersions)) {
      return v.gameVersions.includes(version) && isFileModFabric(v);
    }
    return v.gameVersions === version;
  });
};

export const filterForgeFilesByVersion = (files, version) => {
  return files.filter(v => {
    if (Array.isArray(v.gameVersions)) {
      return v.gameVersions.includes(version) && !isFileModFabric(v);
    }
    return v.gameVersions === version;
  });
};

export const getFirstPreferredCandidate = (files, release) => {
  let counter = release || 1;

  let latestFile = null;
  while (counter <= 3 && !latestFile) {
    const c = counter;
    const latest = files.find(v => v.releaseType <= c);
    if (latest) {
      latestFile = latest;
    }
    counter += 1;
  }
  return latestFile;
};

export const getFileHash = async (filePath, algorithm = 'sha1') => {
  // Calculate sha1 on original file
  const shasum = crypto.createHash(algorithm);

  const s = originalFs.ReadStream(filePath);
  s.on('data', data => {
    shasum.update(data);
  });

  const hash = await new Promise(resolve => {
    s.on('end', () => {
      resolve(shasum.digest('hex'));
    });
  });
  return hash;
};

export const getFilesRecursive = async dir => {
  const subdirs = await originalFs.promises.readdir(dir);
  const files = await Promise.all(
    subdirs.map(async subdir => {
      const res = path.resolve(dir, subdir);
      return (await originalFs.promises.stat(res)).isDirectory()
        ? getFilesRecursive(res)
        : res;
    })
  );
  return files.reduce((a, f) => a.concat(f), []);
};

export const extractFabricVersionFromManifest = manifest => {
  // Backwards compatability for manifest entries that use the `yarn`
  // property to set the fabric loader version. Newer manifests use the
  // format `fabric-<version>` in the id.
  let loaderVersion = manifest?.minecraft?.modLoaders[0]?.yarn;
  if (!loaderVersion) {
    loaderVersion = manifest?.minecraft?.modLoaders[0]?.id?.split('-', 2)[1];
  }
  return loaderVersion;
};

export const convertcurseForgeToCanonical = (
  curseForge,
  mcVersion,
  forgeManifest
) => {
  const patchedCurseForge = curseForge.replace('forge-', '');
  const forgeEquivalent = forgeManifest[mcVersion].find(v => {
    return v.split('-')[1] === patchedCurseForge;
  });
  return forgeEquivalent;
};

export const getPatchedInstanceType = instance => {
  const isForge = instance.loader?.loaderType === FORGE;
  const hasJumpLoader = (instance.mods || []).find(v => v.projectID === 361988);
  if (isForge && !hasJumpLoader) {
    return FORGE;
  }
  return FABRIC;
};
