const winapi = require('winapi-bindings');
const { fs, util, log } = require('vortex-api');
const vortex_api = require('vortex-api');
const path = require('path');

const GAME = {
  id: 'vampiresurvivors',
  name: 'Vampire Survivors',
  logo: 'gameart.jpg',
  exe: 'VampireSurvivors.exe',
  steamId: '1794680'
};

const VS_MOD_LOADER = {
  name: 'VS Mod Loader',
  url: 'https://www.nexusmods.com/vampiresurvivors/mods/64',
  path: 'resources/app/.webpack/renderer/mod_loader',
  mainFile: 'index.js'
};
const MELON_LOADER = {
  name: 'MelonLoader',
  url: 'https://github.com/LavaGang/MelonLoader/releases/latest',
  path: 'MelonLoader',
  mainFile: 'MelonLoader.xml'
};

const NEW_EXTS = [
  { extension: '.dll', destination: 'Mods' },
  { extension: '.ttf', destination: 'UserData' },
  { extension: '.json', destination: 'UserData' },
  { extension: '.xml', destination: 'UserData' },
  { extension: '.cfg', destination: 'UserData' }
];
const OLD_EXTS = [
  { extension: '.js', destination: '' }
];
const OLD_HIER = [
  'resources/app/.webpack/renderer/mod_loader/mods',
  'resources/app/.webpack/renderer/assets',
  'resources/app/.webpack/renderer/main.bundle.js'
];

let CONTEXT_API;
let DISCOVERY_PATH;

/**
 * Registers the game with the provided context.
 * @param {Object} context - The modding context object.
 */
function registerGame(context) {
  CONTEXT_API = context.api;
  context.registerGame({
    id: GAME.id,
    name: GAME.name,
    mergeMods: true,
    queryPath: findGame,
    queryModPath: () => '',
    logo: GAME.logo,
    executable: () => GAME.exe,
    requiredFiles: [GAME.exe],
    setup: prepareForModding,
    environment: { SteamAPPId: GAME.steamId },
    details: { steamAppId: GAME.steamId },
  });
}

/**
 * Registers the installers for the module.
 * @param {Object} context - The modding context object.
 */
function registerInstallers(context) {
  context.registerInstaller('vampiresurvivors-oldengine-mod', 25, testSupportedContentOldEngine, installContentOldEngine);
  context.registerInstaller('vampiresurvivors-newengine-mod', 25, testSupportedContentNewEngine, installContentNewEngine);
}

/**
 * Sets up event listeners for the module.
 * @param {Object} context - The modding context object.
 */
function setupEventListeners(context) {
  try {
    context.api.events.on('did-install-mod', async (gameId, archiveId, modId) => await onDidInstallMod(gameId, archiveId, modId, context));
  } catch { }
}

/**
 * The main function of the module.
 * @param {Object} context - The modding context object.
 * @returns {boolean} - Returns true if the function executed successfully.
 */
function main(context) {
  registerGame(context);
  registerInstallers(context);
  setupEventListeners(context);
  return true;
}

/**
 * Finds the game path for the specified Steam app ID.
 * @returns {Promise<string>} The game path.
 * @throws {Error} If the game installation path is not found.
 */
async function findGame() {
  try {
    const game = await util.steam.findByAppId([GAME.steamId]);
    return game.gamePath;
  } catch {
    const registryKey = 'SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\Steam App ' + GAME.steamId;
    const instPath = winapi.RegGetValue('HKEY_LOCAL_MACHINE', registryKey, 'InstallLocation');
    if (!instPath) {
      log('error', `[find-game] game path not found`);
      throw new Error('Game installation path not found.');
    }
    return instPath.value;
  }
}

/**
 * Prepares for modding by setting the global discovery path and checking the engine version.
 * @param {Object} discovery - The game discovery object.
 * @returns {Promise<boolean>} A promise that resolves true if the modding setup is ensured successfully.
 */
async function prepareForModding(discovery) {
  DISCOVERY_PATH = discovery;
  const isNewEngine = await checkEngineVersion(discovery);
  return await ensureModdingSetup(discovery, isNewEngine);
}

/**
 * Checks the engine version by verifying the existence of UnityCrashHandler64.exe file.
 * @param {Object} discovery - The game discovery object containing the path to check.
 * @returns {Promise<boolean>} - A promise that resolves true if the engine version is valid, false otherwise.
 */
async function checkEngineVersion(discovery) {
  const enginePath = path.join(discovery.path, 'UnityCrashHandler64.exe');
  try {
    await fs.statAsync(enginePath);
    return true;
  } catch {
    return false;
  }
}

