# MIT License
#
# Copyright The SCons Foundation
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be included
# in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

"""
MS Compilers: detect Visual Studio and/or Visual C/C++
"""

import os

import SCons.Errors
import SCons.Util

from .common import (
    debug,
    get_output,
    is_win64,
    normalize_env,
    parse_output,
    read_reg,
)

from .vc import (
    find_vc_pdir,
    get_msvc_version_numeric,
    reset_installed_vcs,
    vswhere_freeze_env,
)


# Visual Studio express version policy when unqualified version is not installed:
#     True:  use express version for unqualified version (e.g., use 12.0Exp for 12.0)
#     False: do not use express version for unqualified version
_VSEXPRESS_USE_VERSTR = True


class VisualStudio:
    """
    An abstract base class for trying to find installed versions of
    Visual Studio.
    """
    def __init__(self, version, **kw) -> None:
        self.version = version
        self.verstr = get_msvc_version_numeric(version)
        self.vernum = float(self.verstr)
        self.is_express = True if self.verstr != self.version else False
        kw['vc_version']  = kw.get('vc_version', version)
        kw['sdk_version'] = kw.get('sdk_version', version)
        self.__dict__.update(kw)
        self._cache = {}

    def find_batch_file(self):
        vs_dir = self.get_vs_dir()
        if not vs_dir:
            debug('no vs_dir')
            return None
        batch_file = os.path.join(vs_dir, self.batch_file_path)
        batch_file = os.path.normpath(batch_file)
        if not os.path.isfile(batch_file):
            debug('%s not on file system', batch_file)
            return None
        return batch_file

    def find_vs_dir_by_vc(self, env):
        dir = find_vc_pdir(self.vc_version, env)
        if not dir:
            debug('no installed VC %s', self.vc_version)
            return None
        return os.path.abspath(os.path.join(dir, os.pardir))

    def find_vs_dir_by_reg(self, env):
        root = 'Software\\'

        if is_win64():
            root = root + 'Wow6432Node\\'
        for key in self.hkeys:
            if key=='use_dir':
                return self.find_vs_dir_by_vc(env)
            key = root + key
            try:
                comps = read_reg(key)
            except OSError:
                debug('no VS registry key %s', repr(key))
            else:
                debug('found VS in registry: %s', comps)
                return comps
        return None

    def find_vs_dir(self, env):
        """ Can use registry or location of VC to find vs dir
        First try to find by registry, and if that fails find via VC dir
        """

        vs_dir = self.find_vs_dir_by_reg(env)
        if not vs_dir:
            vs_dir = self.find_vs_dir_by_vc(env)
        debug('found VS in %s', str(vs_dir))
        return vs_dir

    def find_executable(self, env):
        vs_dir = self.get_vs_dir(env)
        if not vs_dir:
            debug('no vs_dir (%s)', vs_dir)
            return None
        executable = os.path.join(vs_dir, self.executable_path)
        executable = os.path.normpath(executable)
        if not os.path.isfile(executable):
            debug('%s not on file system', executable)
            return None
        return executable

    def get_batch_file(self):
        try:
            return self._cache['batch_file']
        except KeyError:
            batch_file = self.find_batch_file()
            self._cache['batch_file'] = batch_file
            return batch_file

    def get_executable(self, env=None):
        try:
            debug('using cache:%s', self._cache['executable'])
            return self._cache['executable']
        except KeyError:
            executable = self.find_executable(env)
            self._cache['executable'] = executable
            debug('not in cache:%s', executable)
            return executable

    def get_vs_dir(self, env=None):
        try:
            return self._cache['vs_dir']
        except KeyError:
            vs_dir = self.find_vs_dir(env)
            self._cache['vs_dir'] = vs_dir
            return vs_dir

    def get_supported_arch(self):
        try:
            return self._cache['supported_arch']
        except KeyError:
            # RDEVE: for the time being use hardcoded lists
            # supported_arch = self.find_supported_arch()
            self._cache['supported_arch'] = self.supported_arch
            return self.supported_arch

    def reset(self) -> None:
        self._cache = {}

