# Copyright (c) 2008-2011 by Enthought, Inc.
# Copyright (c) 2013-2017 Continuum Analytics, Inc.
# All rights reserved.

from __future__ import absolute_import, unicode_literals

import ctypes
import logging
import os
from os.path import isdir, join, exists, split
import sys
import locale


from .utils import rm_empty_dir, rm_rf
from .knownfolders import get_folder_path, FOLDERID
# KNOWNFOLDERID does provide a direct path to Quick Launch.  No additional path necessary.
from .winshortcut import create_shortcut


# This allows debugging installer issues using DebugView from Microsoft.
OutputDebugString = ctypes.windll.kernel32.OutputDebugStringW
OutputDebugString.argtypes = [ctypes.c_wchar_p]


class DbgViewHandler(logging.Handler):
    def emit(self, record):
        OutputDebugString(self.format(record))


logger = logging.getLogger("menuinst_win32")
logger.setLevel(logging.DEBUG)
stream_handler = logging.StreamHandler()
stream_handler.setLevel(logging.WARNING)
dbgview = DbgViewHandler()
dbgview.setLevel(logging.DEBUG)
logger.addHandler(dbgview)
logger.addHandler(stream_handler)

# When running as 'nt authority/system' as sometimes people do via SCCM,
# various folders do not exist, such as QuickLaunch. This doesn't matter
# as we'll use the "system" key finally and check for the "quicklaunch"
# subkey before adding any Quick Launch menu items.

# It can happen that some of the dirs[] entires refer to folders that do not
# exist, in which case, the 2nd entry of the value tuple is a sub-class of
# Exception.

dirs_src = {"system": {  "desktop": get_folder_path(FOLDERID.PublicDesktop),
                           "start": get_folder_path(FOLDERID.CommonPrograms),
                       "documents": get_folder_path(FOLDERID.PublicDocuments),
                         "profile": get_folder_path(FOLDERID.Profile)},

            "user": {    "desktop": get_folder_path(FOLDERID.Desktop),
                           "start": get_folder_path(FOLDERID.Programs),
                     "quicklaunch": get_folder_path(FOLDERID.QuickLaunch),
                       "documents": get_folder_path(FOLDERID.Documents),
                         "profile": get_folder_path(FOLDERID.Profile)}}


def folder_path(preferred_mode, check_other_mode, key):
    ''' This function implements all heuristics and workarounds for messed up
        KNOWNFOLDERID registry values. It's also verbose (OutputDebugStringW)
        about whether fallbacks worked or whether they would have worked if
        check_other_mode had been allowed.
    '''
    other_mode = 'system' if preferred_mode == 'user' else 'user'
    path, exception = dirs_src[preferred_mode][key]
    if not exception:
        return path
    logger.info("WARNING: menuinst key: '%s'\n"
                "                 path: '%s'\n"
                "     .. excepted with: '%s' in knownfolders.py, implementing workarounds .."
                % (key, path, type(exception).__name__))
    # Since I have seen 'user', 'documents' set as '\\vmware-host\Shared Folders\Documents'
    # when there's no such server, we check 'user', 'profile' + '\Documents' before maybe
    # trying the other_mode (though I have chickened out on that idea).
    if preferred_mode == 'user' and key == 'documents':
        user_profile, exception = dirs_src['user']['profile']
        if not exception:
            path = join(user_profile, 'Documents')
            if os.access(path, os.W_OK):
                logger.info("  .. worked-around to: '%s'" % (path))
                return path
    path, exception = dirs_src[other_mode][key]
    # Do not fall back to something we cannot write to.
    if exception:
        if check_other_mode:
            logger.info("     .. despite 'check_other_mode'\n"
                        "        and 'other_mode' 'path' of '%s'\n"
                        "        it excepted with: '%s' in knownfolders.py" % (path,
                                                                    type(exception).__name__))
        else:
            logger.info("     .. 'check_other_mode' is False,\n"
                        "        and 'other_mode' 'path' is '%s'\n"
                        "        but it excepted anyway with: '%s' in knownfolders.py" % (path, type(exception).__name__))
        return None
    if not check_other_mode:
        logger.info("     .. due to lack of 'check_other_mode' not picking\n"
                    "        non-excepting path of '%s'\n in knownfolders.py" % (path))
        return None
    return path


def quoted(s):
    """
    quotes a string if necessary.
    """
    # strip any existing quotes
    s = s.strip(u'"')
    # don't add quotes for minus or leading space
    if s[0] in (u'-', u' '):
        return s
    if u' ' in s or u'/' in s:
        return u'"%s"' % s
    else:
        return s


def ensure_pad(name, pad="_"):
    """

    Examples:
        >>> ensure_pad('conda')
        '_conda_'

    """
    if not name or name[0] == name[-1] == pad:
        return name
    else:
        return "%s%s%s" % (pad, name, pad)


