# Copyright (c) 2009 ActiveState Software Inc.
# See http://www.activestate.com/activepython/license/ for licensing
# information.

import sys
import string
import os
from os.path import join, abspath, expanduser, dirname, exists
import logging
import textwrap
import operator
import collections
import webbrowser
import site
import shutil
from datetime import datetime
from pkg_resources import parse_requirements

import pypm
from pypm.common import activestate, python, console, log
import pypm.common.cmdln_branches_cfg as cmdln
from pypm.common.supported import PLATNAME
from pypm.common.util import require_option, xjoin, askyesno
from pypm.common.textui import colprint
from pypm.common.repository import RemoteRepositorySet
from pypm.client.base import (make_pypm_environment2, PYPM_CLIENT_CONF_FILE,
                              IDX_PATH, application)
from pypm.client.fs import DOWNLOAD_CACHE
from pypm.client import installer, error
from pypm.client.version import compare_version, sort_packages_by_version

LOG = logging.getLogger('pypm.client')


#
# facade to install or uninstall a package
#

def install(pypmenv, reqname):
    """Install the given requirement"""
    if reqname == 'setuptools': reqname = 'distribute'
    req, = parse_requirements(reqname)
    
    def get_installed_requirement(req):
        """Return the installed package that matches the requirement"""
        ipkg = pypmenv.installed_store.find_only_package(
            req.project_name)
        # TODO: support for req.specs in the above find
        if ipkg.version not in req:
            raise error.VersionConflict, \
              'Conflicting version is installed: %s' % ipkg.full_name
        return ipkg
    
    try:
        ipkg = get_installed_requirement(req)
    except error.NoPackageInstalled:
        pkg = pypmenv.repo_store.find_requirement(req)
    else:
        pkg = pypmenv.repo_store.find_requirement(req)
        if pkg.full_version == ipkg.full_version:
            # FIXME: This is not accurate; we should check the
            # dependencies as well. For instance, the second time the
            # user may type with extras .. as in: "pypm install
            # zope.component[docs]" .. and this time we should install
            # the 'docs' dependencies. But currently, we don't.
            LOG.info('%s is already installed', ipkg.full_name)
            return
    
    return installer.Installer(pypmenv).install(pkg, req.extras)
    
    
def uninstall(pypmenv, package_name):
    """Uninstall the given package"""
    if package_name == 'setuptools': package_name = 'distribute'
    req, = parse_requirements(package_name)
    if req.extras or req.specs:
        raise ValueError, (
            'The "uninstall" command does not support requirement '
            'specifiers. Please specify only the package name.')
    
    installed_package = pypmenv.installed_store.find_only_package(
        req.project_name)
    return installer.Installer(pypmenv).uninstall(installed_package)


#
# first run check
#

def add_pep370_dir_on_users_consent():
    """If ~/.local/bin is not in $PATH, automatically add them
    
    Do this only with the user's consent. And do not run this check more than
    once (i.e., run only when PyPM is *first run*.
    """
    if sys.platform.startswith('win'):
        return
    
    # Proceed only when the terminal is interactive and never run before
    isatty = (sys.stdin.isatty() and sys.stdout.isatty())
    firstrun_file = join(application.locations.user_cache_dir, '.firstrun-pep370')
    if (not isatty) or exists(firstrun_file):
        return
        
    pathenv = [xjoin(x.strip()) for x in os.environ.get('PATH', '').split(':')]
    binpath = xjoin(site.USER_BASE, 'bin')
    profile = expanduser('~/.profile' if sys.platform == 'darwin' else '~/.bashrc')
    profile_lines = [
        '# PEP 370 PATH added by PyPM on %s' % datetime.now(),
        'export PATH=%s:$PATH' % binpath,
    ]
    already_in_profile = exists(profile) and profile_lines[1] in [
        l.strip() for l in open(profile).readlines()
    ]
    
    # Proceed only if ~/.local/bin is neither in $PATH, nor added to profile
    if binpath in pathenv or already_in_profile:
        return
    
    # Add to profile on the user's consent
    msg = ('It seems that "%s" is not in your $PATH which we highly '
           'recommend. According to PEP 370, this is where packages '
           'will install their script files for you to access later. '
           'Would you like PyPM to do this now (by appending to '
           'your "%s")?' % (binpath, profile))
    if askyesno(msg, default=True):
        if exists(profile):
            shutil.copy(profile, profile+'.bak') # take a backup first
            
        with open(profile, 'a') as f:
            f.write('\n%s\n' % '\n'.join(profile_lines))
        print ('You may now reopen your shell for the changes to take effect.')
            
    os.makedirs(dirname(firstrun_file))
    with open(firstrun_file, 'w') as f: pass # prevent future runs


