# Copyright (c) 2012 Google Inc. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

"""
This module helps emulate Visual Studio 2008 behavior on top of other
build systems, primarily ninja.
"""

import os
import re
import subprocess
import sys

from gyp.common import OrderedSet
import gyp.MSVSVersion

windows_quoter_regex = re.compile(r'(\\*)"')

def QuoteForRspFile(arg):
  """Quote a command line argument so that it appears as one argument when
  processed via cmd.exe and parsed by CommandLineToArgvW (as is typical for
  Windows programs)."""
  # See http://goo.gl/cuFbX and http://goo.gl/dhPnp including the comment
  # threads. This is actually the quoting rules for CommandLineToArgvW, not
  # for the shell, because the shell doesn't do anything in Windows. This
  # works more or less because most programs (including the compiler, etc.)
  # use that function to handle command line arguments.

  # For a literal quote, CommandLineToArgvW requires 2n+1 backslashes
  # preceding it, and results in n backslashes + the quote. So we substitute
  # in 2* what we match, +1 more, plus the quote.
  arg = windows_quoter_regex.sub(lambda mo: 2 * mo.group(1) + '\\"', arg)

  # %'s also need to be doubled otherwise they're interpreted as batch
  # positional arguments. Also make sure to escape the % so that they're
  # passed literally through escaping so they can be singled to just the
  # original %. Otherwise, trying to pass the literal representation that
  # looks like an environment variable to the shell (e.g. %PATH%) would fail.
  arg = arg.replace('%', '%%')

  # These commands are used in rsp files, so no escaping for the shell (via ^)
  # is necessary.

  # Finally, wrap the whole thing in quotes so that the above quote rule
  # applies and whitespace isn't a word break.
  return '"' + arg + '"'


def EncodeRspFileList(args):
  """Process a list of arguments using QuoteCmdExeArgument."""
  # Note that the first argument is assumed to be the command. Don't add
  # quotes around it because then built-ins like 'echo', etc. won't work.
  # Take care to normpath only the path in the case of 'call ../x.bat' because
  # otherwise the whole thing is incorrectly interpreted as a path and not
  # normalized correctly.
  if not args: return ''
  if args[0].startswith('call '):
    call, program = args[0].split(' ', 1)
    program = call + ' ' + os.path.normpath(program)
  else:
    program = os.path.normpath(args[0])
  return program + ' ' + ' '.join(QuoteForRspFile(arg) for arg in args[1:])


def _GenericRetrieve(root, default, path):
  """Given a list of dictionary keys |path| and a tree of dicts |root|, find
  value at path, or return |default| if any of the path doesn't exist."""
  if not root:
    return default
  if not path:
    return root
  return _GenericRetrieve(root.get(path[0]), default, path[1:])


def _AddPrefix(element, prefix):
  """Add |prefix| to |element| or each subelement if element is iterable."""
  if element is None:
    return element
  # Note, not Iterable because we don't want to handle strings like that.
  if isinstance(element, list) or isinstance(element, tuple):
    return [prefix + e for e in element]
  else:
    return prefix + element


def _DoRemapping(element, map):
  """If |element| then remap it through |map|. If |element| is iterable then
  each item will be remapped. Any elements not found will be removed."""
  if map is not None and element is not None:
    if not callable(map):
      map = map.get # Assume it's a dict, otherwise a callable to do the remap.
    if isinstance(element, list) or isinstance(element, tuple):
      element = filter(None, [map(elem) for elem in element])
    else:
      element = map(element)
  return element


def _AppendOrReturn(append, element):
  """If |append| is None, simply return |element|. If |append| is not None,
  then add |element| to it, adding each item in |element| if it's a list or
  tuple."""
  if append is not None and element is not None:
    if isinstance(element, list) or isinstance(element, tuple):
      append.extend(element)
    else:
      append.append(element)
  else:
    return element