# The list of supported Visual Studio versions we know how to detect.
#
# How to look for .bat file ?
#  - VS 2008 Express (x86):
#     * from registry key productdir, gives the full path to vsvarsall.bat. In
#     HKEY_LOCAL_MACHINE):
#         Software\Microsoft\VCEpress\9.0\Setup\VC\productdir
#     * from environmnent variable VS90COMNTOOLS: the path is then ..\..\VC
#     relatively to the path given by the variable.
#
#  - VS 2008 Express (WoW6432: 32 bits on windows x64):
#         Software\Wow6432Node\Microsoft\VCEpress\9.0\Setup\VC\productdir
#
#  - VS 2005 Express (x86):
#     * from registry key productdir, gives the full path to vsvarsall.bat. In
#     HKEY_LOCAL_MACHINE):
#         Software\Microsoft\VCEpress\8.0\Setup\VC\productdir
#     * from environmnent variable VS80COMNTOOLS: the path is then ..\..\VC
#     relatively to the path given by the variable.
#
#  - VS 2005 Express (WoW6432: 32 bits on windows x64): does not seem to have a
#  productdir ?
#
#  - VS 2003 .Net (pro edition ? x86):
#     * from registry key productdir. The path is then ..\Common7\Tools\
#     relatively to the key. The key is in HKEY_LOCAL_MACHINE):
#         Software\Microsoft\VisualStudio\7.1\Setup\VC\productdir
#     * from environmnent variable VS71COMNTOOLS: the path is the full path to
#     vsvars32.bat
#
#  - VS 98 (VS 6):
#     * from registry key productdir. The path is then Bin
#     relatively to the key. The key is in HKEY_LOCAL_MACHINE):
#         Software\Microsoft\VisualStudio\6.0\Setup\VC98\productdir
#
# The first version found in the list is the one used by default if
# there are multiple versions installed.  Barring good reasons to
# the contrary, this means we should list versions from most recent
# to oldest.  Pro versions get listed before Express versions on the
# assumption that, by default, you'd rather use the version you paid
# good money for in preference to whatever Microsoft makes available
# for free.
#
# If you update this list, update _VCVER and _VCVER_TO_PRODUCT_DIR in
# Tool/MSCommon/vc.py, and the MSVC_VERSION documentation in Tool/msvc.xml.

