import os
import re
from . import files
from .tools import Cachable, GenericFS, unique
from time import sleep


def get_uuid_dict(reverse=False, devices=()):
    '''Получить словарь со значениями UUID блочных устройств.'''
    blkid_process = files.Process('/sbin/blkid', '-s', 'UUID',
                                  '-c', '/dev/null', *devices)
    re_split = re.compile('^([^:]+):.*UUID="([^"]+)"', re.S)

    output_items = {}
    search_results = []
    lines = blkid_process.read_lines()
    for line in lines:
        search_result = re_split.search(line)
        if search_result:
            search_results.append(search_result.groups())

    output_items = (("UUID={}".format(uuid), udev.get_devname(name=dev,
                                                              fallback=dev))
                    for dev, uuid in search_results)
    if reverse:
        return {v: k for k, v in output_items}
    else:
        return {k: v for k, v in output_items}


def find_device_by_partition(device):
    '''Найти устройство, к которому относится данный раздел.'''
    info = udev.get_device_info(name=device)
    if info.get('DEVTYPE', '') != 'partition':
        return ''
    parent_path = os.path.dirname(info.get('DEVPATH', ''))
    if parent_path:
        device_info = udev.get_device_info(path=parent_path)
        return device_info.get('DEVNAME', '')
    return False


def count_partitions(device_name):
    '''Посчитать количество разделов данного устройства.'''
    syspath = udev.get_device_info(name=device_name).get('DEVPATH', '')
    if not syspath:
        return 0
    device_name = os.path.basename(syspath)
    return len([x for x in sysfs.listdir(syspath)
                if x.startswith(device_name)])


def get_lspci_output(filter_name=None, short_info=False):
    '''Функция для чтения вывода lspci. Возвращает словарь с идентификаторами
    устройств и информацией о них.'''
    re_data = re.compile(r'(\S+)\s"([^"]+)"\s+"([^"]+)"\s+"([^"]+)"', re.S)
    lspci_column_names = ('type', 'vendor', 'name')
    if filter_name:
        if callable(filter_name):
            filter_function = filter_name
        else:
            def filter_function(input):
                return filter_name in input
    else:
        def filter_function(input):
            return input
    lspci_path = files.check_utils('/usr/sbin/lspci')
    lspci_output = files.Process(lspci_path, '-m').read_lines()
    output = {}
    lspci_output = list(filter(filter_function, lspci_output))
    for line in lspci_output:
        search_result = re_data.search(line)
        if search_result:
            search_result = search_result.groups()
            device_id = search_result[0]
            output[device_id] = {key: value for key, value in zip(
                                                            lspci_column_names,
                                                            search_result[1:])}
    return output


class LvmCommand:
    '''Класс для работы с командой lvm и выполнения с ее помощью различных
    действий.'''
    @property
    def lvm_command(self):
        '''Возвращает кешированный путь к lvm.'''
        return files.get_program_path('/sbin/lvm')

    def get_physical_extent_size(self):
        if not self.lvm_command:
            return ''
        pv_data = files.Process(self.lvm_command, 'lvmconfig', '--type',
                                'full', 'allocation/physical_extent_size')
        if pv_data.success():
            return pv_data.read()
        return ''

    def get_pvdisplay_output(self, option):
        '''Получить вывод pv_display c указанной option.'''
        if not self.lvm_command:
            return ''
        pv_data = files.Process(self.lvm_command, 'pvdisplay', '-C', '-o',
                                option, '--noh')
        if pv_data.success():
            return pv_data.read()
        return False

    def vg_scan(self):
        '''Найти существующие в системе группы томов.'''
        if self.lvm_command:
            return files.Process(self.lvm_command, 'vgscan').success()
        return False

    def vg_change(self):
        '''Изменить атрибуты группы томов.'''
        if self.lvm_command:
            failed = files.Process('vgchange', '-ay').failed()
            failed |= files.Process('vgchange', '--refresh').failed()
            return not failed
        return False

    def lv_change(self, groups):
        '''Изменить атрибуты логического тома.'''
        if self.lvm_command:
            failed = False
            for group in groups:
                failed |= files.Process('lvchange', '-ay', group).failed()
                failed |= files.Process('lvchange', '--refresh',
                                        group).failed()
                return failed
        return False

    def execute(self, *command):
        '''Выполнить указанную LVM команду.'''
        if self.lvm_command:
            return files.Process(self.lvm_command, *command).success()
        return False

    def double_execute(self, *command):
        '''Выполнить дважды указанную LVM команду.'''
        if not self.execute(*command):
            sleep(2)
            return self.execute(*command)
        return False

    def remove_lv(self, vg, lv):
        '''Удалить указанный логических томов из системы.'''
        return self.double_execute('lvremove', '{0}/{1}'.format(vg, lv), '-f')

    def remove_vg(self, vg):
        '''Удалить указанную группу томов из системы.'''
        return self.double_execute('vgremove', vg, '-f')

    def remove_pv(self, pv):
        '''Удалить LVM метку с физического тома.'''
        return self.double_execute('pvremove', pv, '-ffy')