/**
 * Ensures the modding setup by checking for the existence of the correct mod loader.
 * It checks for either MelonLoader or ModLoader based on the engine version.
 * @param {Object} discovery - The game discovery object containing the path to check.
 * @param {boolean} isNewEngine - Indicates whether the engine version is new or not.
 * @returns {Promise} - A promise that resolves true if the mod loader check was successful.
 * @throws {Error} - If there is an error during the modding setup.
 */
async function ensureModdingSetup(discovery, isNewEngine) {
  try {
    return isNewEngine ? checkForMelonLoader(discovery) : checkForVSModLoader(discovery);
  } catch (error) {
    log('error', '[mod-setup] failed to ensure modding setup:', error);
    throw error;
  }
}

/**
 * Checks for the existence of VS Mod Loader.
 * @param {Object} discovery - The game discovery object containing the path to check.
 * @returns {boolean} - Returns true if VS Mod Loader exists, false otherwise.
 */
async function checkForVSModLoader(discovery) {
  try {
    await fs.ensureDirWritableAsync(path.join(discovery.path, VS_MOD_LOADER.path));
    await fs.statAsync(path.join(discovery.path, VS_MOD_LOADER.path, VS_MOD_LOADER.mainFile));
    return true;
  } catch {
    const vsModLoaderNameId = VS_MOD_LOADER.name.replaceAll(' ', '').toLowerCase();
    log('info', `[check-loader] ${vsModLoaderNameId}-missing`);
    CONTEXT_API.sendNotification({
      id: `${vsModLoaderNameId}-missing`,
      type: 'warning',
      title: `${VS_MOD_LOADER.name} not found`,
      message: `${VS_MOD_LOADER.name} is recommended for modding. When not installed only one Mod at a time can be loaded.`,
      actions: [{ title: `Get ${VS_MOD_LOADER.name}`, action: () => util.opn(VS_MOD_LOADER.url).catch(() => undefined) }],
    });
    return false;
  }
}

/**
 * Checks for the existence of MelonLoader.
 * @param {Object} discovery - The game discovery object containing the path to check.
 * @returns {boolean} - Returns true if MelonLoader exists, false otherwise.
 */
async function checkForMelonLoader(discovery) {
  try {
    await fs.ensureDirWritableAsync(path.join(discovery.path, MELON_LOADER.path));
    await fs.statAsync(path.join(discovery.path, MELON_LOADER.path, MELON_LOADER.mainFile));
    return true;
  } catch {
    const melonLoaderNameId = MELON_LOADER.name.replaceAll(' ', '').toLowerCase();
    log('info', `[check-loader] ${melonLoaderNameId}-missing`);
    CONTEXT_API.sendNotification({
      id: `${melonLoaderNameId}-missing`,
      type: 'warning',
      title: `${MELON_LOADER.name} not found`,
      message: `${MELON_LOADER.name} is necessary for modding. Please install it.`,
      actions: [{ title: `Get ${MELON_LOADER.name}`, action: () => util.opn(MELON_LOADER.url).catch(() => undefined) }],
    });
    return false;
  }
}

/**
 * Checks if the provided files are supported by the old game engine.
 * @param {string[]} files - An array of file paths.
 * @param {string} gameId - The ID of the game.
 * @param {string} modPath - The path of where the mod is being installed.
 * @returns {Promise<{ supported: boolean, requiredFiles: string[] }>} - An object containing the supported state and required files.
 */
async function testSupportedContentOldEngine(files, gameId, modPath) {
  if (gameId !== GAME.id)
    return { supported: false, requiredFiles: [] };
  const isNewEngine = await checkEngineVersion(DISCOVERY_PATH);
  let supported = files.some(file => OLD_EXTS.some(ext => path.extname(file).toLowerCase() === ext.extension));
  if (supported === false && files.some(file => file.replaceAll('\\', '/').includes('renderer/') || file.replaceAll('\\', '/').includes('assets/')))
    supported = true;
  if (supported && isNewEngine) {
    CONTEXT_API.sendNotification({
      id: `is_new_engine_${(path.parse(path.basename(modPath)).name).toLowerCase()}`,
      type: 'warning',
      title: `Old Mod but New Engine [${path.parse(path.basename(modPath)).name}]`,
      message: 'You are trying to install a Mod for the Old Engine on the New Engine',
    });
  }
  log('info', `[old-e] supported state: ${supported}`);
  return { supported, requiredFiles: [] };
}

/**
 * Checks if the provided files are supported by the new game engine.
 * @param {string[]} files - An array of file paths.
 * @param {string} gameId - The ID of the game.
 * @param {string} modPath - The path of where the mod is being installed.
 * @returns {Promise<{ supported: boolean, requiredFiles: string[] }>} - An object containing the supported state and required files.
 */