SupportedVSList = [
    # Visual Studio 2026
    VisualStudio('14.5',
                 vc_version='14.5',
                 sdk_version='10.0A',
                 hkeys=[],
                 common_tools_var='VS180COMNTOOLS',
                 executable_path=r'Common7\IDE\devenv.com',
                 # should be a fallback, prefer use vswhere installationPath
                 batch_file_path=r'Common7\Tools\VsDevCmd.bat',
                 supported_arch=['x86', 'amd64', "arm", 'arm64'],
                 ),

    # Visual Studio 2022
    VisualStudio('14.3',
                 vc_version='14.3',
                 sdk_version='10.0A',
                 hkeys=[],
                 common_tools_var='VS170COMNTOOLS',
                 executable_path=r'Common7\IDE\devenv.com',
                 # should be a fallback, prefer use vswhere installationPath
                 batch_file_path=r'Common7\Tools\VsDevCmd.bat',
                 supported_arch=['x86', 'amd64', "arm", 'arm64'],
                 ),

    # Visual Studio 2019
    VisualStudio('14.2',
                 vc_version='14.2',
                 sdk_version='10.0A',
                 hkeys=[],
                 common_tools_var='VS160COMNTOOLS',
                 executable_path=r'Common7\IDE\devenv.com',
                 # should be a fallback, prefer use vswhere installationPath
                 batch_file_path=r'Common7\Tools\VsDevCmd.bat',
                 supported_arch=['x86', 'amd64', "arm", 'arm64'],
                 ),

    # Visual Studio 2017
    VisualStudio('14.1',
                 vc_version='14.1',
                 sdk_version='10.0A',
                 hkeys=[],
                 common_tools_var='VS150COMNTOOLS',
                 executable_path=r'Common7\IDE\devenv.com',
                 # should be a fallback, prefer use vswhere installationPath
                 batch_file_path=r'Common7\Tools\VsDevCmd.bat',
                 supported_arch=['x86', 'amd64', "arm", 'arm64'],
                 ),

    # Visual C++ 2017 Express Edition (for Desktop)
    VisualStudio('14.1Exp',
                 vc_version='14.1',
                 sdk_version='10.0A',
                 hkeys=[],
                 common_tools_var='VS150COMNTOOLS',
                 executable_path=r'Common7\IDE\WDExpress.exe',
                 # should be a fallback, prefer use vswhere installationPath
                 batch_file_path=r'Common7\Tools\VsDevCmd.bat',
                 supported_arch=['x86', 'amd64', "arm", 'arm64'],
    ),

    # Visual Studio 2015
    VisualStudio('14.0',
                 vc_version='14.0',
                 sdk_version='10.0',
                 hkeys=[r'Microsoft\VisualStudio\14.0\Setup\VS\ProductDir'],
                 common_tools_var='VS140COMNTOOLS',
                 executable_path=r'Common7\IDE\devenv.com',
                 batch_file_path=r'Common7\Tools\vsvars32.bat',
                 supported_arch=['x86', 'amd64', "arm"],
    ),

    # Visual C++ 2015 Express Edition (for Desktop)
    VisualStudio('14.0Exp',
                 vc_version='14.0',
                 sdk_version='10.0A',
                 hkeys=[r'Microsoft\VisualStudio\14.0\Setup\VS\ProductDir'],
                 common_tools_var='VS140COMNTOOLS',
                 executable_path=r'Common7\IDE\WDExpress.exe',
                 batch_file_path=r'Common7\Tools\vsvars32.bat',
                 supported_arch=['x86', 'amd64', "arm"],
    ),

    # Visual Studio 2013
    VisualStudio('12.0',
                 vc_version='12.0',
                 sdk_version='8.1A',
                 hkeys=[r'Microsoft\VisualStudio\12.0\Setup\VS\ProductDir'],
                 common_tools_var='VS120COMNTOOLS',
                 executable_path=r'Common7\IDE\devenv.com',
                 batch_file_path=r'Common7\Tools\vsvars32.bat',
                 supported_arch=['x86', 'amd64'],
    ),

    # Visual C++ 2013 Express Edition (for Desktop)
    VisualStudio('12.0Exp',
                 vc_version='12.0',
                 sdk_version='8.1A',
                 hkeys=[r'Microsoft\VisualStudio\12.0\Setup\VS\ProductDir'],
                 common_tools_var='VS120COMNTOOLS',
                 executable_path=r'Common7\IDE\WDExpress.exe',
                 batch_file_path=r'Common7\Tools\vsvars32.bat',
                 supported_arch=['x86', 'amd64'],
    ),

    # Visual Studio 2012
    VisualStudio('11.0',
                 sdk_version='8.0A',
                 hkeys=[r'Microsoft\VisualStudio\11.0\Setup\VS\ProductDir'],
                 common_tools_var='VS110COMNTOOLS',
                 executable_path=r'Common7\IDE\devenv.com',
                 batch_file_path=r'Common7\Tools\vsvars32.bat',
                 supported_arch=['x86', 'amd64'],
    ),

    # Visual C++ 2012 Express Edition (for Desktop)
    VisualStudio('11.0Exp',
                 vc_version='11.0',
                 sdk_version='8.0A',
                 hkeys=[r'Microsoft\VisualStudio\11.0\Setup\VS\ProductDir'],
                 common_tools_var='VS110COMNTOOLS',
                 executable_path=r'Common7\IDE\WDExpress.exe',
                 batch_file_path=r'Common7\Tools\vsvars32.bat',
                 supported_arch=['x86', 'amd64'],
    ),

    # Visual Studio 2010
    VisualStudio('10.0',
                 sdk_version='7.0A',
                 hkeys=[r'Microsoft\VisualStudio\10.0\Setup\VS\ProductDir'],
                 common_tools_var='VS100COMNTOOLS',
                 executable_path=r'Common7\IDE\devenv.com',
                 batch_file_path=r'Common7\Tools\vsvars32.bat',
                 supported_arch=['x86', 'amd64'],
    ),

    # Visual C++ 2010 Express Edition
    VisualStudio('10.0Exp',
                 vc_version='10.0',
                 sdk_version='7.0A',
                 hkeys=[r'Microsoft\VCExpress\10.0\Setup\VS\ProductDir'],
                 common_tools_var='VS100COMNTOOLS',
                 executable_path=r'Common7\IDE\VCExpress.exe',
                 batch_file_path=r'Common7\Tools\vsvars32.bat',
                 supported_arch=['x86'],
    ),

    # Visual Studio 2008
    VisualStudio('9.0',
                 sdk_version='6.0A',
                 hkeys=[r'Microsoft\VisualStudio\9.0\Setup\VS\ProductDir'],
                 common_tools_var='VS90COMNTOOLS',
                 executable_path=r'Common7\IDE\devenv.com',
                 batch_file_path=r'Common7\Tools\vsvars32.bat',
                 supported_arch=['x86', 'amd64'],
    ),

    # Visual C++ 2008 Express Edition
    VisualStudio('9.0Exp',
                 vc_version='9.0',
                 sdk_version='6.0A',
                 hkeys=[r'Microsoft\VCExpress\9.0\Setup\VS\ProductDir'],
                 common_tools_var='VS90COMNTOOLS',
                 executable_path=r'Common7\IDE\VCExpress.exe',
                 batch_file_path=r'Common7\Tools\vsvars32.bat',
                 supported_arch=['x86'],
    ),

    # Visual Studio 2005
    VisualStudio('8.0',
                 sdk_version='6.0A',
                 hkeys=[r'Microsoft\VisualStudio\8.0\Setup\VS\ProductDir'],
                 common_tools_var='VS80COMNTOOLS',
                 executable_path=r'Common7\IDE\devenv.com',
                 batch_file_path=r'Common7\Tools\vsvars32.bat',
                 default_dirname='Microsoft Visual Studio 8',
                 supported_arch=['x86', 'amd64'],
    ),

    # Visual C++ 2005 Express Edition
    VisualStudio('8.0Exp',
                 vc_version='8.0Exp',
                 sdk_version='6.0A',
                 hkeys=[r'Microsoft\VCExpress\8.0\Setup\VS\ProductDir'],
                 common_tools_var='VS80COMNTOOLS',
                 executable_path=r'Common7\IDE\VCExpress.exe',
                 batch_file_path=r'Common7\Tools\vsvars32.bat',
                 default_dirname='Microsoft Visual Studio 8',
                 supported_arch=['x86'],
    ),

    # Visual Studio .NET 2003
    VisualStudio('7.1',
                 sdk_version='6.0',
                 hkeys=[r'Microsoft\VisualStudio\7.1\Setup\VS\ProductDir'],
                 common_tools_var='VS71COMNTOOLS',
                 executable_path=r'Common7\IDE\devenv.com',
                 batch_file_path=r'Common7\Tools\vsvars32.bat',
                 default_dirname='Microsoft Visual Studio .NET 2003',
                 supported_arch=['x86'],
    ),

    # Visual Studio .NET
    VisualStudio('7.0',
                 sdk_version='2003R2',
                 hkeys=[r'Microsoft\VisualStudio\7.0\Setup\VS\ProductDir'],
                 common_tools_var='VSCOMNTOOLS',
                 executable_path=r'Common7\IDE\devenv.com',
                 batch_file_path=r'Common7\Tools\vsvars32.bat',
                 default_dirname='Microsoft Visual Studio .NET',
                 supported_arch=['x86'],
    ),

    # Visual Studio 6.0
    VisualStudio('6.0',
                 sdk_version='2003R1',
                 hkeys=[r'Microsoft\VisualStudio\6.0\Setup\Microsoft Visual Studio\ProductDir',
                        'use_dir'],
                 common_tools_var='MSDevDir',
                 executable_path=r'Common\MSDev98\Bin\MSDEV.COM',
                 batch_file_path=r'Common7\Tools\vsvars32.bat',
                 default_dirname='Microsoft Visual Studio',
                 supported_arch=['x86'],
    ),
]