class Lvm(Cachable):
    '''Класс для получения информации о lvm.'''
    def __init__(self, lvm_command):
        super().__init__()
        self.lvm_command = lvm_command

    def get_pvdisplay_full(self):
        '''Получить полный вывод команды pvdisplay.'''
        get_pvdisplay_output = self.get_pvdisplay_output(
                                                      'vg_name,lv_name,pv_name'
                                                  ).strip()
        if get_pvdisplay_output:
            output = (line.split() for line in
                      get_pvdisplay_output.split('\n'))
            return [tuple(y.strip() for y in x)
                    for x in output if x and len(x) == 3]

    @property
    def get_volume_groups(self):
        '''Получить имеющиеся в системе группы томов.'''
        return sorted({vg for vg, lv, pv in self.get_pvdisplay_full()})

    @Cachable.method_cached()
    def get_physical_extent_size(self):
        '''Получить размер физического диапазона.'''
        return self.lvm_command.get_physical_extent_size()

    @Cachable.method_cached()
    def get_pvdisplay_output(self, option):
        '''Получить с помощью pvdisplay информацию .'''
        return self.lvm_command.get_pvdisplay_output(option)

    def get_used_partitions(self, vg_condition, lv_condition):
        '''Получить испоьзуемые разделы.'''
        return list(sorted({part for vg, lv, part in self.get_pvdisplay_full()
                            if vg == vg_condition and lv == lv_condition}))

    def refresh(self):
        if not os.environ.get('EBUILD_PHASE', False):
            self.lvm_command.vg_scan()
            self.lvm_command.vg_change()
            self.lvm_command.lv_change(lvm)

    def remove_lv(self, vg, lv):
        return self.lvm_command.remove_lv(vg, lv)

    def remove_vg(self, vg):
        return self.lvm_command.remove_vg(vg)

    def remove_pv(self, pv):
        return self.lvm_command.remove_pv(pv)


class MdadmCommand:
    '''Класс для работы с командой mdadm.'''
    @property
    def mdadm_command(self):
        '''Возвращает кешированный путь к mdamd.'''
        return files.get_program_path('/sbin/mdadm')

    def stop_raid(self, raid_device):
        '''Остановить устройство из RAID-массива.'''
        if not self.mdadm_command:
            return False
        return files.Process(self.mdadm_command, '-S', raid_device).success()

    def zero_superblock(self, device):
        '''Затереть superblock для составляющей RAID-массива.'''
        if not self.mdadm_command:
            return False
        return files.Process(self.mdadm_command, '--zero_superblock',
                             device).success()