def _FindDirectXInstallation():
  """Try to find an installation location for the DirectX SDK. Check for the
  standard environment variable, and if that doesn't exist, try to find
  via the registry. May return None if not found in either location."""
  # Return previously calculated value, if there is one
  if hasattr(_FindDirectXInstallation, 'dxsdk_dir'):
    return _FindDirectXInstallation.dxsdk_dir

  dxsdk_dir = os.environ.get('DXSDK_DIR')
  if not dxsdk_dir:
    # Setup params to pass to and attempt to launch reg.exe.
    cmd = ['reg.exe', 'query', r'HKLM\Software\Microsoft\DirectX', '/s']
    p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    for line in p.communicate()[0].splitlines():
      if 'InstallPath' in line:
        dxsdk_dir = line.split('    ')[3] + "\\"

  # Cache return value
  _FindDirectXInstallation.dxsdk_dir = dxsdk_dir
  return dxsdk_dir


def GetGlobalVSMacroEnv(vs_version):
  """Get a dict of variables mapping internal VS macro names to their gyp
  equivalents. Returns all variables that are independent of the target."""
  env = {}
  # '$(VSInstallDir)' and '$(VCInstallDir)' are available when and only when
  # Visual Studio is actually installed.
  if vs_version.Path():
    env['$(VSInstallDir)'] = vs_version.Path()
    env['$(VCInstallDir)'] = os.path.join(vs_version.Path(), 'VC') + '\\'
  # Chromium uses DXSDK_DIR in include/lib paths, but it may or may not be
  # set. This happens when the SDK is sync'd via src-internal, rather than
  # by typical end-user installation of the SDK. If it's not set, we don't
  # want to leave the unexpanded variable in the path, so simply strip it.
  dxsdk_dir = _FindDirectXInstallation()
  env['$(DXSDK_DIR)'] = dxsdk_dir if dxsdk_dir else ''
  # Try to find an installation location for the Windows DDK by checking
  # the WDK_DIR environment variable, may be None.
  env['$(WDK_DIR)'] = os.environ.get('WDK_DIR', '')
  return env

def ExtractSharedMSVSSystemIncludes(configs, generator_flags):
  """Finds msvs_system_include_dirs that are common to all targets, removes
  them from all targets, and returns an OrderedSet containing them."""
  all_system_includes = OrderedSet(
      configs[0].get('msvs_system_include_dirs', []))
  for config in configs[1:]:
    system_includes = config.get('msvs_system_include_dirs', [])
    all_system_includes = all_system_includes & OrderedSet(system_includes)
  if not all_system_includes:
    return None
  # Expand macros in all_system_includes.
  env = GetGlobalVSMacroEnv(GetVSVersion(generator_flags))
  expanded_system_includes = OrderedSet([ExpandMacros(include, env)
                                         for include in all_system_includes])
  if any(['$' in include for include in expanded_system_includes]):
    # Some path relies on target-specific variables, bail.
    return None

  # Remove system includes shared by all targets from the targets.
  for config in configs:
    includes = config.get('msvs_system_include_dirs', [])
    if includes:  # Don't insert a msvs_system_include_dirs key if not needed.
      # This must check the unexpanded includes list:
      new_includes = [i for i in includes if i not in all_system_includes]
      config['msvs_system_include_dirs'] = new_includes
  return expanded_system_includes


class MsvsSettings(object):
  """A class that understands the gyp 'msvs_...' values (especially the
  msvs_settings field). They largely correpond to the VS2008 IDE DOM. This
  class helps map those settings to command line options."""

  def __init__(self, spec, generator_flags):
    self.spec = spec
    self.vs_version = GetVSVersion(generator_flags)

    supported_fields = [
        ('msvs_configurat
ion_attributes', dict),
        ('msvs_settings', dict),
        ('msvs_system_include_dirs', list),
        ('msvs_disabled_wa
rnings', list),
        ('msvs_precompiled_header', str),
        ('msvs_precompiled_source', str),
        ('msvs_con