# Copyright 2017 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

import winreg
import os
import re
import subprocess
import sys


def _RegistryGetValue(key, value):
  """Use the winreg module to obtain the value of a registry key.

  Args:
    key: The registry key.
    value: The particular registry value to read.
  Return:
    contents of the registry key's value, or None on failure.
  """
  try:
    root, subkey = key.split('\\', 1)
    assert root == 'HKLM'  # Only need HKLM for now.
    with winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, subkey) as hkey:
      return winreg.QueryValueEx(hkey, value)[0]
  except WindowsError:
    return None


def _ExtractImportantEnvironment(output_of_set):
  """Extracts environment variables required for the toolchain to run from
  a textual dump output by the cmd.exe 'set' command."""
  envvars_to_save = (
      'include',
      'lib',
      'libpath',
      'path',
      'pathext',
      'systemroot',
      'temp',
      'tmp',
      )
  env = {}
  for line in output_of_set.splitlines():
    line = line.decode("utf-8")
    for envvar in envvars_to_save:
      if re.match(envvar + '=', line.lower()):
        var, setting = line.split('=', 1)
        env[var.upper()] = setting
        break
  for required in ('SYSTEMROOT', 'TEMP', 'TMP'):
    if required not in env:
      raise Exception('Environment variable "%s" '
                      'required to be set to valid path' % required)
  return env


def _FormatAsEnvironmentBlock(envvar_dict):
  """Format as an 'environment block' directly suitable for CreateProcess.
  Briefly this is a list of key=value\0, terminated by an additional \0. See
  CreateProcess() documentation for more details."""
  block = ''
  nul = '\0'
  for key, value in envvar_dict.items():
    block += key + '=' + value + nul
  block += nul
  return block