async function testSupportedContentNewEngine(files, gameId, modPath) {
  if (gameId !== GAME.id)
    return { supported: false, requiredFiles: [] };
  const isNewEngine = await checkEngineVersion(DISCOVERY_PATH);
  const supported = files.some(file => NEW_EXTS.some(ext => path.extname(file).toLowerCase() === ext.extension));
  if (supported && !isNewEngine) {
    CONTEXT_API.sendNotification({
      id: `is_old_engine${(path.parse(path.basename(modPath)).name).toLowerCase()}`,
      type: 'warning',
      title: `New Mod but Old Engine [${path.parse(path.basename(modPath)).name}]`,
      message: 'You are trying to install a Mod for the new Engine on the Old Engine',
    });
  }
  log('info', `[new-e] supported state: ${supported}`);
  return { supported, requiredFiles: [] };
}

/**
 * Installs the mod files for the old engine.
 * @param {string[]} files - The files to be installed.
 * @returns {Promise<Object[]>} - An array of objects containing the instructions for copying the files.
 */
async function installContentOldEngine(files) {
  files = prepareFilesOldEngine(files);
  return installContent(files);
}

/**
 * Prepares files for the old engine.
 * @param {string[]} files - The array of files to be prepared.
 * @returns {Object[]} - The array of prepared files, each containing a source and destination path.
 */
function prepareFilesOldEngine(files) {
  const preparedFiles = [];
  log('info', `[old-e] prepare files:"${files}"`);

  let modPathPre = '';
  for (let file of files) {
    file = file.replaceAll('\\', '/');
    const fileComponents = file.split('/');

    for (const hierPath of OLD_HIER) {
      const hierComponents = hierPath.split('/');
      const hierIndex = hierComponents.indexOf(fileComponents[0]);

      if (hierIndex !== -1) {
        modPathPre = hierComponents.slice(0, hierIndex).join('/');
        break;
      }
    }

    if (modPathPre && modPathPre.length !== 0) {
      modPathPre += '/';
      break;
    }
  }

  for (let file of files) {
    file = file.replaceAll('\\', '/');
    if (file.endsWith('/')) continue;
    // const extension = path.extname(file).toLowerCase();
    // const matchingConfig = OLD_EXTS.find(config => config.extension === extension);
    // if (matchingConfig) {
    preparedFiles.push({ source: file, destination: `${modPathPre}${file}` });
    // }
  }

  let logString = '';
  for (const file of preparedFiles) {
    logString += `(source:${file.source}|destination:${file.destination})`;
  }
  log('info', `[old-e] prepared files:"${logString}"`);

  return preparedFiles;
}

/**
 * Installs the mod files for the new engine.
 * @param {string[]} files - The files to be installed.
 * @returns {Promise<Object[]>} - An array of objects containing the instructions for copying the files.
 */
async function installContentNewEngine(files) {
  files = prepareFilesNewEngine(files);
  return installContent(files);
}

/**
 * Prepares files for the new engine.
 * @param {string[]} files - An array of files to be prepared.
 * @returns {Object[]} - An array of prepared files, each containing a source and destination path.
 */
function prepareFilesNewEngine(files) {
  const preparedFiles = [];
  log('info', `[new-e] prepare files:"${files}"`);

  let hasFolder = false;
  for (let file of files) {
    file = file.replace(/\\/g, "/");
    const dirName = path.dirname(file);
    if (dirName !== '.' && dirName !== '') {
      hasFolder = true;
      break;
    }
  }

  for (let file of files) {
    file = file.replace(/\\/g, '/');
    if (file.endsWith('/'))
      continue;
    const dirName = path.dirname(file);
    const extension = path.extname(file).toLowerCase();
    const matchingConfig = NEW_EXTS.find(config => config.extension === extension);
    if (matchingConfig) {
      if (hasFolder === false && (dirName === '.' || dirName === '')) {
        const newFilePath = path.join(matchingConfig.destination, path.basename(file));
        preparedFiles.push({ source: file, destination: newFilePath });
        continue;
      } else {
        preparedFiles.push({ source: file, destination: file });
      }
    }
  }

  let logString = '';
  for (const file of preparedFiles) {
    logString += `{source:${file.source},destination:${file.destination}}`;
  }
  log('info', `[new-e] prepared files:"${logString}"`);

  return preparedFiles;
}

/**
 * Installs mod files by copying files from source to destination.
 * @param {Object[]} files - An array of file objects containing source and destination paths.
 * @returns {Object[]} - An object containing the instructions for copying the files.
 */
