import os
import subprocess
from functools import lru_cache
from typing import Dict, Optional


class DpkgArchitectureBuildProcessValuesTable:
    """Dict-like interface to dpkg-architecture values"""

    def __init__(self, *, mocked_answers: Dict[str, str] = None):
        """Create a new dpkg-architecture table; NO INSTANTIATION

        This object will be created for you; if you need a production instance
        then call dpkg_architecture_table().  If you need a testing instance,
        then call mock_arch_table(...)

        :param mocked_answers: Used for testing purposes.  Do not use directly;
        instead use mock_arch_table(...) to create the table you want.
        """
        self._architecture_cache = {}
        self._has_run_dpkg_architecture = False
        if mocked_answers is None:
            self._architecture_cache = {}
            self._respect_environ = True
            self._has_run_dpkg_architecture = False
        else:
            self._architecture_cache = mocked_answers
            self._respect_environ = False
            self._has_run_dpkg_architecture = True

    def __contains__(self, item: str) -> bool:
        try:
            self[item]
        except KeyError:
            return False
        else:
            return True

    def __getitem__(self, item: str):
        if item not in self._architecture_cache:
            if self._respect_environ:
                value = os.environ.get(item)
                if value is not None:
                    self._architecture_cache[item] = value
                    return value
            if not self._has_run_dpkg_architecture:
                self._load_dpkg_architecture_values()
            # Fall through and look it up in the cache
        return self._architecture_cache[item]

    def __iter__(self) -> str:
        if not self._has_run_dpkg_architecture:
            self._load_dpkg_architecture_values()
        yield from self._architecture_cache

    @property
    def current_host_arch(self):
        """The architecture we are building for

        This is the architecture name you need if you are in doubt.
        """
        return self["DEB_HOST_ARCH"]

    @property
    def current_host_multiarch(self):
        """The multi-arch path basename

        This is the multi-arch basename name you need if you are in doubt.  It
        goes here:

            "/usr/lib/{MA}".format(table.current_host_multiarch)

        """
        return self["DEB_HOST_MULTIARCH"]

    @property
    def is_cross_compiling(self):
        """Whether we are cross-compiling

        This is defined as DEB_BUILD_GNU_TYPE != DEB_HOST_GNU_TYPE and
        affects whether we can rely on being able to run the binaries
        that are compiled.
        """
        return self["DEB_BUILD_GNU_TYPE"] != self["DEB_HOST_GNU_TYPE"]

    def _load_dpkg_architecture_values(self):
        env = dict(os.environ)
        # For performance, disable dpkg's translation later
        env["DPKG_NLS"] = "0"
        kw_pairs = _parse_dpkg_arch_output(
            subprocess.check_output(
                ["dpkg-architecture"],
                env=env,
            )
        )
        for k, v in kw_pairs:
            self._architecture_cache[k] = os.environ.get(k, v)
        self._has_run_dpkg_architecture = True


def _parse_dpkg_arch_output(output: bytes):
    text = output.decode("utf-8")
    for line in text.splitlines():
        k, v = line.strip().split("=", 1)
        yield k, v


def _rewrite(value, from_pattern, to_pattern):
    assert value.startswith(from_pattern)
    return to_pattern + value[len(from_pattern) :]


def faked_arch_table(
    host_arch: str,
    *,
    build_arch: Optional[str] = None,
    target_arch: Optional[str] = None,
) -> DpkgArchitectureBuildProcessValuesTable:
    """Creates a mocked instance of DpkgArchitectureBuildProcessValuesTable


    :param host_arch: The dpkg architecture to mock answers for.  This affects
      DEB_HOST_* values and defines the default for DEB_{BUILD,TARGET}_* if
      not overridden.
    :param build_arch: If set and has a different value than host_arch, then
      pretend this is a cross-build.  This value affects the DEB_BUILD_* values.
    :param target_arch: If set and has a different value than host_arch, then
      pretend this is a build _of_ a cross-compiler.  This value affects the
      DEB_TARGET_* values.
    """

    if build_arch is None:
        build_arch = host_arch

    if target_arch is None:
        target_arch = host_arch
    return _faked_arch_tables(host_arch, build_arch, target_arch)


@lru_cache
def _faked_arch_tables(
    host_arch: str, build_arch: str, target_arch: str
) -> DpkgArchitectureBuildProcessValuesTable:
    mock_table = {}

    env = dict(os.environ)
    # Set CC to /bin/true avoid a warning from dpkg-architecture
    env["CC"] = "/bin/true"
    # For performance, disable dpkg's translation later
    env["DPKG_NLS"] = "0"
    # Clear environ variables that might confuse dpkg-architecture
    for k in os.environ:
        if k.startswith("DEB_"):
            del env[k]

    if build_arch == host_arch:
        # easy / common case - we can handle this with a single call
        kw_pairs = _parse_dpkg_arch_output(
            subprocess.check_output(
                ["dpkg-architecture", "-a", host_arch, "-A", target_arch],
                env=env,
            )
        )
        for k, v in kw_pairs:
            if k.startswith(("DEB_HOST_", "DEB_TARGET_")):
                mock_table[k] = v
            # Clone DEB_HOST_* into DEB_BUILD_* as well
            if k.startswith("DEB_HOST_"):
                k2 = _rewrite(k, "DEB_HOST_", "DEB_BUILD_")
                mock_table[k2] = v
    elif build_arch != host_arch and host_arch != target_arch:
        # This will need two dpkg-architecture calls because we cannot set
        # DEB_BUILD_* directly.  But we can set DEB_HOST_* and then rewrite
        # it
        # First handle the build arch
        kw_pairs = _parse_dpkg_arch_output(
            subprocess.check_output(
                ["dpkg-architecture", "-a", build_arch],
                env=env,
            )
        )
        for k, v in kw_pairs:
            if k.startswith("DEB_HOST_"):
                k = _rewrite(k, "DEB_HOST_", "DEB_BUILD_")
                mock_table[k] = v

        kw_pairs = _parse_dpkg_arch_output(
            subprocess.check_output(
                ["dpkg-architecture", "-a", host_arch, "-A", target_arch],
                env=env,
            )
        )
        for k, v in kw_pairs:
            if k.startswith(("DEB_HOST_", "DEB_TARGET_")):
                mock_table[k] = v
    else:
        # This is a fun special case.  We know that:
        # * build_arch != host_arch
        # * host_arch == target_arch
        # otherwise we would have hit one of the previous cases.
        #
        # We can do this in a single call to dpkg-architecture by
        # a bit of "cleaver" rewriting.
        #
        # - Use -a to set DEB_HOST_* and then rewrite that as
        #   DEB_BUILD_*
        # - use -A to set DEB_TARGET_* and then use that for both
        #   DEB_HOST_* and DEB_TARGET_*

        kw_pairs = _parse_dpkg_arch_output(
            subprocess.check_output(
                ["dpkg-architecture", "-a", build_arch, "-A", target_arch], env=env
            )
        )
        for k, v in kw_pairs:
            if k.startswith("DEB_HOST_"):
                k2 = _rewrite(k, "DEB_HOST_", "DEB_BUILD_")
                mock_table[k2] = v
                continue
            if k.startswith("DEB_TARGET_"):
                mock_table[k] = v
                k2 = _rewrite(k, "DEB_TARGET_", "DEB_HOST_")
                mock_table[k2] = v

    table = DpkgArchitectureBuildProcessValuesTable(mocked_answers=mock_table)
    return table


_ARCH_TABLE = DpkgArchitectureBuildProcessValuesTable()


def dpkg_architecture_table() -> DpkgArchitectureBuildProcessValuesTable:
    return _ARCH_TABLE
