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

"""
    pypm.client.fs
    ~~~~~~~~~~~~~~

    Functionality related to Input/Output .. i.e., downloading, extracting, etc..
"""

import os
import sys
import logging
import tempfile
from urlparse import urlparse
from urllib2 import HTTPError

from pypm.common import console, net, python, activestate
from pypm.common.util import xjoin, existing
from pypm.common.package import PackageFile
from pypm.common.repository import RepoPackage
from pypm.client.base import PyPMFeature, application
from pypm.client import error

LOG = logging.getLogger(__name__)

DOWNLOAD_CACHE = xjoin(application.locations.user_cache_dir, 'download-cache')

class Downloader(PyPMFeature):

    def download_package(self, package):
        assert type(package) is RepoPackage

        console.mkdirs(DOWNLOAD_CACHE)
        
        LOG.info('Get: [%s] %s %s',
                 urlparse(package.download_url).netloc,
                 package.canonical_name,
                 package.full_version)
        
        auth = activestate.get_be_license_auth()
        send_license = package.requires_be_license
        license_installed = auth is not None
        
        access_msg = ('Access to package "{0.full_name}" requires a valid ' +
                      'Business Edition license. Please visit {1} for more ' +
                      'details').format(package, activestate.BE_HOME_PAGE)
        
        if not license_installed and send_license:
            raise error.PackageAccessError(
                package,
                'requires Business Edition subscription',
                ('Please visit {0} to learn more\n'
                 'about the ActivePython Business Edition offering.'
                ).format(activestate.BE_HOME_PAGE))
       
        try:
            # At this point, the user is already know to have a BE license
            file_location = net.download_file(
                package.download_url, DOWNLOAD_CACHE, dict(auth=auth))
        except HTTPError, e:
            reason = str(e)
            LOG.debug("HTTPError while accessing URL: %s -- reason: %s",
                      package.download_url, reason)
            
            if send_license and e.code in (401, 402, 403):
                msg = (
                    'Your ActivePython Business Edition subscription seems to '
                    'have expired.\n'
                    'Please visit your account at '
                    'https://account.activestate.com to \n'
                    'renew your subscription.'
                )
            else:
                msg = ''
                
            raise error.PackageAccessError(package, reason, msg)

        return file_location



class Extractor(PyPMFeature):
    """Extracts the binary package to Python directory

    This is not as simple as it may sound. While we build all packages in a
    simple Python directory structure (including virtualenv) and store that very
    same directory structure in the created binary packages, the *user* may be
    using a different directory structure.

    PEP 370, for example, uses $APPDATA/Python/Python26 as LIB directory on
    Windows. But as far as the binary package file is concerned, when extracted
    as it is over $APPDATA/Python, it implicitly expects the LIB directory to be
    $APPDATA/Python/Lib).

    The perfect solution to this problem is to support EGG format .. which do
    not assume any directory structure at all. However, we have to be practical
    .. and gto with the special-case hack for Windows during the first beta
    releases. Perhaps later, we can drop these hacks when supporting EGG format.
    """

    def extract_package(self, pypm_filename):
        """Extract files of the package over Python directory"""
        bpkgfile = PackageFile(pypm_filename)
        pyenv = self.pypmenv.pyenv
        
        if isinstance(pyenv, python.UserLocalPythonEnvironment):
            return self._extract_pep370(bpkgfile)
        else:
            return self._extract_generic(bpkgfile)

    def _extract_generic(self, bpkgfile):
        """Extract package to a generic Python environment"""
        bpkgfile.extract_over(self.pypmenv.pyenv.root_dir)
        return bpkgfile.get_files_list()

    def _extract_pep370(self, bpkgfile):
        """Extract to PEP370 user local directory"""
        pyenv = self.pypmenv.pyenv
        assert isinstance(pyenv, python.UserLocalPythonEnvironment)
        if sys.platform.startswith('win'):
            return self._extract_pep370_windows(bpkgfile)
        else:
            bpkgfile.extract_over(pyenv.user_base_dir)
            return bpkgfile.get_files_list()

    def _extract_pep370_windows(self, bpkgfile):
        """Extract to PEP370 user local directory on Windows

        Unlike others, on Windows .. Lib/ directory is different:
        $APPDATA/Python/PythonXY
        """
        pyenv = self.pypmenv.pyenv
        assert isinstance(pyenv, python.UserLocalPythonEnvironment)
        
        # hack: extract all to a temporary directory first and then rename Lib/
        # to PythonXY/ - before copying them back to `pyenv.user_base_dir`
        # XXX: what if pyenv.user_site_dir differs from the default?
        new_lib_dir = 'Python{0[0]}{0[1]}'.format(pyenv.pyversion_info)
        with bpkgfile.extract_over2(pyenv.user_base_dir) as tf:
            for tinfo in tf.getmembers():
                if tinfo.name.lower().startswith('lib'):
                    tinfo.name = new_lib_dir + tinfo.name[3:]

            return tf.getnames()

    def undo_extract(self, files_list):
        """Undo whatever self.extract_package did"""
        # sort in descending order so that children of a directory
        # get removed before the directory itself
        files_list.sort()
        files_list.reverse()
        
        for path in files_list:
            path = self.pypmenv.pyenv.get_abspath(path)
            
            if not os.path.exists(path):
                LOG.warn('no longer exists: %s', path)
            else:
                if os.path.isfile(path):
                    console.rm(path)
                    # remove the corresponding .pyc and .pyo files
                    if path.endswith('.py'):
                        console.rm(path+'c')
                        console.rm(path+'o')
                elif os.path.isdir(path):
                    if len(os.listdir(path)) > 0:
                        # cannot delete directory with files added
                        # after the installation
                        LOG.debug(
                            'non-empty directory: %s - hence skipping', path)
                    else:
                        # directory `path` is empty
                        console.rm(path)
                else:
                    raise TypeError, (
                        "don't know what to do with this type of file: " + path)
    
