'use strict';

const { execFileSync } = require('child_process');
const { readFileSync, statSync } = require('fs');
const { win32: path } = require('path');

const VS_VERSIONS_MODERN = new Map([
  [15, {
    year: 2017,
    msbuild: path.join('MSBuild', '15.0', 'Bin', 'MSBuild.exe'),
    toolset: 'v141',
  }],
  [16, {
    year: 2019,
    msbuild: path.join('MSBuild', 'Current', 'Bin', 'MSBuild.exe'),
    toolset: 'v142',
  }],
  [17, {
    year: 2022,
    msbuild:
      (process.arch === 'x64'
       ? path.join('MSBuild', 'Current', 'Bin', 'amd64', 'MSBuild.exe')
       : path.join('MSBuild', 'Current', 'Bin', 'MSBuild.exe')),
    toolset: 'v143',
  }],
]);
const PACKAGES = {
  msbuild: /^Microsoft[.]VisualStudio[.]VC[.]MSBuild[.](?:v\d+[.])?Base$/i,
  vctools: /^Microsoft[.]VisualStudio[.]Component[.]VC[.]Tools[.]x86[.]x64$/i,
  express: /^Microsoft[.]VisualStudio[.]WDExpress$/i,
  winsdk:
    /^Microsoft[.]VisualStudio[.]Component[.]Windows(81|10|11)SDK(?:[.](\d+)(?:[.]Desktop.*)?)?$/,
};
const SDK_REG = 'HKLM\\Software\\Microsoft\\Microsoft SDKs\\Windows';
const SDK32_REG =
  'HKLM\\Software\\Wow6432Node\\Microsoft\\Microsoft SDKs\\Windows';

function checkRequiredPackages(packages) {
  if (!Array.isArray(packages))
    return false;

  let foundMSBuild = false;
  let foundVCTools = false;
  let foundExpress = false;
  for (const pkg of packages) {
    if (!foundMSBuild && PACKAGES.msbuild.test(pkg))
      foundMSBuild = true;
    else if (!foundVCTools && PACKAGES.vctools.test(pkg))
      foundVCTools = true;
    else if (!foundExpress && PACKAGES.express.test(pkg))
      foundExpress = true;

    if (foundMSBuild && (foundVCTools || foundExpress))
      return true;
  }
}

// Sorts newest to oldest
function versionStringCompare(a, b) {
  const splitA = a.split('.');
  const splitB = b.split('.');
  const len = Math.min(splitA.length, splitB.length);
  for (let i = 0; i < len; ++i) {
    const nA = parseInt(splitA[i], 10);
    const nB = parseInt(splitB[i], 10);
    if (nA > nB)
      return -1;
    if (nA < nB)
      return 1;
  }
  if (splitA.length > splitB.length)
    return -1;
  else if (splitA.length < splitB.length)
    return 1;
  return 0;
}

function sdkVersionCompare(a, b) {
  return versionStringCompare(a.fullVersion, b.fullVersion);
}

function getSDKPaths(fullVer) {
  if (typeof fullVer !== 'string' || !fullVer)
    return;
  try {
    const arch = (process.arch === 'ia32' ? 'x86' : 'x64');
    const shortVer = `v${/^\d+[.]\d+/.exec(fullVer)[0]}`;
    let verPath = getRegValue(`${SDK_REG}\\${shortVer}`, 'InstallationFolder');
    if (!verPath)
      verPath = getRegValue(`${SDK32_REG}\\${shortVer}`, 'InstallationFolder');
    if (!verPath)
      return;

    // Includes
    const includePaths = [];
    for (const type of ['shared', 'um', 'ucrt']) {
      const testPath = path.resolve(verPath, 'Include', fullVer, type);
      statSync(testPath);
      includePaths.push(testPath);
    }

    // Libraries
    const libPaths = [];
    for (const type of ['um', 'ucrt']) {
      const testPath = path.resolve(verPath, 'Lib', fullVer, type, arch);
      statSync(testPath);
      libPaths.push(testPath);
    }

    return { includePaths, libPaths };
  } catch {}
}

const execOpts = {
  encoding: 'utf8',
  stdio: ['ignore', 'pipe', 'pipe'],
  windowsHide: true,
};

