from pathlib import Path
import shlex
import subprocess
from typing import cast, List

from setuptools.command.sdist import sdist as setuptools_sdist
from setuptools.command.build_py import build_py as setuptools_build_py
from setuptools.command.install_lib import install_lib as setuptools_install_lib


class build_py(setuptools_build_py):

    def run(self) -> None:
        for package in self.packages:
            package_files = self.package_data.get(package, [])
            if 'VERSION' not in package_files:
                package_files.append('VERSION')
                self.package_data[package] = package_files
        super().run()


class sdist(setuptools_sdist):

    def make_release_tree(self, base_dir: str, files: List[str]) -> None:
        """Override 'orig.sdist.make_release_tree()' in order to append VERSION files to `sdist``
           distributions.

        The call stack is:
            sdist.run()
                sdist.make_distribution()
                    orig.sdist.make_distribution()
                        orig.sdist.make_release_tree()

        `orig.sdist.make_release_tree()` creates the directory structure for the distribution
        and copies all collected files (from `self.filelist`) into it. The archive is created
        by `orig.sdist.make_distribution()`. Between those, we can add our VERSION files to the
        tree.
        """
        super().make_release_tree(base_dir, files)

        buildpy: setuptools_build_py = cast(
            setuptools_build_py, self.get_finalized_command('build_py'))
        for package in buildpy.packages:
            version_file = Path(base_dir) / package / 'VERSION'
            print(f'writing {version_file}')
            with version_file.open('w+') as file:
                file.write(_describe())


class install_lib(setuptools_install_lib):

    def copy_tree(self,
                  infile: str,
                  outfile: str,
                  preserve_mode: int = 1,
                  preserve_times: int = 1,
                  preserve_symlinks: int = 0,
                  level: int = 1) -> List[str]:
        """Override `install_lib.copy_tree()` in order to generate a VERSION file for each package
           within `infile` directory, for the `bdist_wheel` command. This will then be copied to
           the `outfile` directory by the super method."""
        buildpy: setuptools_build_py = cast(
            setuptools_build_py, self.get_finalized_command('build_py'))
        print(type(outfile))
        for package in buildpy.packages:
            package_path = Path(infile) / package
            if package_path.is_dir():
                version_file = package_path / 'VERSION'
                print(f'creating {version_file}')
                with version_file.open('w+') as file:
                    file.write(_describe())
        return cast(List[str], super().copy_tree(
            infile, outfile, preserve_mode, preserve_times, preserve_symlinks, level))


def _describe(dirty_flag: str = '+dirty') -> str:
    """Attempt to describe the version from `git describe`, or return "unknown."""

    cmd = shlex.split(f'git describe --tags --always --dirty="{dirty_flag}"')
    try:
        proc = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
        if proc.returncode == 0:
            return proc.stdout.strip().decode()
    except (FileNotFoundError, subprocess.SubprocessError):
        pass
    return 'unknown'


def get(local_flag: str = '', dirty_flag: str = '+dirty') -> str:
    """Finds a version from either the VERSION file (generated by setup.py uppon 'sdist'), or
       from `git describe`.

    The `local_flag` can be passed to mark a version as running locally, when no VERSION file is
    present and `git describe` is used.

    This setup allows for building the correct version in the following cases:
    1. in setup.py on "sdist", `git describe` is used (without a local flag)
    2. in setup.py on "install", the VERSION file is used (previously packaged by "sdist")
    3. when running from a pip installation, the VERSION file is used
    4. when running locally, `git describe` plus a local flag is used, given the following
       definition in the packages __init__.py:
       ```
       __version__ = version.get(local_flag='.local')
       ```
    """
    # FIXME: __package__ will very likely give the wrong package, when version is an own package
    version_file = Path(str(__import__(__package__).__file__)).parent / 'VERSION'
    try:
        with version_file.open('r') as file:
            return file.read().strip()
    except OSError:
        return _describe(dirty_flag) + local_flag