class RAID:
    '''Класс для работы с RAID-массивами.'''
    def __init__(self, mdadm_command):
        self.mdadm_command = mdadm_command

    def get_devices_info(self, raid_device):
        '''Получить информацию о RAID-массиве.'''
        device = udev.get_device_info(path=raid_device)
        if udev.is_raid(device):
            for raid_block in sysfs.glob(raid_device, 'md/dev-*', 'block'):
                yield udev.get_device_info(sysfs.join_path(raid_block))

    def get_devices(self, raid_device, path_name='DEVPATH'):
        '''Получить устройства /dev, из которых состоит RAID-массив.
        Не возвращает список этих устройств для раздела сделанного для RAID
        (/dev/md0p1).'''
        for device_info in self.get_devices_info(raid_device):
            device_name = device_info.get(path_name, '')
            if device_name:
                yield device_name

    def get_devices_syspath(self, raid_device):
        '''Получить sysfs пути устройств, из которых состоит RAID-массив.'''
        for device in self.get_devices(raid_device, path_name='DEVPATH'):
            yield device

    def remove_raid(self, raid_name):
        '''Удалить RAID-массив.'''
        raid_parts = list(self.get_devices(udev.get_syspath(name=raid_name)))
        failed = False
        failed |= not (self.mdadm_command.stop_raid(raid_name) or
                       self.mdadm_command.stop_raid(raid_name))
        for device in raid_parts:
            failed |= not (self.mdadm_command.zero_superblock(device) or
                           self.mdadm_command.zero_superblock(device))
        return not failed


class DeviceFS(GenericFS):
    '''Базовый класс для классов предназначенных для работы с sysfs и /dev'''
    def __init__(self, filesystem=None):
        if isinstance(filesystem, GenericFS):
            self.filesystem = filesystem
        else:
            self.filesystem = files.RealFS('/')

    def join_path(self, *paths):
        output = os.path.join('/', *(_path[1:] if _path.startswith('/')
                              else _path for _path in paths))
        return output

    def exists(self, *paths):
        return self.filesystem.exists(self.join_path(*paths))

    def read(self, *paths):
        return self.filesystem.read(self.join_path(*paths))

    def realpath(self, *paths):
        return self.filesystem.realpath(self.join_path(*paths))

    def write(self, *args):
        data = args[-1]
        paths = args[:-1]
        self.filesystem.write(self.join_path(*paths), data)

    def listdir(self, *paths, full_path=False):
        return self.filesystem.listdir(self.join_path(*paths),
                                       full_path=full_path)

    def glob(self, *paths):
        for file_path in self.filesystem.glob(self.join_path(*paths)):
            yield file_path


class SysFS(DeviceFS):
    '''Класс для работы с sysfs.'''
    BASE_DIRECTORY = '/sys'
    PATH = {'firmware': 'firmware',
            'efi': 'firmware/efi',
            'efivars': 'firmware/efi/efivars',
            'classnet': 'class/net',
            'input': 'class/input',
            'block': 'block',
            'dmi': 'class/dmi/id',
            'module': 'module',
            'block_scheduler': 'queue/scheduler'}

    def join_path(self, *paths):
        output = os.path.join('/', *(_path[1:] if _path.startswith('/')
                                     else _path for _path in paths))
        if output.startswith(self.BASE_DIRECTORY):
            return output
        else:
            return os.path.join(self.BASE_DIRECTORY, output[1:])

    def glob(self, *paths):
        for _path in self.filesystem.glob(self.join_path(*paths)):
            yield _path[len(self.BASE_DIRECTORY):]


class DevFS(DeviceFS):
    '''Класс для работы с /dev.'''
    BASE_DIRECTORY = '/dev'

    def join_path(self, *paths):
        output = os.path.join('/', *(_path[1:] if _path.startswith('/')
                                     else _path for _path in paths))
        if output.startswith(self.BASE_DIRECTORY):
            return output
        else:
            return os.path.join(self.BASE_DIRECTORY, output[1:])

    def glob(self, *paths):
        for _path in self.filesystem.glob(self.join_path(*paths)):
            yield _path[len(self.BASE_DIRECTORY):]


class UdevAdmNull:
    def info_property(self, path=None, name=None):
        return {}

    def info_export(self):
        return ''

    def settle(self, timeout=15):
        pass

    def trigger(self, subsystem=None):
        pass

    @property
    def broken(self):
        return False