SupportedVSMap = {}
for vs in SupportedVSList:
    SupportedVSMap[vs.version] = vs


# Finding installed versions of Visual Studio isn't cheap, because it
# goes not only to the registry but also to the disk to sanity-check
# that there is, in fact, a Visual Studio directory there and that the
# registry entry isn't just stale.  Find this information once, when
# requested, and cache it.

InstalledVSList = None
InstalledVSMap  = None

def get_installed_visual_studios(env=None):
    global InstalledVSList
    global InstalledVSMap
    vswhere_freeze_env(env)
    if InstalledVSList is None:
        InstalledVSList = []
        InstalledVSMap = {}
        for vs in SupportedVSList:
            debug('trying to find VS %s', vs.version)
            if vs.get_executable(env):
                debug('found VS %s', vs.version)
                InstalledVSList.append(vs)
                if vs.is_express and vs.verstr not in InstalledVSMap:
                    if _VSEXPRESS_USE_VERSTR:
                        InstalledVSMap[vs.verstr] = vs
                InstalledVSMap[vs.version] = vs
    return InstalledVSList

def _get_installed_vss(env=None):
    get_installed_visual_studios(env)
    versions = list(InstalledVSMap.keys())
    return versions

def reset_installed_visual_studios() -> None:
    global InstalledVSList
    global InstalledVSMap
    debug('')
    InstalledVSList = None
    InstalledVSMap  = None
    for vs in SupportedVSList:
        vs.reset()

    # Need to clear installed VC's as well as they are used in finding
    # installed VS's
    reset_installed_vcs()


