import logging
import re
try:
    import dnf
except ImportError:
    dnf = None
from pkg_resources import safe_name

from pyp2rpm import settings


logger = logging.getLogger(__name__)


class NameConvertor(object):
    distro = settings.DEFAULT_TEMPLATE

    def __init__(self, distro):
        self.distro = distro
        self.reg_start = re.compile(r'^[Pp]ython(\d*|)-(.*)')
        self.reg_end = re.compile(r'(.*)-(python)(\d*|)$')

    @classmethod
    def get_default_py_version(cls):
        try:
            return settings.DEFAULT_PYTHON_VERSIONS[cls.distro][0]
        except KeyError:
            logger.error('Default python versions for template {0} are '
                         'missing in settings, using versions of template '
                         '{1}.'.format(cls.distro,
                                       settings.DEFAULT_TEMPLATE))
            return settings.DEFAULT_PYTHON_VERSIONS[
                settings.DEFAULT_TEMPLATE][0]

    @classmethod
    def rpm_versioned_name(cls, name, version, default_number=False,
                           use_macros=False):
        """Properly versions the name.
        For example:
        rpm_versioned_name('python-foo', '26') will return python26-foo
        rpm_versioned_name('pyfoo, '3') will return python3-pyfoo

        If version is same as settings.DEFAULT_PYTHON_VERSION, no change
        is done.

        Args:
            name: name to version
            version: version or None
        Returns:
            Versioned name or the original name if given version is None.
        """
        regexp = re.compile(r'^python(\d*|)-(.*)')
        auto_provides_regexp = re.compile(r'^python(\d*|)dist(.*)')

        if (not version or version == cls.get_default_py_version() and
                not default_number):
            found = regexp.search(name)
            # second check is to avoid renaming of python2-devel to
            # python-devel
            if found and found.group(2) != 'devel':
                if 'epel' not in cls.distro:
                    return 'python-{0}'.format(regexp.search(name).group(2))
            return name

        versioned_name = name
        if version:

            if regexp.search(name):
                versioned_name = re.sub(r'^python(\d*|)-', 'python{0}-'.format(
                    version), name)
            elif auto_provides_regexp.search(name):
                versioned_name = re.sub(
                    r'^python(\d*|)dist', 'python{0}dist'.format(
                        version), name)

            else:
                versioned_name = 'python{0}-{1}'.format(version, name)
            if (use_macros and 'epel' in cls.distro and version !=
                    cls.get_default_py_version()):
                versioned_name = versioned_name.replace('python{0}-'.format(
                    version), 'python%{{python{0}_pkgversion}}-'.format(version))
        return versioned_name

    def rpm_name(self, name, python_version=None, pkg_name=False):
        """Returns name of the package converted to (possibly) correct package
           name according to Packaging Guidelines.
        Args:
            name: name to convert
            python_version: python version for which to retrieve the name of
                            the package
            pkg_name: flag to perform conversion of rpm package name,
                      present in this class just for API compatibility reason
        Returns:
            Converted name of the package, that should be in line with
            Fedora Packaging Guidelines. If for_python is not None,
            the returned name is in form python%(version)s-%(name)s
        """
        logger.debug("Converting name: {0} to rpm name, version: {1}.".format(
            name, python_version))
        rpmized_name = self.base_name(name)

        rpmized_name = 'python-{0}'.format(rpmized_name)

        if self.distro == 'mageia':
            rpmized_name = rpmized_name.lower()
        logger.debug('Rpmized name of {0}: {1}.'.format(name, rpmized_name))
        return NameConvertor.rpm_versioned_name(rpmized_name, python_version)

    def base_name(self, name):
        """Removes any python prefixes of suffixes from name if present."""
        base_name = name.replace('.', "-")
        # remove python prefix if present
        found_prefix = self.reg_start.search(name)
        if found_prefix:
            base_name = found_prefix.group(2)

        # remove -pythonXY like suffix if present
        found_end = self.reg_end.search(name.lower())
        if found_end:
            base_name = found_end.group(1)

        return base_name


