#! /usr/bin/env python
# This file is part of curtin. See LICENSE file for copyright and license info.

import os
import re
import subprocess
import signal
import threading
from unittest import TestLoader

from curtin.util import get_platform_arch


class Command(object):
    """
    based on https://gist.github.com/kirpit/1306188
    """
    command = None
    process = None
    status = None
    exception = None
    returncode = -1

    def __init__(self, command, signal=signal.SIGTERM):
        self.command = command
        self.signal = signal

    def run(self, timeout=None, **kwargs):
        """ Run a command then return: (status, output, error). """
        def target(**kwargs):
            try:
                self.process = subprocess.Popen(self.command, **kwargs)
                self.process.communicate()
                self.status = self.process.returncode
            except subprocess.CalledProcessError as e:
                self.exception = e
                self.returncode = e.returncode
            except Exception as e:
                self.exception = e
        # thread
        thread = threading.Thread(target=target, kwargs=kwargs)
        thread.start()
        thread.join(timeout)
        if thread.is_alive():
            self.process.send_signal(self.signal)
            thread.join()
            self.exception = TimeoutExpired(
                cmd=self.command, timeout=timeout)

        if self.exception:
            raise self.exception

        if self.status != 0:
            raise subprocess.CalledProcessError(cmd=self.command,
                                                returncode=self.status)

        return 0


try:
    TimeoutExpired = subprocess.TimeoutExpired
except AttributeError:
    class TimeoutExpired(subprocess.CalledProcessError):
        def __init__(self, *args, **kwargs):
            if not kwargs:
                kwargs = {}
            if len(args):
                # if args are given, convert them to kwargs.
                # *args is a tuple, convert it to a list to use pop
                args = list(args)
                for arg in ('cmd', 'output', 'timeout'):
                    kwargs[arg] = args.pop(0)
                    if not len(args):
                        break

            returncode = -1
            if 'timeout' in kwargs:
                self.timeout = kwargs.pop('timeout')
            else:
                self.timeout = -1

            # do not use super here as it confuses pylint
            # https://github.com/PyCQA/pylint/issues/773
            subprocess.CalledProcessError.__init__(self, returncode, **kwargs)


def check_call(cmd, signal=signal.SIGTERM, **kwargs):
    # provide a 'check_call' like interface, but kill with a nice signal
    return Command(cmd, signal).run(**kwargs)


def find_testcases_by_attr(**kwargs):
    class_match = set()
    for test_case in find_testcases(**kwargs):
        tc_name = str(test_case.__class__)
        full_path = tc_name.split("'")[1].split(".")
        class_name = full_path[-1]
        if class_name in class_match:
            continue
        class_match.add(class_name)
        filename = "/".join(full_path[0:-1]) + ".py"
        yield "%s:%s" % (filename, class_name)


def _attr_match(pattern, value):
    if not value:
        return False
    return re.match(pattern, str(value))


def find_testcases(**kwargs):
    # Use the TestLoder to load all test cases defined within tests/vmtests/
    # and figure out what distros and releases they are testing. Any tests
    # which are disabled will be excluded.
    loader = TestLoader()
    # dir with the vmtest modules (i.e. tests/vmtests/)
    tests_dir = os.path.dirname(__file__)
    # The root_dir for the curtin branch. (i.e. curtin/)
    root_dir = os.path.split(os.path.split(tests_dir)[0])[0]
    # Find all test modules defined in curtin/tests/vmtests/
    module_test_suites = loader.discover(tests_dir, top_level_dir=root_dir)
    filter_attrs = [attr for attr, value in kwargs.items() if value]
    for mts in module_test_suites:
        for class_test_suite in mts:
            for test_case in class_test_suite:
                # skip disabled tests
                if not getattr(test_case, '__test__', False):
                    continue
                # compare each filter attr with the specified value
                tcmatch = [not _attr_match(kwargs[attr],
                                           getattr(test_case, attr, False))
                           for attr in filter_attrs]
                if any(tcmatch):
                    continue
                yield test_case


def find_arches():
    """
    Return a list of uniq target arch values from test cases
    """
    arch = get_platform_arch()
    target_arches = []
    for test_case in find_testcases():
        if arch in getattr(test_case, 'arch_skip', None):
            continue
        target_arch = getattr(test_case, 'target_arch', None)
        if target_arch and target_arch not in target_arches:
            target_arches.append(target_arch)
    return target_arches


def find_releases_by_distro():
    """
    Returns a dictionary of distros and the distro releases that will be tested

    distros:
        ubuntu:
            releases: []
            krels: []
        centos:
            releases: []
            krels: []
    """
    # find all distros and releases tested for each distro
    releases = []
    krels = []
    rel_by_dist = {}
    for test_case in find_testcases():
        for (dist, rel, krel) in (
                (getattr(test_case, a, None) for a in attrs)
                for attrs in (('distro', 'release', 'krel'),
                              ('target_distro', 'target_release',
                               'krel'))):

            if dist and rel:
                distro = rel_by_dist.get(dist, {'releases': [],
                                                'krels': []})
                releases = distro.get('releases')
                krels = distro.get('krels')
                if rel not in releases:
                    releases.append(rel)
                if krel and krel not in krels:
                    krels.append(krel)
                rel_by_dist.update({dist: distro})

    return rel_by_dist