#
# sub-commands
#

@cmdln.option('-E', '--virtualenv', default=None,
              help='Work on the specified virtualenv directory')
@cmdln.option('-L', '--repository-urls', default=None, # refers to RepositorySet
              help='Whitespace separated list of repository URLs')
@cmdln.option('-n', '--no-autosync', action="store_true",
              help='Do not auto sync repositories')
@cmdln.option('-y', '--assume-yes', action="store_true",
              help=('Automatic yes to prompts; assume "yes" as answer '
                    'to all prompts and run non-interactively.'))
@cmdln.useconfig(filenames = [PYPM_CLIENT_CONF_FILE])
class Commands(log.LogawareCmdln):
    name = "pypm"
    version = '%s\nType "pypm info" for more details' % pypm.__version__

    def initialize(self):
        require_option(self.options, 'repository-urls')
        venv = self.options.virtualenv

        if venv:
            pyenv = python.VirtualPythonEnvironment(venv)
        else:
            pyenv = python.UserLocalPythonEnvironment()

        # Should we be interactive in CLI?
        interactive = not self.options.assume_yes
        isatty = (sys.stdin.isatty() and sys.stdout.isatty())
        if interactive and not isatty:
            LOG.warn('Terminal is not a TTY. Specify -y for a non-interactive run.')

        self.pypmenv = make_pypm_environment2(
            self.options.repository_urls.split(),
            interactive=interactive,
            pyenv=pyenv)

        LOG.debug("using pythonenv: %s", self.pypmenv.pyenv.root_dir)
        LOG.debug('using repo list: %s', self.options.repository_urls)

    @cmdln.option('-f', '--force', action="store_true",
                  help='force downloading of the index')
    def do_sync(self, subcmd, opts):
        """${cmd_name}: Download lists of new/upgradable packages

        PyPM uses a private index database to keep track of which packages are
        installed, which are not installed and which are available for
        installation. It uses this database to find out how to install packages
        requested by the user and to find out which additional packages are
        needed in order for a selected package to work properly.

        To update this list, you would use the command "pypm sync". This command
        downloads the index from the repositories specified in the "client.conf"
        (which is parsed by the ``ConfigParser`` module) file inside the
        ``pypm.client`` package.

        This command is automatically run once a day, so you do not have to run
        it manually.

        To find where the local index directory is located, run::
        
          $ pypm info
        
        ${cmd_usage}
        ${cmd_option_list}
        """
        with self.bootstrapped():
            self.pypmenv.repo_store.sync(opts.force)

    @cmdln.option('-w', '--web', action="store_true",
                  help="Search PyPI using your web browser")
    def do_search(self, subcmd, opts, *keywords):
        """${cmd_name}: Search for a package by name or description

        If no KEYWORDS are given, all available packages are listed.
        
        ${cmd_usage}
        ${cmd_option_list}
        """
        with self.bootstrapped():
            if opts.web:
                u = 'http://pypi.python.org/pypi?:action=search&term='
                webbrowser.open(u+' '.join(keywords))
            else:
                self._autosync()
                
                results = list(self.pypmenv.repo_store.search(*keywords))

                # prune duplicates
                verlists = collections.defaultdict(list)
                for pkg in results:
                    verlists[pkg.canonical_name].append(pkg)
                for k, verlist in verlists.items():
                    sort_packages_by_version(verlist)
                    
                # prepare what needs to be printed
                output_table = []
                is_be_user = activestate.user_has_be_license()
                print_be_warning = False
                for (name, verlist) in sorted(verlists.items()):
                    pkg = verlist[0] # pick the latest version
                    if 'be' in pkg.tags.split() and not is_be_user:
                        mark = '[BE]'
                        print_be_warning = True
                    else:
                        mark = ''
                    output_table.append((mark, name, pkg.summary or ''))
                    
                # If the user is not a BE user and there are BE-only packages
                # in the search results, show the standard BE warning message.
                if print_be_warning:
                    print >>sys.stderr, (
                        '*** Packages marked [BE] below require a valid \n'
                        '*** Business Edition license to install. Please visit\n'
                        '*** {0} for more details.\n').format(
                            activestate.BE_HOME_PAGE)
                colprint(output_table)
                
                return results

    @cmdln.option('-w', '--open-home-page', action="store_true",
                  help=('Also opens the project home page, '
                        'if available, in your webbrowser'))
    def do_show(self, subcmd, opts, name, version=None):
        """${cmd_name}: Display detailed information about a package

        If the package is already installed, show the location of the
        site-packages directory under which it is installed.
        
        ${cmd_usage}
        ${cmd_option_list}
        """
        with self.bootstrapped():
            self._autosync()
            # Show metadata stored in repository index
            pkg = self.pypmenv.repo_store.find_package(name, version)
            dependencies = ', '.join(pkg.install_requires[''])
            
            LOG.info(textwrap.dedent(u"""\
            Name: {0.name}
            Version: {0.version}-{0.pkg_version}
            Author: {0.author} <{0.author_email}>
            Summary: {0.summary}
            Home Page: {0.home_page}
            License: {0.license}
            Dependencies: {1}""").format(pkg, dependencies))
            
            # Is this package installed or not?
            try:
                ipkg = self.pypmenv.installed_store.find_only_package(name)
            except error.NoPackageInstalled:
                LOG.info('Status: Not installed')
                if pkg.requires_be_license and not activestate.user_has_be_license():
                    LOG.info(
                        'NOTE: This package requires a valid Business Edition '
                        'license. Please visit %s for more details.',
                        activestate.BE_HOME_PAGE)
            else:
                LOG.info('Status: Installed %s (%s) at %s',
                         ipkg.canonical_name,
                         ipkg.full_version,
                         self.pypmenv.pyenv.site_packages_dir)
                # Is a newer version available for upgrade?
                if compare_version(ipkg, pkg) < 0:
                    LOG.info('Status: '
                             'A newer version of this package is available. '
                             'Type "pypm install %s" to upgrade',
                             pkg.canonical_name)

            if opts.open_home_page:
                if pkg.home_page:
                    webbrowser.open(pkg.home_page)
                
            return pkg

    def do_files(self, subcmd, opts, name):
        """${cmd_name}: Display the list of files in an installed package

        ${cmd_usage}
        ${cmd_option_list}
        """
        with self.bootstrapped():
            pyenv = self.pypmenv.pyenv
            pkg = self.pypmenv.installed_store.find_only_package(name)

            # PEP 370 aware
            if isinstance(pyenv, python.UserLocalPythonEnvironment):
                base_dir = pyenv.user_base_dir
            else:
                base_dir = pyenv.root_dir
                
            for f in sorted(pkg.get_files_list()):
                f = join(base_dir, f)
                LOG.info('%s %s', f, '' if exists(f) else '(missing)')

    def do_install(self, subcmd, opts, name):
        """${cmd_name}: Install/upgrade package

        Installing a package will automatically install its dependencies that
        are specified via "install_requires" (in setuptools) or "requires"
        (distutils)
        
        Note that you can also pass in setuptools' style requirement string.

        Packages are by default installed to PEP 370 style user directories
        (~/.local or %APPDATA%\Python) unless the -E option is used (type 'pypm
        help virtualenv' for more information).
        
        ${cmd_usage}
        ${cmd_option_list}
        """
        with self.bootstrapped():
            self._autosync()
            install(self.pypmenv, name)

    
    @cmdln.alias('rm', 'remove', 'del', 'delete')
    def do_uninstall(self, subcmd, opts, package_name):
        """${cmd_name}: Uninstall package

        Removing a package will automatically uninstall other packages that
        depend on the package to be uninstall.
        
        Packages are by default uninstalled from PEP 370 style user directories
        (~/.local or %APPDATA%\Python) unless the -E option is used (type 'pypm
        help virtualenv' for more information).
        
        ${cmd_usage}
        ${cmd_option_list}
        """
        with self.bootstrapped():
            uninstall(self.pypmenv, package_name)
     
    
    @cmdln.alias('ls')
    def do_list(self, subcmd, opts):
        """${cmd_name}: List the currently installed packages

        As of now, PyPM only lists packages installed via PyPM
        itself. Eventually, the plan is to list every packages installed by all
        package managers.
        
        ${cmd_usage}
        ${cmd_option_list}
        """
        with self.bootstrapped():
            l = list(self.pypmenv.installed_store.find_all_packages())
            colprint([(ipkg.name, ipkg.full_version, ipkg.summary or '')
                      for ipkg in sorted(l, key=operator.attrgetter('name'))])
            return l
        

    def do_info(self, subcmd, otps):
        """${cmd_name}: Show version and other diagnostic details

        ${cmd_usage}
        ${cmd_option_list}
        """
        with self.bootstrapped():
            from pypm.common.activestate import user_has_be_license
            pyenv = python.CurrentPythonEnvironment()

            LOG.info('PyPM %s <%s>', pypm.__version__, dirname(pypm.__file__))
            try:
                import activestate
            except ImportError:
                LOG.warn('You are not running PyPM from ActivePython. '
                         'This is unsupported.')
            else:
                LOG.info('ActivePython (%s, %s, %s) built on %s',
                         activestate.version_info['python_src'][0],
                         activestate.version_info['build_num'],
                         activestate.version_info['scm_revision'],
                         activestate.version_info['build_time'])
            LOG.info('Installation target: Python %s <%s>',
                     self.pypmenv.pyenv.pyver, self.pypmenv.pyenv)
            LOG.info('Platform: %s', PLATNAME)
            LOG.info('Repositories: %s', [
                    r.url for r in self.pypmenv.repo_store.repository_list
                    ])
            LOG.info('Business Edition license installed: %s',
                     user_has_be_license())
            LOG.info('Log file: %s', application.locations.log_file_path)
            LOG.info('Repository index cache: %s', IDX_PATH)
            LOG.info('Download cache: %s', DOWNLOAD_CACHE)

    def _autosync(self):
        if not self.options.no_autosync:
            self.pypmenv.repo_store.autosync()

    def help_virtualenv(self):
        """Virtualenv integration

        PyPM supports virtualenv_ (a tool for creating isolated Python
        environments) with the ``-E`` option::

          pypm -E C:\myvirtualenv install pylons

        .. _virtualenv: http://pypi.python.org/pypi/virtualenv
        """
        return self.help_virtualenv.__doc__

    def help_proxy(self):
        """Proxies and Firewalls

        If you connect to the internet through a proxy server, you may need to
        set the ``http_proxy`` environment variable.

        Set the http_proxy variable with the hostname or IP address of the proxy
        server::

          http_proxy=http://proxy.example.org

        If the proxy server requires a user name and password, include them in the
        following form::

          http_proxy=http://username:password@proxy.example.org

        If the proxy server uses a port other than 80, include the port number::

          http_proxy=http://username:password@proxy.example.org:8080
        """
        return self.help_proxy.__doc__


def main():
    l = logging.getLogger('')
    log.setup_trace(l, application.locations.log_file_path)
    add_pep370_dir_on_users_consent()
    Commands(install_console=True).main()
