import sys
from collections import defaultdict
import apt_pkg
from debian import deb822
from functools import cmp_to_key
import subprocess
import io
import json
import networkx as nx
import yaml
try:
    from yaml import CBaseLoader as yamlLoader
except ImportError:
    from yaml import BaseLoader as yamlLoader


def write_plain(path):
    class fh_out:
        fd = None
        path = None

        def __init__(self, path):
            self.path = path

        def __enter__(self):
            if self.path == "-":
                self.fd = sys.stdout
            else:
                self.fd = open(self.path, "w", encoding="utf8")
            return self.fd

        def __exit__(self, type, value, traceback):
            self.fd.close()
    return fh_out(path)


def read_yaml_file(path):
    if path == '-':
        data = yaml.load(sys.stdin, Loader=yamlLoader)
    else:
        with open(path) as f:
            data = yaml.load(f, Loader=yamlLoader)
    return data


def read_json_file(path):
    if path == '-':
        return json.load(sys.stdin)
    else:
        with open(path) as f:
            return json.load(f)


def read_graphml(path):
    if path == '-':
        return nx.read_graphml(sys.stdin)
    else:
        return nx.read_graphml(path)


def write_graphml(path):
    if path == '-':
        return lambda g: nx.write_graphml(g, sys.stdout)
    else:
        return lambda g: nx.write_graphml(g, path)


def write_dot(path):
    if path == '-':
        return lambda g: nx.write_dot(g, sys.stdout)
    else:
        return lambda g: nx.write_dot(g, path)


class fh_out:
    fd = None
    path = None

    def __init__(self, path):
        self.path = path

    def __enter__(self):
        if self.path == "-":
            self.fd = getattr(sys.stdout, 'buffer', sys.stdout)
        elif self.path.endswith(".gz"):
            import gzip
            self.fd = gzip.GzipFile(self.path, "w")
        elif self.path.endswith(".bz2"):
            import bz2
            self.fd = bz2.BZ2File(self.path, "w")
        elif self.path.endswith(".xz"):
            import lzma
            self.fd = lzma.LZMAFile(self.path, "w")
        else:
            self.fd = open(self.path, "wb")
        return self.fd

    def __exit__(self, type, value, traceback):
        self.fd.close()


class fh_out_read_write(fh_out):
    pkgs = []

    def __init__(self, path, pkgs):
        fh_out.__init__(self, path)
        self.pkgs = pkgs

    def __iter__(self):
        return iter(self.pkgs)


def get_fh_out(path):
    return fh_out(path)


def read_tag_file(path):
    # we read all the input into memory because some of the decompressors can
    # only create file like objects from filenames anyway
    # juggling with multiple file descriptors and making sure they are all
    # cleaned up afterwards seems hard enough to just waste some megs of memory
    # instead
    if path == '-':
        data = getattr(sys.stdin, 'buffer', sys.stdin).read()
    else:
        with open(path, "rb") as f:
            data = f.read()
    if data[:2] == b"\x1f\x8b":
        import gzip
        with io.BytesIO(data) as f:
            data = gzip.GzipFile(fileobj=f).read()
    elif data[:3] == b"BZh":
        import bz2
        data = bz2.decompress(data)
    elif data[:5] == b"\xfd7zXZ":
        import lzma
        data = lzma.decompress(data)
    with io.BytesIO(data) as f:
        # not using apt_pkg because that will leave file handles open
        # (see bug#748922)
        # not using apt_pkg because it can't transparently decompress data from
        # filehandles
        pkgs = list(deb822.Deb822.iter_paragraphs(f, use_apt_pkg=False))
    return pkgs


def read_write_tag_file(path):
    pkgs = read_tag_file(path)
    return fh_out_read_write(path, pkgs)


def read_fas(filename):
    fas = defaultdict(set)
    with open(filename) as f:
        for line in f:
            # remove everything after first '#'
            line = line.split('#', 1)[0]
            line = line.strip()
            if not line:
                continue
            src, deps = line.split(' ', 1)
            fas[src].update(deps)
    return fas


def read_weak_deps(filename):
    weak_deps = set()
    with open(filename) as f:
        for line in f:
            # remove everything after first '#'
            line = line.split('#', 1)[0]
            line = line.strip()
            if not line:
                continue
            weak_deps.add(line)
    return weak_deps


def read_reduced_deps(filenames):
    reduced_deps = defaultdict(set)
    for filename in filenames.split(','):
        with open(filename) as f:
            for line in f:
                # remove everything after first '#'
                line = line.split('#', 1)[0]
                line = line.strip()
                if not line:
                    continue
                src, pkgs = line.split(' ', 1)
                pkgs = pkgs.split(' ')
                reduced_deps[src].update(set(pkgs))
    return reduced_deps


def graph_remove_weak(g, weak_deps):
    def is_weak(a):
        if a['kind'] == 'SrcPkg':
            return False
        n = a['name'].split(':')[1]
        return n in weak_deps
    weak_inst_sets = [n for n, a in g.nodes_iter(data=True) if is_weak(a)]
    g.remove_nodes_from(weak_inst_sets)


