from collections import defaultdict
import io
import logging
import os
from pathlib import Path
from posixpath import join as pjoin
from pprint import pformat
import tarfile

from flit_core.sdist import SdistBuilder as SdistBuilderCore
from flit_core.common import Module, VCSError
from flit.vcs import identify_vcs

log = logging.getLogger(__name__)

# Our generated setup.py deliberately loads distutils, not setuptools, to
# discourage running it directly and getting a setuptools mess. Tools like pip
# handle this correctly - loading setuptools anyway but avoiding its issues.

SETUP = """\
#!/usr/bin/env python
# setup.py generated by flit for tools that don't yet use PEP 517

from distutils.core import setup

{before}
setup(name={name!r},
      version={version!r},
      description={description!r},
      author={author!r},
      author_email={author_email!r},
      url={url!r},
      {extra}
     )
"""




def namespace_packages(module: Module):
    """Get parent package names"""
    name_parts = []
    for part in module.namespace_package_name.split('.'):
        name_parts.append(part)
        yield '.'.join(name_parts)


def auto_packages(module: Module):
    """Discover subpackages and package_data"""
    pkgdir = os.path.normpath(str(module.path))
    pkg_name = module.name

    packages = []
    if module.in_namespace_package:
        packages.extend(namespace_packages(module))
    packages.append(pkg_name)

    pkg_data = defaultdict(list)
    # Undocumented distutils feature: the empty string matches all package names
    pkg_data[''].append('*')

    subpkg_paths = set()

    def find_nearest_pkg(rel_path):
        parts = rel_path.split(os.sep)
        for i in reversed(range(1, len(parts))):
            ancestor = '/'.join(parts[:i])
            if ancestor in subpkg_paths:
                pkg = '.'.join([pkg_name] + parts[:i])
                return pkg, '/'.join(parts[i:])

        # Relative to the top-level package
        return pkg_name, rel_path

    for path, dirnames, filenames in os.walk(pkgdir, topdown=True):
        if os.path.basename(path) == '__pycache__':
            continue

        from_top_level = os.path.relpath(path, pkgdir)
        if from_top_level == '.':
            continue

        is_subpkg = '__init__.py' in filenames
        if is_subpkg:
            subpkg_paths.add(from_top_level)
            parts = from_top_level.split(os.sep)
            packages.append('.'.join([pkg_name] + parts))
        else:
            pkg, from_nearest_pkg = find_nearest_pkg(from_top_level)
            pkg_data[pkg].append(pjoin(from_nearest_pkg, '*'))

    # Sort values in pkg_data
    pkg_data = {k: sorted(v) for (k, v) in pkg_data.items()}

    return sorted(packages), pkg_data


def include_path(p):
    return not (p.startswith('dist' + os.sep)
                or (os.sep+'__pycache__' in p)
                or p.endswith('.pyc'))


def _parse_req(requires_dist):
    """Parse "Foo (v); python_version == '2.x'" from Requires-Dist

    Returns pip-style appropriate for requirements.txt.
    """
    if ';' in requires_dist:
        name_version, env_mark = requires_dist.split(';', 1)
        env_mark = env_mark.strip()
    else:
        name_version, env_mark = requires_dist, None

    if '(' in name_version:
        # turn 'name (X)' and 'name (<X.Y)'
        # into 'name == X' and 'name < X.Y'
        name, version = name_version.split('(', 1)
        name = name.strip()
        version = version.replace(')', '').strip()
        if not any(c in version for c in '=<>'):
            version = '==' + version
        name_version = name + version

    return name_version, env_mark


def convert_requires(reqs_by_extra):
    """Regroup requirements by (extra, env_mark)"""
    grouping = defaultdict(list)
    for extra, reqs in reqs_by_extra.items():
        for req in reqs:
            name_version, env_mark = _parse_req(req)
            grouping[(extra, env_mark)].append(name_version)

    install_reqs = grouping.pop(('.none',  None), [])
    extra_reqs = {}
    for (extra, env_mark), reqs in grouping.items():
        if extra == '.none':
            extra = ''
        if env_mark is None:
            extra_reqs[extra] = reqs
        else:
            extra_reqs[extra + ':' + env_mark] = reqs

    return install_reqs, extra_reqs


class SdistBuilder(SdistBuilderCore):
    """Build a complete sdist

    This extends the minimal sdist-building in flit_core:

    - Include any files tracked in version control, such as docs sources and
      tests.
    - Add a generated setup.py for compatibility with tools which don't yet know
      about PEP 517.
    """
    def select_files(self):
        vcs_mod = identify_vcs(self.cfgdir)
        if vcs_mod is not None:
            untracked_deleted = vcs_mod.list_untracked_deleted_files(self.cfgdir)
            if any(include_path(p) and not self.excludes.match_file(p)
                   for p in untracked_deleted):
                raise VCSError(
                    "Untracked or deleted files in the source directory. "
                    "Commit, undo or ignore these files in your VCS.",
                    self.cfgdir)

            files = [os.path.normpath(p)
                     for p in vcs_mod.list_tracked_files(self.cfgdir)]
            files = sorted(filter(include_path, files))
            log.info("Found %d files tracked in %s", len(files), vcs_mod.name)
        else:
            files = super().select_files()

        return files

    def add_setup_py(self, files_to_add, target_tarfile):
        if 'setup.py' in files_to_add:
            log.warning(
                "Using setup.py from repository, not generating setup.py")
        else:
            setup_py = self.make_setup_py()
            log.info("Writing generated setup.py")
            ti = tarfile.TarInfo(pjoin(self.dir_name, 'setup.py'))
            ti.size = len(setup_py)
            target_tarfile.addfile(ti, io.BytesIO(setup_py))

    def make_setup_py(self):
        before, extra = [], []
        if self.module.is_package:
            packages, package_data = auto_packages(self.module)
            before.append("packages = \\\n%s\n" % pformat(sorted(packages)))
            before.append("package_data = \\\n%s\n" % pformat(package_data))
            extra.append("packages=packages,")
            extra.append("package_data=package_data,")
        else:
            extra.append("py_modules={!r},".format([self.module.name]))
            if self.module.in_namespace_package:
                packages = list(namespace_packages(self.module))
                before.append("packages = \\\n%s\n" % pformat(packages))
                extra.append("packages=packages,")

        if self.module.prefix:
            package_dir = pformat({'': self.module.prefix})
            before.append("package_dir = \\\n%s\n" % package_dir)
            extra.append("package_dir=package_dir,")

        install_reqs, extra_reqs = convert_requires(self.reqs_by_extra)
        if install_reqs:
            before.append("install_requires = \\\n%s\n" % pformat(install_reqs))
            extra.append("install_requires=install_requires,")
        if extra_reqs:
            before.append("extras_require = \\\n%s\n" % pformat(extra_reqs))
            extra.append("extras_require=extras_require,")

        entrypoints = self.prep_entry_points()
        if entrypoints:
            before.append("entry_points = \\\n%s\n" % pformat(entrypoints))
            extra.append("entry_points=entry_points,")

        if self.metadata.requires_python:
            extra.append('python_requires=%r,' % self.metadata.requires_python)

        return SETUP.format(
            before='\n'.join(before),
            name=self.metadata.name,
            version=self.metadata.version,
            description=self.metadata.summary,
            author=self.metadata.author,
            author_email=self.metadata.author_email,
            url=self.metadata.home_page,
            extra='\n      '.join(extra),
        ).encode('utf-8')