def to_unicode(var, codec=locale.getpreferredencoding()):
    if sys.version_info[0] < 3 and isinstance(var, unicode):
        return var
    if not codec:
        codec = "utf-8"
    if hasattr(var, "decode"):
        var = var.decode(codec)
    return var


def to_bytes(var, codec=locale.getpreferredencoding()):
    if isinstance(var, bytes):
        return var
    if not codec:
        codec="utf-8"
    if hasattr(var, "encode"):
        var = var.encode(codec)
    return var


unicode_root_prefix = to_unicode(sys.prefix)
if u'\\envs\\' in unicode_root_prefix:
    logger.warn('menuinst called from non-root env %s', unicode_root_prefix)


def substitute_env_variables(text, dir):
    # When conda is using Menuinst, only the root conda installation ever
    # calls menuinst.  Thus, these calls to sys refer to the root conda
    # installation, NOT the child environment
    py_major_ver = sys.version_info[0]
    py_bitness = 8 * tuple.__itemsize__

    env_prefix = to_unicode(dir['prefix'])
    root_prefix = to_unicode(dir['root_prefix'])
    text = to_unicode(text)
    env_name = to_unicode(dir['env_name'])

    for a, b in (
        (u'${PREFIX}', env_prefix),
        (u'${ROOT_PREFIX}', root_prefix),
        (u'${DISTRIBUTION_NAME}', os.path.split(root_prefix)[-1].capitalize()),
        (u'${PYTHON_SCRIPTS}',
          os.path.normpath(join(env_prefix, u'Scripts')).replace(u"\\", u"/")),
        (u'${MENU_DIR}', join(env_prefix, u'Menu')),
        (u'${PERSONALDIR}', dir['documents']),
        (u'${USERPROFILE}', dir['profile']),
        (u'${ENV_NAME}', env_name),
        (u'${PY_VER}', u'%d' % (py_major_ver)),
        (u'${PLATFORM}', u"(%s-bit)" % py_bitness),
        ):
        if b:
            text = text.replace(a, b)
    return text


class Menu(object):
    def __init__(self, name, prefix=unicode_root_prefix, env_name=u"", mode=None, root_prefix=unicode_root_prefix):
        """
        Prefix is the system prefix to be used -- this is needed since
        there is the possibility of a different Python's packages being managed.
        """

        # bytestrings passed in need to become unicode
        self.prefix = to_unicode(prefix)
        self.root_prefix = to_unicode(root_prefix)
        used_mode = mode if mode else ('user' if exists(join(self.prefix, u'.nonadmin')) else 'system')
        logger.debug("Menu: name: '%s', prefix: '%s', env_name: '%s', mode: '%s', used_mode: '%s', root_prefix: '%s'"
                    % (name, self.prefix, env_name, mode, used_mode, root_prefix))
        try:
            self.set_dir(name, self.prefix, env_name, used_mode, root_prefix)
        except WindowsError:
            # We get here if we aren't elevated.  This is different from
            #   permissions: a user can have permission, but elevation is still
            #   required.  If the process isn't elevated, we get the
            #   WindowsError
            if 'user' in dirs_src and used_mode == 'system':
                logger.warn("Insufficient permissions to write menu folder.  "
                            "Falling back to user location")
                try:
                    self.set_dir(name, self.prefix, env_name, 'user')
                except:
                    pass
            else:
                logger.fatal("Unable to create AllUsers menu folder")

    def set_dir(self, name, prefix, env_name, mode, root_prefix):
        self.mode = mode
        self.dir = dict()
        # I have chickened out on allowing check_other_mode. Really there needs
        # to be 3 distinct cases that 'menuinst' cares about:
        # priv-user doing system install
        # priv-user doing user-only install
        # non-priv-user doing user-only install
        # (priv-user only exists in an AllUsers installation).
        check_other_mode = False
        for k, v in dirs_src[mode].items():
            # We may want to cache self.dir to some files, one for AllUsers
            # (system) installs and one for each subsequent user install?
            self.dir[k] = folder_path(mode, check_other_mode, k)
        self.dir['prefix'] = prefix
        self.dir['root_prefix'] = root_prefix
        self.dir['env_name'] = env_name
        folder_name = substitute_env_variables(name, self.dir)
        self.path = join(self.dir["start"], folder_name)
        self.create()

    def create(self):
        if not isdir(self.path):
            os.mkdir(self.path)

    def remove(self):
        rm_empty_dir(self.path)


def extend_script_args(args, shortcut):
    try:
        args.append(shortcut['scriptargument'])
    except KeyError:
        pass
    try:
        args.extend(shortcut['scriptarguments'])
    except KeyError:
        pass