class NameVariants(object):
    """Class to generate variants of python package name and choose
    most likely correct one.
    """

    def __init__(self, name, version, py_init=True):
        self.name = name
        self.version = version
        self.variants = {}
        if py_init:
            self.names_init()
            self.variants_init()

    def find_match(self, name):
        for variant in ['python_ver_name', 'pyver_name', 'name_python_ver',
                        'raw_name']:
            # iterates over all variants and store name to variants if matches
            if canonical_form(name) == canonical_form(getattr(self, variant)):
                self.variants[variant] = name

    def merge(self, other):
        """Merges object with other NameVariants object, not set values
        of self.variants are replace by values from other object.
        """
        if not isinstance(other, NameVariants):
            raise TypeError("NameVariants isinstance can be merge with"
                            "other isinstance of the same class")
        for key in self.variants:
            self.variants[key] = self.variants[key] or other.variants[key]

        return self

    def names_init(self):
        self.python_ver_name = 'python{0}-{1}'.format(self.version, self.name)
        self.pyver_name = self.name if self.name.startswith(
            'py') else 'py{0}{1}'.format(self.version, self.name)
        self.name_python_ver = '{0}-python{1}'.format(self.name, self.version)
        self.raw_name = self.name

    def variants_init(self):
        self.variants = {'python_ver_name': None,
                         'pyver_name': None,
                         'name_python_ver': None,
                         'raw_name': None}

    @property
    def best_matching(self):
        return (self.variants['python_ver_name'] or
                self.variants['pyver_name'] or
                self.variants['name_python_ver'] or
                self.variants['raw_name'])


class DandifiedNameConvertor(NameConvertor):
    """Name convertor based on DNF API query, checks if converted
    name of the package exists in Fedora repositories. If it doesn't, searches
    for the correct variant of the name.
    """

    def __init__(self, *args):
        super(DandifiedNameConvertor, self).__init__(*args)
        if dnf is None:
            raise RuntimeError("DandifiedNameConvertor needs an optional "
                               "requirement dnf.")
        with dnf.Base() as base:
            RELEASEVER = dnf.rpm.detect_releasever(base.conf.installroot)
            base.conf.substitutions['releasever'] = RELEASEVER
            base.read_all_repos()
            base.fill_sack()
            self.query = base.sack.query()

    def rpm_name(self, name, python_version=None, pkg_name=False):
        """Checks if name converted using superclass rpm_name_method match name
        of package in the query. Searches for correct name if it doesn't.
        Args:
            name: name to convert
            python_version: python version for which to retrieve the name of
                            the package
            pkg_name: flag to perform conversion of rpm package name
                      (foo -> python-foo)
        """
        if pkg_name:
            return super(DandifiedNameConvertor, self).rpm_name(
                name, python_version)
        original_name = name
        converted = super(DandifiedNameConvertor, self).rpm_name(
            name, python_version)
        python_query = self.query.filter(name__substr=[
            'python', 'py', original_name, canonical_form(original_name)])
        if converted in [pkg.name for pkg in python_query]:
            logger.debug("Converted name exists")
            return converted

        logger.debug("Converted name not found, searches for correct form")

        not_versioned_name = NameVariants(self.base_name(original_name), '')
        versioned_name = NameVariants(self.base_name(original_name),
                                      python_version)

        if self.base_name(original_name).startswith("py"):
            nonpy_name = NameVariants(self.base_name(
                original_name)[2:], python_version)

        for pkg in python_query:
            versioned_name.find_match(pkg.name)
            not_versioned_name.find_match(pkg.name)
            if 'nonpy_name' in locals():
                nonpy_name.find_match(pkg.name)

        if 'nonpy_name' in locals():
            versioned_name = versioned_name.merge(nonpy_name)

        correct_form = versioned_name.merge(not_versioned_name).best_matching
        logger.debug("Most likely correct form of the name {0}.".format(
            correct_form))
        return correct_form or converted


def canonical_form(name):
    return name.lower().replace('-', '').replace('_', '')


class AutoProvidesNameConvertor(NameConvertor):
    """Name convertor based on Automatic Provides for Python RPM Packages.
    Args:
        name: name to convert
        python_version: python version for which to retrieve the name of
                        the package
        pkg_name: flag to perform conversion of rpm package name
                  (foo -> python-foo)
    """
    def rpm_name(self, name, python_version=settings.DEFAULT_PYTHON_VERSION,
                 pkg_name=False):
        if pkg_name:
            return super(AutoProvidesNameConvertor, self).rpm_name(
                name, python_version)
        canonical_name = safe_name(name).lower()
        return "python{0}dist({1})".format(python_version, canonical_name)