# We may be asked to update multiple construction environments with
# SDK information.  When doing this, we check on-disk for whether
# the SDK has 'mfc' and 'atl' subdirectories.  Since going to disk
# is expensive, cache results by directory.

#SDKEnvironmentUpdates = {}
#
#def set_sdk_by_directory(env, sdk_dir):
#    global SDKEnvironmentUpdates
#    try:
#        env_tuple_list = SDKEnvironmentUpdates[sdk_dir]
#    except KeyError:
#        env_tuple_list = []
#        SDKEnvironmentUpdates[sdk_dir] = env_tuple_list
#
#        include_path = os.path.join(sdk_dir, 'include')
#        mfc_path = os.path.join(include_path, 'mfc')
#        atl_path = os.path.join(include_path, 'atl')
#
#        if os.path.exists(mfc_path):
#            env_tuple_list.append(('INCLUDE', mfc_path))
#        if os.path.exists(atl_path):
#            env_tuple_list.append(('INCLUDE', atl_path))
#        env_tuple_list.append(('INCLUDE', include_path))
#
#        env_tuple_list.append(('LIB', os.path.join(sdk_dir, 'lib')))
#        env_tuple_list.append(('LIBPATH', os.path.join(sdk_dir, 'lib')))
#        env_tuple_list.append(('PATH', os.path.join(sdk_dir, 'bin')))
#
#    for variable, directory in env_tuple_list:
#        env.PrependENVPath(variable, directory)

def msvs_exists(env=None) -> bool:
    return len(get_installed_visual_studios(env)) > 0