function findModernVS() {
  const versions = [];
  const ps = path.join(
    process.env.SystemRoot,
    'System32',
    'WindowsPowerShell',
    'v1.0',
    'powershell.exe'
  );
  const cs = path.resolve(__dirname, '..', 'deps', 'Find-VisualStudio.cs');
  const args = [
    '-ExecutionPolicy',
    'Unrestricted',
    '-NoProfile',
    '-Command',
    `&{Add-Type -Path '${cs}';[VisualStudioConfiguration.Main]::PrintJson()}`
  ];
  try {
    const out = execFileSync(ps, args, execOpts);
    const info = JSON.parse(out);
    if (Array.isArray(info)) {
      for (const vs of info) {
        const vsPath = path.resolve(vs.path);
        let vsVer = /^(?<major>\d+)[.](?<minor>\d+)[.]/.exec(vs.version);
        if (!vsVer)
          continue;
        vsVer = {
          full: vs.version,
          major: +vsVer.groups.major,
          minor: +vsVer.groups.minor,
        };
        const verInfo = VS_VERSIONS_MODERN.get(vsVer.major);
        if (verInfo === undefined)
          continue;
        if (!checkRequiredPackages(vs.packages))
          continue;
        const vsSDKs = [];
        for (const pkg of vs.packages) {
          let fullVersion;
          let version;
          const m = PACKAGES.winsdk.exec(pkg);
          if (!m)
            continue;
          const sdk = m[1];
          switch (sdk) {
            case '81':
              fullVersion = version = '8.1';
              break;
            case '10':
            case '11': {
              if (m[2] === undefined)
                continue;
              const sdkVer = parseInt(m[2], 10);
              if (!isFinite(sdkVer) || sdkVer < 0)
                continue;
              fullVersion = `10.0.${sdkVer}.0`;
              version = '10.0';
              break;
            }
          }
          const paths = getSDKPaths(fullVersion);
          if (!paths)
            continue;
          vsSDKs.push({
            version,
            fullVersion,
            ...paths,
          });
        }
        if (vsSDKs.length === 0)
          continue;
        let clPath;
        const includePaths = [];
        const libPaths = [];
        try {
          let vcVerFile;
          let clVer;
          try {
            vcVerFile = path.join(
              vsPath,
              'VC',
              'Auxiliary',
              'Build',
              `Microsoft.VCToolsVersion.${verInfo.toolset}.default.txt`
            );
            clVer = readFileSync(vcVerFile, { encoding: 'utf8' }).trim();
          } catch {}
          if (!clVer) {
            vcVerFile = path.join(
              vsPath,
              'VC',
              'Auxiliary',
              'Build',
              'Microsoft.VCToolsVersion.default.txt'
            );
            clVer = readFileSync(vcVerFile, { encoding: 'utf8' }).trim();
          }
          const arch = (process.arch === 'ia32' ? 'x86' : 'x64');
          let testPath = path.join(
            vsPath,
            'VC',
            'Tools',
            'MSVC',
            clVer,
            'bin',
            `Host${arch}`,
            arch,
            'cl.exe'
          );
          statSync(testPath);
          clPath = testPath;

          testPath = path.join(
            vsPath,
            'VC',
            'Tools',
            'MSVC',
            clVer,
            'include'
          );
          statSync(testPath);
          includePaths.push(testPath);

          testPath = path.join(
            vsPath,
            'VC',
            'Tools',
            'MSVC',
            clVer,
            'lib',
            arch
          );
          statSync(testPath);
          libPaths.push(testPath);
        } catch {
          continue;
        }
        vsSDKs.sort(sdkVersionCompare);
        versions.push({
          path: vsPath,
          version: vsVer,
          sdks: vsSDKs,
          ...verInfo,
          msbuild: path.join(vsPath, verInfo.msbuild),
          cl: clPath,
          includePaths,
          libPaths,
        });
      }
    }
  } catch {}
  return versions;
}

const VS_VERSIONS_OLDER = [
  {
    version: { full: '12.0', major: 12, minor: 0 },
    year: 2013,
    toolset: 'v120',
  },
  {
    version: { full: '14.0', major: 14, minor: 0 },
    year: 2015,
    toolset: 'v140',
  },
];

const VC_REG = 'HKLM\\Software\\Microsoft\\VisualStudio\\SxS\\VC7';
const VC32_REG =
  'HKLM\\Software\\Wow6432Node\\Microsoft\\VisualStudio\\SxS\\VC7';
const MSBUILD_REG = 'HKLM\\Software\\Microsoft\\MSBuild\\ToolsVersions';

function getRegValue(key, value, use32) {
  const extraArgs = (use32 ? [ '/reg:32' ] : []);
  const regexp = new RegExp(`^\\s+${value}\\s+REG_\\w+\\s+(\\S.*)$`, 'im');
  const reg = path.join(process.env.SystemRoot, 'System32', 'reg.exe');
  const args = [ 'query', key, '/v', value, ...extraArgs ];

  try {
    const out = execFileSync(reg, args, execOpts);
    const m = regexp.exec(out);
    if (m)
      return m[1];
  } catch {}
}

function findOlderVS() {
  const versions = [];
  try {
    for (const vs of VS_VERSIONS_OLDER) {
      let vsPath = getRegValue(VC_REG, vs.version.full);
      if (!vsPath)
        vsPath = getRegValue(VC32_REG, vs.version.full);
      if (!vsPath)
        continue;
      vsPath = path.resolve(vsPath, '..');

      const msbuildPath = getRegValue(
        `${MSBUILD_REG}\\${vs.version.full}`,
        'MSBuildToolsPath',
        (process.arch === 'ia32')
      );
      if (!msbuildPath)
        continue;
      versions.push({
        path: vsPath,
        ...vs,
        msbuild: path.join(msbuildPath, 'MSBuild.exe'),
        cl: path.join(vsPath, 'VC', 'bin', 'cl.exe'),
        includePaths: [path.join(vsPath, 'VC', 'include')],
        libPaths: [path.join(vsPath, 'VC', 'lib')],
        sdks: [],
      });
    }
  } catch {}
  return versions;
}

module.exports = () => {
  const versions = findModernVS().concat(findOlderVS());
  // Sorts newest to oldest
  versions.sort((a, b) => {
    return versionStringCompare(a.version.full, b.version.full);
  });
  return versions;
};