class UdevAdmCommand(UdevAdmNull):
    def __init__(self):
        self.first_run = True

    @property
    def udevadm_cmd(self):
        return files.get_program_path('/sbin/udevadm')

    @property
    def broken(self):
        return not bool(self.udevadm_cmd)

    def info_property(self, path=None, name=None):
        if self.first_run:
            self.trigger("block")
            self.settle()
            self.first_run = False

        if name is not None:
            type_query = "--name"
            value = name
        else:
            type_query = "--path"
            value = path
        udevadm_output = files.Process(self.udevadm_cmd, "info",
                                       "--query", "property",
                                       type_query, value).read_lines()
        output_items = []
        for line in udevadm_output:
            if '=' in line:
                output_items.append(line.partition('=')[0::2])
        return dict(output_items)

    def info_export(self):
        return files.Process(self.udevadm_cmd, 'info', '-e').read().strip()

    def trigger(self, subsystem=None):
        if subsystem:
            files.Process(self.udevadm_cmd, 'trigger', '--subsystem-match',
                          subsystem).success()
        else:
            files.Process(self.udevadm_cmd, 'trigger').success()

    def settle(self, timeout=15):
        files.Process(self.udevadm_cmd, 'settle', '--timeout={}'.
                      format(timeout)).success()


class Udev:
    '''Класс возвращающий преобразованную или кэшированную информацию о системе
    из udev.'''
    def __init__(self, udevadm=None):
        self.path_cache = {}
        self.name_cache = {}
        if isinstance(udevadm, UdevAdmCommand):
            self.udevadm = udevadm
        else:
            self.udevadm = UdevAdmCommand()
        if self.udevadm.broken:
            self.udevadm = UdevAdmNull()

    def clear_cache(self):
        self.path_cache = {}
        self.name_cache = {}
        self.udevadm = UdevAdmCommand()

    def get_device_info(self, path=None, name=None):
        if name is not None:
            cache = self.name_cache
            value = devfs.realpath(name)
            name = value
        else:
            cache = self.path_cache
            value = sysfs.realpath(path)
            path = value

        if value not in cache:
            data = self.udevadm.info_property(path, name)
            devname = data.get('DEVNAME', '')
            devpath = data.get('DEVPATH', '')
            if devname:
                self.name_cache[devname] = data
            if devpath:
                devpath = '/sys{}'.format(devpath)
                self.path_cache[devname] = data
            return data
        else:
            return cache[value]

    def get_block_devices(self):
        for block in sysfs.glob(sysfs.PATH['block'], '*'):
            yield block
            blockname = os.path.basename(block)
            for part in sysfs.glob(block, '{}*'.format(blockname)):
                yield part

    def syspath_to_devname(self, devices, drop_empty=True):
        for device_path in devices:
            info = self.get_device_info(path=device_path)
            if 'DEVNAME' in info:
                yield info['DEVNAME']
            elif not drop_empty:
                yield ''

    def devname_to_syspath(self, devices, drop_empty=True):
        for device_name in devices:
            info = self.get_device_info(name=device_name)
            if 'DEVPATH' in info:
                yield info['DEVPATH']
            elif not drop_empty:
                yield ''

    def get_devname(self, path=None, name=None, fallback=None):
        if fallback is None:
            if name:
                fallback = name
            else:
                fallback = ''
        return self.get_device_info(path, name).get('DEVNAME', fallback)

    def get_syspath(self, path=None, name=None, fallback=None):
        if fallback is None:
            if path:
                fallback = path
            else:
                fallback = ''
        return self.get_device_info(path, name).get('DEVPATH', fallback)

    def refresh(self, trigger_only=False):
        if not trigger_only:
            self.clear_cache()
        files.quite_unlink('/etc/blkid.tab')
        if not os.environ.get('EBUILD_PHASE'):
            self.udevadm.trigger(subsystem='block')
            self.udevadm.settle(15)

    def is_cdrom(self, udev_data):
        return udev_data.get('ID_CDROM', '') == '1'

    def is_device(self, udev_data):
        if 'DEVPATH' not in udev_data:
            return False
        return (sysfs.exists(udev_data.get('DEVPATH', ''), 'device') and
                udev_data.get('DEVTYPE', '') == 'disk')

    def is_raid(self, udev_data):
        return (udev_data.get('MD_LEVEL', '').startswith('raid') and
                udev_data.get('DEVTYPE', '') == 'disk')

    def is_raid_partition(self, udev_data):
        return (udev_data.get('MD_LEVEL', '').startswith('raid') and
                udev_data.get('DEVTYPE', '') == 'partition')

    def is_lvm(self, udev_data):
        return 'DM_LV_NAME' in udev_data and 'DM_VG_NAME' in udev_data

    def is_partition(self, udev_data):
        return udev_data.get('DEVTYPE', '') == 'partition'

    def is_block_device(self, udev_data):
        return udev_data.get('SUBSYSTEM', '') == 'block'

    def get_device_type(self, path=None, name=None):
        info = self.get_device_info(path, name)
        syspath = info.get('DEVPATH', '')
        if self.is_cdrom(info):
            return 'cdrom'
        if self.is_device(info):
            return 'disk'
        if self.is_partition(info):
            return '{}-partition'.format(
                                self.get_device_type(os.path.dirname(syspath)))
        if self.is_raid(info):
            for raid_device in raid.get_devices_syspath(syspath):
                return '{}-{}'.format(self.get_device_type(raid_device),
                                      info['MD_LEVEL'])
            else:
                return 'loop'
        if self.is_lvm(info):
            for lv_device in lvm.get_used_partitions(info['DM_VG_NAME'],
                                                     info['DM_LV_NAME']):
                return '{}-lvm'.format(self.get_device_type(name=lv_device))
        if self.is_block_device(info):
            return 'loop'
        return ''

    def get_partition_type(self, path=None, name=None):
        info = self.get_device_info(path, name)
        if info.get('ID_PART_ENTRY_SCHEME') == 'dos':
            part_id = info.get('ID_PART_ENTRY_TYPE', '')
            part_number = info.get('ID_PART_ENTRY_NUMBER', '')
            if part_id and part_number:
                if part_id == '0x5':
                    return 'extended'
                elif int(part_number) > 4:
                    return 'logical'
                else:
                    return 'primary'
        return info.get('ID_PART_TABLE_TYPE', '')

    def _get_disk_devices(self, path=None, name=None):
        info = udev.get_device_info(path=path, name=name)
        syspath = info.get('DEVPATH', '')
        if syspath:
            # real device
            if self.is_device(info):
                yield True, info.get('DEVNAME', '')
            # partition
            elif self.is_partition(info):
                for device in self._get_disk_devices(
                                                path=os.path.dirname(syspath)):
                    yield device
            # md raid
            elif udev.is_raid(info):
                yield False, info.get('DEVNAME', '')
                for raid_device in sorted(raid.get_devices_syspath(syspath)):
                    for device in self._get_disk_devices(path=raid_device):
                        yield device
            # lvm
            elif udev.is_lvm(info):
                yield False, info.get('DEVNAME', '')
                for lv_device in lvm.get_used_partitions(info['DM_VG_NAME'],
                                                         info['DM_LV_NAME']):
                    for device in self._get_disk_devices(name=lv_device):
                        yield device

    def get_disk_devices(self, path=None, name=None):
        return sorted({
            device for real_device, device in
            self._get_disk_devices(path, name) if real_device
                      })

    def get_all_base_devices(self, path=None, name=None):
        try:
            devices = (device for real_device, device in
                       self._get_disk_devices(path, name)
                       if not device.startswith('/dev/loop'))
            if not self.is_partition(self.get_device_info(path, name)):
                next(devices)
            return list(unique(devices))
        except StopIteration:
            return []


sysfs = SysFS()
devfs = DevFS()

udev = Udev(UdevAdmCommand())
lvm = Lvm(LvmCommand())
raid = RAID(MdadmCommand())