def _GenerateEnvironmentFiles(install_dir, out_dir, script_path):
  """It's not sufficient to have the absolute path to the compiler, linker, etc.
  on Windows, as those tools rely on .dlls being in the PATH. We also need to
  support both x86 and x64 compilers. Different architectures require a
  different compiler binary, and different supporting environment variables
  (INCLUDE, LIB, LIBPATH). So, we extract the environment here, wrap all
  invocations of compiler tools (cl, link, lib, rc, midl, etc.) to set up the
  environment, and then do not prefix the compiler with an absolute path,
  instead preferring something like "cl.exe" in the rule which will then run
  whichever the environment setup has put in the path."""
  archs = ('x86', 'amd64', 'arm64')
  result = []
  for arch in archs:
    # Extract environment variables for subprocesses.
    args = [os.path.join(install_dir, script_path)]
    script_arch_name = arch
    if script_path.endswith('SetEnv.cmd') and arch == 'amd64':
      script_arch_name = '/x64'
    if arch == 'arm64':
      script_arch_name = 'x86_arm64'
    args.extend((script_arch_name, '&&', 'set'))
    popen = subprocess.Popen(
        args, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
    variables, _ = popen.communicate()
    if popen.returncode != 0:
      raise Exception('"%s" failed with error %d' % (args, popen.returncode))
    env = _ExtractImportantEnvironment(variables)

    env_block = _FormatAsEnvironmentBlock(env)
    basename = 'environment.' + arch
    with open(os.path.join(out_dir, basename), 'wb') as f:
      f.write(env_block.encode())
    result.append(basename)
  return result


def _GetEnvAsDict(arch):
  """Gets the saved environment from a file for a given architecture."""
  # The environment is saved as an "environment block" (see CreateProcess()
  # for details, which is the format required for ninja). We convert to a dict
  # here. Drop last 2 NULs, one for list terminator, one for trailing vs.
  # separator.
  pairs = open(arch).read()[:-2].split('\0')
  kvs = [item.split('=', 1) for item in pairs]
  return dict(kvs)


class WinTool(object):
  def Dispatch(self, args):
    """Dispatches a string command to a method."""
    if len(args) < 1:
      raise Exception("Not enough arguments")

    method = "Exec%s" % self._CommandifyName(args[0])
    return getattr(self, method)(*args[1:])

  def _CommandifyName(self, name_string):
    """Transforms a tool name like recursive-mirror to RecursiveMirror."""
    return name_string.title().replace('-', '')

  def ExecLinkWrapper(self, arch, *args):
    """Filter diagnostic output from link that looks like:
    '   Creating library ui.dll.lib and object ui.dll.exp'
    This happens when there are exports from the dll or exe.
    """
    env = _GetEnvAsDict(arch)
    args = list(args)  # *args is a tuple by default, which is read-only.
    args[0] = args[0].replace('/', '\\')
    link = subprocess.Popen(args, env=env, shell=True, stdout=subprocess.PIPE)
    out, _ = link.communicate()
    for line in out.splitlines():
      line = line.decode("utf-8")
      if (not line.startswith('   Creating library ') and
          not line.startswith('Generating code') and
          not line.startswith('Finished generating code')):
        print(line)
    return link.returncode

  def ExecAsmWrapper(self, arch, *args):
    """Filter logo banner from invocations of asm.exe."""
    env = _GetEnvAsDict(arch)
    popen = subprocess.Popen(args, env=env, shell=True,
                             stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
    out, _ = popen.communicate()
    for line in out.splitlines():
      line = line.decode("utf-8")
      if (not line.startswith('Copyright (C) Microsoft Corporation') and
          not line.startswith('Microsoft (R) Macro Assembler') and
          not line.startswith(' Assembling: ') and
          line):
        print(line)
    return popen.returncode

  def ExecGetVisualStudioData(self, outdir, toolchain_path):
    setenv_paths = [
      # cipd packaged SDKs from 10.0.19041.0 onwards.
      os.path.join('Windows Kits', '10', 'bin', 'SetEnv.cmd'),
      # cipd packaged SDKs prior to 10.0.19041.0.
      os.path.join('win_sdk', 'bin', 'SetEnv.cmd'),
    ]

    def explicit():
      for setenv_path in setenv_paths:
        if os.path.exists(os.path.join(toolchain_path, setenv_path)):
          return toolchain_path, setenv_path

    def env():
      from_env = os.environ.get('VSINSTALLDIR')
      for setenv_path in setenv_paths:
        if from_env and os.path.exists(os.path.join(from_env, setenv_path)):
          return from_env, setenv_path

    def autodetect():
      # Try vswhere, which will find VS2017.2+. Note that earlier VS2017s will
      # not be found.
      vswhere_path = os.path.join(os.environ.get('ProgramFiles(x86)'),
          'Microsoft Visual Studio', 'Installer', 'vswhere.exe')
      if os.path.exists(vswhere_path):
        installation_path = subprocess.check_output(
            [vswhere_path, '-latest', '-property', 'installationPath']).strip()
        if installation_path:
          return (installation_path.decode("utf-8"),
                  os.path.join('VC', 'Auxiliary', 'Build', 'vcvarsall.bat'))

      # Otherwise, try VS2015.
      version = '14.0'
      keys = [r'HKLM\Software\Microsoft\VisualStudio\%s' % version,
              r'HKLM\Software\Wow6432Node\Microsoft\VisualStudio\%s' % version]
      for key in keys:
        path = _RegistryGetValue(key, 'InstallDir')
        if not path:
          continue
        return (os.path.normpath(os.path.join(path, os.pardir, os.pardir)),
                os.path.join('VC', 'vcvarsall.bat'))

    def fail(): raise Exception('Visual Studio installation dir not found')

    # Use an explicitly specified toolchain path, if provided and found.
    # Otherwise, try using a standard environment variable. Finally, try
    # autodetecting using vswhere.
    install_dir, script_path = (explicit() or env() or autodetect() or fail())

    x86_file, x64_file, arm64_file = _GenerateEnvironmentFiles(
        install_dir, outdir, script_path)
    # gn is unhappy with trailing backslashes.
    install_dir = install_dir.rstrip('\\')
    result = '''install_dir = "%s"
x86_environment_file = "%s"
x64_environment_file = "%s"
arm64_environment_file = "%s"''' % (install_dir, x86_file, x64_file, arm64_file)
    print(result)
    return 0

  def ExecStamp(self, path):
    """Simple stamp command."""
    open(path, 'w').close()
    return 0


if __name__ == '__main__':
  sys.exit(WinTool().Dispatch(sys.argv[1:]))