def _parse_ip_a(ip_a):
    """
    2: interface0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1480 qdisc pfifo_fast\
        state UP group default qlen 1000
        link/ether 52:54:00:12:34:00 brd ff:ff:ff:ff:ff:ff
        inet 192.168.1.2/24 brd 192.168.1.255 scope global interface0
            valid_lft forever preferred_lft forever
        inet6 2001:4800:78ff:1b:be76:4eff:fe06:1000/64 scope global
            valid_lft forever preferred_lft forever
        inet6 fe80::5054:ff:fe12:3400/64 scope link
        valid_lft forever preferred_lft forever
    """
    ifaces = {}
    combined_fields = {
        'brd': 'broadcast',
        'link/ether': 'mac_address',
    }
    interface_fields = [
        'group',
        'master',
        'mtu',
        'qdisc',
        'qlen',
        'state',
    ]
    inet_fields = [
        'valid_lft',
        'preferred_left'
    ]
    boolmap = {
        'BROADCAST': 'broadcast',
        'LOOPBACK': 'loopback',
        'LOWER_UP': 'lower_up',
        'MULTICAST': 'multicast',
        'RUNNING': 'running',
        'UP': 'up',
    }

    for line in ip_a.splitlines():
        if not line:
            continue

        toks = line.split()
        if not line.startswith("    "):
            cur_iface = line.split()[1].rstrip(":")
            cur_data = {
                'inet4': [],
                'inet6': [],
                'interface': cur_iface
            }
            # vlan's get a fancy name <iface name>@<vlan_link>
            if '@' in cur_iface:
                cur_iface, vlan_link = cur_iface.split("@")
                cur_data.update({'interface': cur_iface,
                                 'vlan_link': vlan_link})
            for t in boolmap.values():
                # <BROADCAST,MULTICAST,UP,LOWER_UP>
                cur_data[t] = t.upper() in line[2]
            ifaces[cur_iface] = cur_data

        for i in range(0, len(toks)):
            cur_tok = toks[i]
            try:
                next_tok = toks[i+1]
            except IndexError:
                next_tok = None

            # parse link/ether, brd
            if cur_tok in combined_fields.keys():
                cur_data[combined_fields[cur_tok]] = next_tok
            # mtu an other interface line key/value pairs
            elif cur_tok in interface_fields:
                cur_data[cur_tok] = next_tok
            elif cur_tok.startswith("inet"):
                cidr = toks[1]
                address = cidr
                prefixlen = None
                if '/' in cidr:
                    address, prefixlen = cidr.split("/")
                cur_ip = {
                    'address': address,
                    'prefixlen': prefixlen,
                }
                if ":" in address:
                    cur_ipv6 = cur_ip.copy()
                    cur_ipv6.update({'scope': toks[3]})
                    cur_data['inet6'].append(cur_ipv6)
                else:
                    cur_ipv4 = cur_ip.copy()
                    if len(toks) > 5:
                        cur_ipv4.update({'scope': toks[5]})
                    else:
                        cur_ipv4.update({'scope': toks[3]})
                    cur_data['inet4'].append(cur_ipv4)

                continue
            elif cur_tok in inet_fields:
                if ":" in address:
                    cur_ipv6[cur_tok] = next_tok
                else:
                    cur_ipv4[cur_tok] = next_tok
                continue

    return ifaces


def ip_a_to_dict(ip_a):
    # return a dictionary of network information like:
    # {'interface0': {'broadcast': '10.0.2.255',
    #                 'group': 'default',
    #                 'inet4': [{'address': '10.0.2.15',
    #                            'preferred_lft': 'forever',
    #                            'prefixlen': '24',
    #                            'scope': 'global',
    #                            'valid_lft': 'forever'}],
    #                 'inet6': [{'address': 'fe80::5054:ff:fe12:3400',
    #                            'preferred_lft': 'forever',
    #                            'prefixlen': '64',
    #                            'scope': 'link',
    #                            'valid_lft': 'forever'}],
    #                 'interface': 'interface0',
    #                 'loopback': False,
    #                 'lower_up': False,
    #                 'mac_address': '52:54:00:12:34:00',
    #                 'mtu': '1500',
    #                 'multicast': False,
    #                 'qdisc': 'pfifo_fast',
    #                 'qlen': '1000',
    #                 'running': False,
    #                 'state': 'UP',
    #                 'up': False},
    # from iproute2 `ip a` command output
    return _parse_ip_a(ip_a)

# vi: ts=4 expandtab syntax=python