def quote_args(args):
    # cmd.exe /K or /C expects a single string argument and requires
    # doubled-up quotes when any sub-arguments have spaces:
    # https://stackoverflow.com/a/6378038/3257826
    if (len(args) > 2 and ("CMD.EXE" in args[0].upper() or "%COMSPEC%" in args[0].upper())
            and (args[1].upper() == '/K' or args[1].upper() == '/C')
            and any(' ' in arg for arg in args[2:])
    ):
        args = [
            ensure_pad(args[0], '"'),  # cmd.exe
            args[1],  # /K or /C
            '"%s"' % (' '.join(ensure_pad(arg, '"') for arg in args[2:])),  # double-quoted
        ]
    else:
        args = [quoted(arg) for arg in args]
    return args


class ShortCut(object):
    def __init__(self, menu, shortcut):
        self.menu = menu
        self.shortcut = shortcut

    def remove(self):
        self.create(remove=True)

    def create(self, remove=False):
        # Substitute env variables early because we may need to escape spaces in the value.
        args = []
        fix_win_slashes = [0]
        prefix = self.menu.prefix.replace('/', '\\')
        unicode_root_prefix = self.menu.root_prefix.replace('/', '\\')
        root_py  = join(unicode_root_prefix, u"python.exe")
        root_pyw = join(unicode_root_prefix, u"pythonw.exe")
        env_py  = join(prefix, u"python.exe")
        env_pyw = join(prefix, u"pythonw.exe")
        cwp_py  = [root_py,  join(unicode_root_prefix, u'cwp.py'), prefix, env_py]
        cwp_pyw = [root_pyw, join(unicode_root_prefix, u'cwp.py'), prefix, env_pyw]
        if "pywscript" in self.shortcut:
            args = cwp_pyw
            fix_win_slashes = [len(args)]
            args += self.shortcut["pywscript"].split()
        elif "pyscript" in self.shortcut:
            args = cwp_py
            fix_win_slashes = [len(args)]
            args += self.shortcut["pyscript"].split()
        elif "webbrowser" in self.shortcut:
            args = [root_pyw, '-m', 'webbrowser', '-t', self.shortcut['webbrowser']]
        elif "script" in self.shortcut:
            # It is unclear whether running through cwp.py is what we want here. In
            # the long term I would rather this was made an explicit choice.
            args = [root_py,  join(unicode_root_prefix, u'cwp.py'), prefix]
            fix_win_slashes = [len(args)]
            args += self.shortcut["script"].split()
            extend_script_args(args, self.shortcut)
        elif "system" in self.shortcut:
            args = self.shortcut["system"].split()
            extend_script_args(args, self.shortcut)
        else:
            raise Exception("Nothing to do: %r" % self.shortcut)
        args = [substitute_env_variables(arg, self.menu.dir) for arg in args]
        for fws in fix_win_slashes:
            args[fws] = args[fws].replace('/', '\\')

        args = quote_args(args)

        cmd = args[0]
        args = args[1:]
        logger.debug('Shortcut cmd is %s, args are %s' % (cmd, args))
        workdir = self.shortcut.get('workdir', '')
        icon = self.shortcut.get('icon', '')

        workdir = substitute_env_variables(workdir, self.menu.dir)
        icon = substitute_env_variables(icon, self.menu.dir)

        # Fix up the '/' to '\'
        workdir = workdir.replace('/', '\\')
        icon = icon.replace('/', '\\')

        # Create the working directory if it doesn't exist
        if workdir:
            if not isdir(workdir):
                os.makedirs(workdir)
        else:
            workdir = '%HOMEPATH%'

        # Menu link
        dst_dirs = [self.menu.path]

        # Desktop link
        if self.shortcut.get('desktop'):
            dst_dirs.append(self.menu.dir['desktop'])

        # Quicklaunch link
        if self.shortcut.get('quicklaunch') and 'quicklaunch' in self.menu.dir:
            dst_dirs.append(self.menu.dir['quicklaunch'])

        name_suffix = " ({})".format(self.menu.dir['env_name']) if self.menu.dir['env_name'] else ""
        for dst_dir in dst_dirs:
            name = substitute_env_variables(self.shortcut['name'], self.menu.dir)
            dst = join(dst_dir, name + name_suffix + '.lnk')
            if remove:
                rm_rf(dst)
            else:
                # The API for the call to 'create_shortcut' has 3
                # required arguments (path, description and filename)
                # and 4 optional ones (args, working_dir, icon_path and
                # icon_index).
                create_shortcut(
                    u'' + cmd,
                    u'' + name + name_suffix,
                    u'' + dst,
                    u' '.join(arg for arg in args),
                    u'' + workdir,
                    u'' + icon,
                )