async function installContent(files) {
  log('info', `[install] files:"${files.map(file => file.source)}"`);
  const instructions = [];

  for (const file of files) {
    instructions.push({
      type: "copy",
      source: file.source,
      destination: file.destination
    });
  }

  for (const file of instructions) {
    log('info', `[instructions] source:"${file.source}" | destination:"${file.destination}"`);
  }
  return { instructions };
}

/**
 * Handles the event when a mod is installed.
 * When on old engine, it fixes the mod by editing the main mod file, if necessary.
 * @param {string} gameId - The ID of the game.
 * @param {string} archiveId - The ID of the mod archive (unused).
 * @param {string} modId - The ID of the mod.
 * @param {Object} context - The modding context object containing the API and state.
 * @returns {Promise} - A promise that resolves when the function completes.
 */
async function onDidInstallMod(gameId, archiveId, modId, context) {
  const state = context.api.getState();
  const installPath = vortex_api.selectors.installPathForGame(state, gameId);
  const mod = state.persistent.mods?.[gameId]?.[modId];
  if (!installPath || !mod?.installationPath)
    return;

  const isNewEngine = await checkEngineVersion(DISCOVERY_PATH);
  if (isNewEngine === true)
    return;

  log('info', `[old-e] fixing old mod:"${modId}" on path:"${mod.installationPath}"`);
  const mainModPath = findMainModFile(path.join(installPath, mod.installationPath));
  if (mainModPath) {
    const success = fixGetMods(mainModPath);
    if (success) {
      log('info', `[old-e] fixed old mod:"${modId}"`);
      context.api.sendNotification({
        id: `fix_success_${modId}`,
        type: 'info',
        title: 'Fixed Mod',
        message: `Successfully fixed Mod: "${modId}"`,
      });
    }
  }
}

/**
 * Fixes a mods getMods function by removing the line that makes it fail (old engine only).
 * @param {string} filePath - The path to the mods main file.
 * @returns {boolean} - Whether the fix was successful or not.
 */
function fixGetMods(filePath) {
  try {
    log('info', `[fix-get-mods] filePath:"${filePath}"`);
    let data = fs.readFileSync(filePath, "utf8");

    const getModsRegex = /getMods\s*\(\)\s*{([\s\S]*?)}/;
    const readdirSyncRegex = /"mods\/"\),\s*{\s*withFileTypes:\s*true\s*}/;

    const getModsMatch = data.match(getModsRegex);
    if (!getModsMatch) return false;

    const readdirSyncMatch = getModsMatch[0].match(readdirSyncRegex);
    if (!readdirSyncMatch) return false;

    const modifiedData = data.replace(readdirSyncRegex, `${readdirSyncMatch[0]}).filter((dir) => dir.name !== "__folder_managed_by_vortex"`);

    fs.writeFileSync(filePath, modifiedData, "utf8");
    return true;
  } catch (err) {
    log('error', `could not fix mod:"${err}"`);
    return false;
  }
}

/**
 * Finds the main mod file in the specified mod path (old engine only).
 * @param {string} modPath - The path to the mod.
 * @returns {string|undefined} - The path to the main mod file, or undefined if not found.
 */
function findMainModFile(modPath) {
  try {
    const modsFolderPath = findModsFolder(modPath);
    const files = fs.readdirSync(modsFolderPath);
    let dirname;
    for (const file of files) {
      const filePath = path.join(modsFolderPath, file);
      const stat = fs.statSync(filePath);
      if (stat.isDirectory()) {
        dirname = path.basename(filePath);
        const fileName = dirname + '.js';
        const targetFilePath = path.join(filePath, fileName);
        const exists = fs.statSync(targetFilePath, (err, stats) => {
          if (!err && stats.isFile()) {
            return targetFilePath;
          } else {
            return;
          }
        });
        if (exists) {
          return targetFilePath;
        } else {
          return;
        }
      }
    }
  } catch {
    return;
  }
}

/**
 * Recursively searches for the "mods" folder within the given folder path (old engine only).
 * @param {string} folderPath - The path of the folder to search in.
 * @returns {string|undefined} - The path of the "mods" folder, or undefined if not found.
 */
function findModsFolder(folderPath) {
  try {
    const files = fs.readdirSync(folderPath);

    for (const file of files) {
      const filePath = path.join(folderPath, file);
      const stats = fs.statSync(filePath);

      if (stats.isDirectory()) {
        if (file === 'mods') {
          return filePath;
        } else {
          const modsFolderPath = findModsFolder(filePath);
          if (modsFolderPath) {
            return modsFolderPath;
          }
        }
      }
    }
    return;
  } catch {
    return;
  }
}

module.exports = {
  default: main,
};