def get_vs_by_version(msvs, env=None):
    global InstalledVSMap
    global SupportedVSMap

    debug('called')
    if msvs not in SupportedVSMap:
        msg = "Visual Studio version %s is not supported" % repr(msvs)
        raise SCons.Errors.UserError(msg)
    get_installed_visual_studios(env)
    vs = InstalledVSMap.get(msvs)
    debug('InstalledVSMap:%s', InstalledVSMap)
    debug('found vs:%s', vs)
    # Some check like this would let us provide a useful error message
    # if they try to set a Visual Studio version that's not installed.
    # However, we also want to be able to run tests (like the unit
    # tests) on systems that don't, or won't ever, have it installed.
    # It might be worth resurrecting this, with some configurable
    # setting that the tests can use to bypass the check.
    #if not vs:
    #    msg = "Visual Studio version %s is not installed" % repr(msvs)
    #    raise SCons.Errors.UserError, msg
    return vs

def get_default_version(env):
    """Returns the default version string to use for MSVS.

    If no version was requested by the user through the MSVS environment
    variable, query all the available visual studios through
    get_installed_visual_studios, and take the highest one.

    Return
    ------
    version: str
        the default version.
    """
    if 'MSVS' not in env or not SCons.Util.is_Dict(env['MSVS']):
        # get all versions, and remember them for speed later
        versions = _get_installed_vss(env)
        env['MSVS'] = {'VERSIONS' : versions}
    else:
        versions = env['MSVS'].get('VERSIONS', [])

    if 'MSVS_VERSION' not in env:
        if versions:
            env['MSVS_VERSION'] = versions[0] #use highest version by default
        else:
            debug('WARNING: no installed versions found, '
                  'using first in SupportedVSList (%s)',
                  SupportedVSList[0].version)
            env['MSVS_VERSION'] = SupportedVSList[0].version

    env['MSVS']['VERSION'] = env['MSVS_VERSION']

    return env['MSVS_VERSION']

def get_default_arch(env):
    """Return the default arch to use for MSVS

    if no version was requested by the user through the MSVS_ARCH environment
    variable, select x86

    Return
    ------
    arch: str
    """
    arch = env.get('MSVS_ARCH', 'x86')

    msvs = InstalledVSMap.get(env['MSVS_VERSION'])

    if not msvs:
        arch = 'x86'
    elif arch not in msvs.get_supported_arch():
        fmt = "Visual Studio version %s does not support architecture %s"
        raise SCons.Errors.UserError(fmt % (env['MSVS_VERSION'], arch))

    return arch

def merge_default_version(env) -> None:
    version = get_default_version(env)
    arch = get_default_arch(env)

# TODO: refers to versions and arch which aren't defined; called nowhere. Drop?
def msvs_setup_env(env) -> None:
    msvs = get_vs_by_version(version, env)
    if msvs is None:
        return
    batfilename = msvs.get_batch_file()

    # XXX: I think this is broken. This will silently set a bogus tool instead
    # of failing, but there is no other way with the current scons tool
    # framework
    if batfilename is not None:

        vars = ('LIB', 'LIBPATH', 'PATH', 'INCLUDE')

        msvs_list = get_installed_visual_studios(env)
        vscommonvarnames = [vs.common_tools_var for vs in msvs_list]
        save_ENV = env['ENV']
        nenv = normalize_env(env['ENV'],
                             ['COMSPEC'] + vscommonvarnames,
                             force=True)
        try:
            output = get_output(batfilename, arch, env=nenv)
        finally:
            env['ENV'] = save_ENV
        vars = parse_output(output, vars)

        for k, v in vars.items():
            env.PrependENVPath(k, v, delete_existing=1)

def query_versions(env=None):
    """Query the system to get available versions of VS. A version is
    considered when a batfile is found."""
    versions = _get_installed_vss(env)
    return versions

# Local Variables:
# tab-width:4
# indent-tabs-mode:nil
# End:
# vim: set expandtab tabstop=4 shiftwidth=4:
