"""
Functions to handle software packages. The functions covered here aim to be
generic, with implementations that deal with different package managers, such
as dpkg and rpm.
"""

__author__ = 'lucasmr@br.ibm.com (Lucas Meneghel Rodrigues)'

import os, re, logging
from autotest.client import os_dep, utils
from autotest.client.shared import error

# As more package methods are implemented, this list grows up
KNOWN_PACKAGE_MANAGERS = ['rpm', 'dpkg']


def _rpm_info(rpm_package):
    """\
    Private function that returns a dictionary with information about an
    RPM package file
    - type: Package management program that handles the file
    - system_support: If the package management program is installed on the
    system or not
    - source: If it is a source (True) our binary (False) package
    - version: The package version (or name), that is used to check against the
    package manager if the package is installed
    - arch: The architecture for which a binary package was built
    - installed: Whether the package is installed (True) on the system or not
    (False)
    """
    # We will make good use of what the file command has to tell us about the
    # package :)
    file_result = utils.system_output('file ' + rpm_package)
    package_info = {}
    package_info['type'] = 'rpm'
    try:
        os_dep.command('rpm')
        # Build the command strings that will be used to get package info
        # s_cmd - Command to determine if package is a source package
        # a_cmd - Command to determine package architecture
        # v_cmd - Command to determine package version
        # i_cmd - Command to determiine if package is installed
        s_cmd = 'rpm -qp --qf %{SOURCE} ' + rpm_package + ' 2>/dev/null'
        a_cmd = 'rpm -qp --qf %{ARCH} ' + rpm_package + ' 2>/dev/null'
        v_cmd = 'rpm -qp ' + rpm_package + ' 2>/dev/null'
        i_cmd = 'rpm -q ' + utils.system_output(v_cmd) + ' 2>&1 >/dev/null'

        package_info['system_support'] = True
        # Checking whether this is a source or src package
        source = utils.system_output(s_cmd)
        if source == '(none)':
            package_info['source'] = False
        else:
            package_info['source'] = True
        package_info['version'] = utils.system_output(v_cmd)
        package_info['arch'] = utils.system_output(a_cmd)
        # Checking if package is installed
        try:
            utils.system(i_cmd)
            package_info['installed'] = True
        except Exception:
            package_info['installed'] = False

    except Exception:
        package_info['system_support'] = False
        package_info['installed'] = False
        # File gives a wealth of information about rpm packages.
        # However, we can't trust all this info, as incorrectly
        # packaged rpms can report some wrong values.
        # It's better than nothing though :)
        if len(file_result.split(' ')) == 6:
            # Figure if package is a source package
            if file_result.split(' ')[3] == 'src':
                package_info['source'] = True
            elif file_result.split(' ')[3] == 'bin':
                package_info['source'] = False
            else:
                package_info['source'] = False
            # Get architecture
            package_info['arch'] = file_result.split(' ')[4]
            # Get version
            package_info['version'] = file_result.split(' ')[5]
        elif len(file_result.split(' ')) == 5:
            # Figure if package is a source package
            if file_result.split(' ')[3] == 'src':
                package_info['source'] = True
            elif file_result.split(' ')[3] == 'bin':
                package_info['source'] = False
            else:
                package_info['source'] = False
            # When the arch param is missing on file, we assume noarch
            package_info['arch'] = 'noarch'
            # Get version
            package_info['version'] = file_result.split(' ')[4]
        else:
            # If everything else fails...
            package_info['source'] =  False
            package_info['arch'] = 'Not Available'
            package_info['version'] = 'Not Available'
    return package_info


def _dpkg_info(dpkg_package):
    """\
    Private function that returns a dictionary with information about a
    dpkg package file
    - type: Package management program that handles the file
    - system_support: If the package management program is installed on the
    system or not
    - source: If it is a source (True) our binary (False) package
    - version: The package version (or name), that is used to check against the
    package manager if the package is installed
    - arch: The architecture for which a binary package was built
    - installed: Whether the package is installed (True) on the system or not
    (False)
    """
    # We will make good use of what the file command has to tell us about the
    # package :)
    file_result = utils.system_output('file ' + dpkg_package)
    package_info = {}
    package_info['type'] = 'dpkg'
    # There's no single debian source package as is the case
    # with RPM
    package_info['source'] = False
    try:
        os_dep.command('dpkg')
        # Build the command strings that will be used to get package info
        # a_cmd - Command to determine package architecture
        # v_cmd - Command to determine package version
        # i_cmd - Command to determiine if package is installed
        a_cmd = 'dpkg -f ' + dpkg_package + ' Architecture 2>/dev/null'
        v_cmd = 'dpkg -f ' + dpkg_package + ' Package 2>/dev/null'
        i_cmd = 'dpkg -s ' + utils.system_output(v_cmd) + ' 2>/dev/null'

        package_info['system_support'] = True
        package_info['version'] = utils.system_output(v_cmd)
        package_info['arch'] = utils.system_output(a_cmd)
        # Checking if package is installed
        package_status = utils.system_output(i_cmd, ignore_status=True)
        not_inst_pattern = re.compile('not-installed', re.IGNORECASE)
        dpkg_not_installed = re.search(not_inst_pattern, package_status)
        if dpkg_not_installed:
            package_info['installed'] = False
        else:
            package_info['installed'] = True

    except Exception:
        package_info['system_support'] = False
        package_info['installed'] = False
        # The output of file is not as generous for dpkg files as
        # it is with rpm files
        package_info['arch'] = 'Not Available'
        package_info['version'] = 'Not Available'

    return package_info


def list_all():
    """Returns a list with the names of all currently installed packages."""
    logging.debug("Listing all packages available")
    support_info = os_support()
    installed_packages = []

    if support_info['rpm']:
        cmd_result = utils.run('rpm -qa', ignore_status=True, verbose=False)
        installed_packages += cmd_result.stdout.splitlines()

    if support_info['dpkg']:
        cmd_result = utils.run('dpkg -l', ignore_status=True, verbose=False)
        raw_list = cmd_result.stdout.splitlines()[5:]
        for line in raw_list:
            parts = line.split()
            if parts[0] == "ii":  # only grab "installed" packages
                installed_packages.append("%s-%s" % (parts[1], parts[2]))

    return installed_packages


def info(package):
    """\
    Returns a dictionary with package information about a given package file:
    - type: Package management program that handles the file
    - system_support: If the package management program is installed on the
    system or not
    - source: If it is a source (True) our binary (False) package
    - version: The package version (or name), that is used to check against the
    package manager if the package is installed
    - arch: The architecture for which a binary package was built
    - installed: Whether the package is installed (True) on the system or not
    (False)

    Implemented package types:
    - 'dpkg' - dpkg (debian, ubuntu) package files
    - 'rpm' - rpm (red hat, suse) package files
    Raises an exception if the package type is not one of the implemented
    package types.
    """
    if not os.path.isfile(package):
        raise ValueError('invalid file %s to verify' % package)
    # Use file and libmagic to determine the actual package file type.
    file_result = utils.system_output('file ' + package)
    for package_manager in KNOWN_PACKAGE_MANAGERS:
        if package_manager == 'rpm':
            package_pattern = re.compile('RPM', re.IGNORECASE)
        elif package_manager == 'dpkg':
            package_pattern = re.compile('Debian', re.IGNORECASE)

        result = re.search(package_pattern, file_result)

        if result and package_manager == 'rpm':
            return _rpm_info(package)
        elif result and package_manager == 'dpkg':
            return _dpkg_info(package)

    # If it's not one of the implemented package manager methods, there's
    # not much that can be done, hence we throw an exception.
    raise error.PackageError('Unknown package type %s' % file_result)


def install(package, nodeps = False):
    """\
    Tries to install a package file. If the package is already installed,
    it prints a message to the user and ends gracefully. If nodeps is set to
    true, it will ignore package dependencies.
    """
    my_package_info = info(package)
    type = my_package_info['type']
    system_support = my_package_info['system_support']
    source = my_package_info['source']
    installed = my_package_info['installed']

    if not system_support:
        e_msg = ('Client does not have package manager %s to handle %s install'
                 % (type, package))
        raise error.PackageError(e_msg)

    opt_args = ''
    if type == 'rpm':
        if nodeps:
            opt_args = opt_args + '--nodeps'
        install_command = 'rpm %s -U %s' % (opt_args, package)
    if type == 'dpkg':
        if nodeps:
            opt_args = opt_args + '--force-depends'
        install_command = 'dpkg %s -i %s' % (opt_args, package)

    # RPM source packages can be installed along with the binary versions
    # with this check
    if installed and not source:
        return 'Package %s is already installed' % package

    # At this point, the most likely thing to go wrong is that there are
    # unmet dependencies for the package. We won't cover this case, at
    # least for now.
    utils.system(install_command)
    return 'Package %s was installed successfuly' % package


def convert(package, destination_format):
    """\
    Convert packages with the 'alien' utility. If alien is not installed, it
    throws a NotImplementedError exception.
    returns: filename of the package generated.
    """
    try:
        os_dep.command('alien')
    except Exception:
        e_msg = 'Cannot convert to %s, alien not installed' % destination_format
        raise error.TestError(e_msg)

    # alien supports converting to many formats, but its interesting to map
    # convertions only for the implemented package types.
    if destination_format == 'dpkg':
        deb_pattern = re.compile('[A-Za-z0-9_.-]*[.][d][e][b]')
        conv_output = utils.system_output('alien --to-deb %s 2>/dev/null'
                                          % package)
        converted_package = re.findall(deb_pattern, conv_output)[0]
    elif destination_format == 'rpm':
        rpm_pattern = re.compile('[A-Za-z0-9_.-]*[.][r][p][m]')
        conv_output = utils.system_output('alien --to-rpm %s 2>/dev/null'
                                          % package)
        converted_package = re.findall(rpm_pattern, conv_output)[0]
    else:
        e_msg = 'Convertion to format %s not implemented' % destination_format
        raise NotImplementedError(e_msg)

    print 'Package %s successfuly converted to %s' % \
            (os.path.basename(package), os.path.basename(converted_package))
    return os.path.abspath(converted_package)


def os_support():
    """\
    Returns a dictionary with host os package support info:
    - rpm: True if system supports rpm packages, False otherwise
    - dpkg: True if system supports dpkg packages, False otherwise
    - conversion: True if the system can convert packages (alien installed),
    or False otherwise
    """
    support_info = {}
    for package_manager in KNOWN_PACKAGE_MANAGERS:
        try:
            os_dep.command(package_manager)
            support_info[package_manager] = True
        except Exception:
            support_info[package_manager] = False

    try:
        os_dep.command('alien')
        support_info['conversion'] = True
    except Exception:
        support_info['conversion'] = False

    return support_info