def graph_remove_droppable(g, reduced_deps):
    def is_droppable(e):
        v1, v2 = e
        if g.node[v1]['kind'] == 'InstSet':
            False
        n1 = g.node[v1]['name']
        n2 = g.node[v2]['name'].split(':')[1]
        return n1 in reduced_deps and n2 in reduced_deps[n1]
    droppable_edges = [e for e in g.edges_iter() if is_droppable(e)]
    g.remove_edges_from(droppable_edges)

apt_pkg.init()


def cmp(a, b):
    return (a > b) - (a < b)

# sort by name, then by version, then by arch


def sort_pkgs(pkg1, pkg2):
    n1, a1, v1 = pkg1
    n2, a2, v2 = pkg2
    name_cmp = cmp(n1, n2)
    if name_cmp:
        return name_cmp
    else:
        ver_cmp = apt_pkg.version_compare(v1, v2)
        if ver_cmp:
            return ver_cmp
        else:
            return cmp(a1, a2)

sort_pkgs_key = cmp_to_key(sort_pkgs)

_arch_matches_cache = dict()


def arch_matches(arch, wildcard):
    if wildcard == 'any' or wildcard == 'all':
        return True
    cached = _arch_matches_cache.get((arch, wildcard), None)
    if cached is not None:
        return cached
    # environment must be empty or otherwise the DEB_HOST_ARCH environment
    # variable will influence the result
    ret = subprocess.call(
        ['dpkg-architecture', '-i%s' % wildcard, '-a%s' % arch],
        env={})
    ret = True if ret == 0 else False
    _arch_matches_cache[(arch, wildcard)] = ret
    return ret


def parse_dose_yaml(yamlin):
    missing = defaultdict(lambda: defaultdict(set))
    conflict = defaultdict(lambda: defaultdict(set))
    data = {"bin": defaultdict(set), "src": defaultdict(set)}

    if yamlin.get('report') is None:
        return ({}, {}, {"bin": {}, "src": {}})

    for p in yamlin['report']:
        n, v = p['package'], p['version']
        # only add name to avoid duplicates when more than one
        # version exists
        if p['status'] == 'broken':
            for r in p['reasons']:
                if r.get('missing'):
                    unsatdep = (r['missing']['pkg']['unsat-dependency']
                                .split(' ', 1)[0], None)
                    c = r['missing'].get('depchains')
                    if c:
                        # normalize the order of depchains
                        depchains = tuple(sorted(set(
                            [tuple([("", None)] + [(p['package'], p['version'])
                             for p in depchain['depchain'][1:]] + [unsatdep])
                             for depchain in c])))
                    else:
                        depchains = tuple([(("", None), (unsatdep))])
                    missing[unsatdep][depchains].add((n, v))
                    data['bin'][r['missing']['pkg']['unsat-dependency']
                                .split(' ', 1)[0].split(':',
                                                        1)[1]].add('missing')
                    data['src'][n.split(':', 1)[1]].add('missing')
                if r.get('conflict'):
                    pkg1 = (r['conflict']['pkg1']['package'],
                            r['conflict']['pkg1']['version'])
                    pkg2 = (r['conflict']['pkg2']['package'],
                            r['conflict']['pkg2']['version'])
                    c1 = r['conflict'].get('depchain1')
                    if c1:
                        depchain1 = tuple(sorted(set(
                            [tuple([("", None)] + [(p['package'], p['version'])
                             for p in depchain['depchain'][1:]] + [pkg1])
                             for depchain in c1])))
                    else:
                        depchain1 = tuple([(("", None), pkg1)])
                    c2 = r['conflict'].get('depchain2')
                    if c2:
                        depchain2 = tuple(sorted(set(
                            [tuple([("", None)] + [(p['package'], p['version'])
                             for p in depchain['depchain'][1:]] + [pkg2])
                             for depchain in c2])))
                    else:
                        depchain2 = tuple([(("", None), pkg2)])
                    # a conflict with depchains [A, B] is equal to a conflict
                    # with depchains [B, A], so normalize the order
                    if depchain1 is None and depchain2 is None:
                        depchains = (None, None)
                    elif depchain1 is None:
                        depchains = (depchain2, None)
                    elif depchain2 is None:
                        depchains = (depchain1, None)
                    else:
                        depchains = tuple(sorted([depchain1, depchain2]))
                    # conflict A<->B is equal to a conflict B<->A, so normalize
                    # the order
                    conflict[tuple(sorted([pkg1, pkg2]))][depchains].add((n,
                                                                          v))
                    data['bin'][r['conflict']['pkg1'][
                        'package'].split(':', 1)[1]].add('conflict')
                    data['bin'][r['conflict']['pkg2'][
                        'package'].split(':', 1)[1]].add('conflict')
                    data['src'][n.split(':', 1)[1]].add('conflict')
    return (missing, conflict, data)


def human_readable_size(val):
    for unit in ['', 'KiB', 'MiB']:
        if val < 1024:
            return "%.1f %s" % (val, unit)
        val /= 1024.0
    return "%.1f GiB" % val
