# -*- coding: utf-8 -*-

# Copyright 2017 Mir Calculate. http://www.calculate-linux.org
#
#  Licensed under the Apache License, Version 2.0 (the "License");
#  you may not use this file except in compliance with the License.
#  You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
#  Unless required by applicable law or agreed to in writing, software
#  distributed under the License is distributed on an "AS IS" BASIS,
#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#  See the License for the specific language governing permissions and
#  limitations under the License.
import sys
from abc import ABCMeta, abstractmethod
from .files import process, getProgPath, getRunCommands
from .tools import Sizes, classificate, traverse
from .device import humanreadableSize
import re
import os

_ = lambda x: x
from ..cl_lang import setLocalTranslate

setLocalTranslate('cl_lib3', sys.modules[__name__])

def round_disk_size(size):
    return size - (size % (4 * Sizes.M))

class VolumesError(Exception):
    pass

class VolumesBuilder(metaclass=ABCMeta):

    class DeviceType():
        Device = 0
        RAID = 1

    class VolumeType():
        Generic = 0
        Raid = 2
        Fat32 = 3
        Linux = 4
        LinuxSwap = 5
        Ntfs = 6
        BiosBoot = 7
        Efi = 8
        Lvm = 9
        Extended = 10

    class Purpose():
        Undefined = 100
        EfiBoot = 101
        BiosBoot = 102
        MainRoot = 103
        UpdateRoot = 104
        Swap = 105
        Calculate = 106
        Lvm = 107
        Windows = 108
        Raid = 109
        Linux = 110

    purpose_type = {
        Purpose.Undefined: VolumeType.Generic,
        Purpose.Lvm: VolumeType.Lvm,
        Purpose.BiosBoot: VolumeType.BiosBoot,
        Purpose.EfiBoot: VolumeType.Efi,
        Purpose.MainRoot: VolumeType.Linux,
        Purpose.UpdateRoot: VolumeType.Linux,
        Purpose.Calculate: VolumeType.Linux,
        Purpose.Linux: VolumeType.Linux,
        Purpose.Swap: VolumeType.LinuxSwap,
        Purpose.Windows: VolumeType.Ntfs,
        Purpose.Raid: VolumeType.Raid
    }

    SizeAllFree = "allfree"

    @abstractmethod
    def create(self):
        """
        Пересоздать таблицу разделов
        """
        pass

    @abstractmethod
    def create_volume(self, size, name, purpose=0):
        pass

    @abstractmethod
    def create_last_volume(self, size, name, purpose=0):
        pass

    @abstractmethod
    def set_minimal_volume_size(self, size):
        pass

    @abstractmethod
    def finish(self):
        pass


class DosDisk(metaclass=ABCMeta):

    @abstractmethod
    def get_partition_name(self, num):
        pass

    @abstractmethod
    def create_table(self):
        pass

    @abstractmethod
    def create_primary_partition(self, size):
        pass

    @abstractmethod
    def create_logical_partition(self, size):
        pass

    @abstractmethod
    def create_extended_partition(self):
        pass

    @abstractmethod
    def change_partition_type(self, partid, partnum=None):
        pass

    @abstractmethod
    def write(self):
        pass


class GptDisk(metaclass=ABCMeta):

    @abstractmethod
    def create_table(self):
        pass

    @abstractmethod
    def create_partition(self, size):
        pass

    @abstractmethod
    def change_partition_type(self, partid, partnum=None):
        pass

    @abstractmethod
    def write(self):
        pass

    @abstractmethod
    def get_partition_name(self, num):
        pass


class LvmDisk(metaclass=ABCMeta):

    @abstractmethod
    def create_lvm(self):
        pass

    @abstractmethod
    def create_logical_volume(self, name, size):
        pass

    @abstractmethod
    def write(self):
        pass


class BlockDisk():
    pass

class VirtualDiskError(Exception):
    pass

class SizeableDisk():
    AllFree = None
    header_size = 0

    def __init__(self, size):
        self.disk_size = round_disk_size(size)
        self.free_size = self.disk_size - self.header_size

    def occupe_size(self, size):
        if size is not self.AllFree:
            if size < 0:
                self.free_size = size
            else:
                self.free_size -= size
        else:
            self.free_size = 0

    def size(self, size):
        if size is not self.AllFree:
            if size < 0:
                return self.free_size + size
            else:
                return size
        else:
            return self.free_size

    def check_free_size(self):
        if self.free_size < 0:
            needbytes = abs(self.free_size)
            availbytes = self.disk_size
            all_usebytes = availbytes + needbytes
            all_use = humanreadableSize(all_usebytes)
            raise VirtualDiskError(
                _("There is not enough space on this device, "
                  "to perform the autopartition need {need}").format(
                    need=all_use))


class VirtualDisk(SizeableDisk, DosDisk, GptDisk):
    parttype = ""

    class Partition():
        def __init__(self, dev=None, mount=None, part=None,
                     partid=None, size=None):
            self.dev = dev
            self.mount = mount
            self.part = part
            self.partid = partid
            self.size = size
            self.format = ""

    def __init__(self, parent, dev, size):
        self.parent = parent
        self.dev = dev
        self.partitions = []
        super().__init__(size)

    volume_types_map = {}

    def get_partition_name(self, num):
        if self.dev[-1].isdigit():
            return ("%sp%%d" % self.dev) % num
        else:
            return ("%s%%d" % self.dev) % num

    def create_table(self):
        self.partitions = []
        return True

    def create_partition(self, size, parttype=None):
        real_size = self.size(size)
        self.occupe_size(real_size)
        self.partitions.append(
            self.Partition(
                dev=self.get_partition_name(len(self.partitions)+1),
                mount="", part=parttype or self.parttype, size=real_size)
        )
        return True

    def create_primary_partition(self, size):
        self.create_partition(size, parttype="primary")
        return True

    def create_logical_partition(self, size):
        self.create_partition(size, parttype="logical")
        return True

    def create_extended_partition(self):
        self.create_partition(0, parttype="extended")
        self._get_partition().size = self.free_size
        self._get_partition().partid = DosPartitionId.get(
            VolumesBuilder.VolumeType.Extended)
        return True

    def _get_partition(self, partnum=None):
        if partnum is None:
            return self.partitions[-1]
        else:
            return self.partitions[partnum]

    def change_partition_type(self, partid, partnum=None):
        part = self._get_partition(partnum)
        part.partid = partid

    def change_purpose(self, purpose, partnum=None):
        part = self._get_partition(partnum)

        part.mount = self.parent.get_mount_point(purpose)
        if purpose == VolumesBuilder.Purpose.EfiBoot:
            self.parent.set_efi(part.dev)
        part.format = self.parent.get_format(purpose)

        if purpose is VolumesBuilder.Purpose.UpdateRoot:
            self.parent.dev_from = part.dev

    def write(self):
        self.check_free_size()
        devtype = self.parent.get_device_type(self.dev)
        for partition in self.partitions:
            disk_type = "%s-partition" % devtype
            if partition.partid in (
                GptPartitionId.get(VolumesBuilder.VolumeType.BiosBoot),
                DosPartitionId.get(VolumesBuilder.VolumeType.Extended),
                DosPartitionId.get(VolumesBuilder.VolumeType.Lvm),
                GptPartitionId.get(VolumesBuilder.VolumeType.Lvm),
            ):
                if partition.partid == GptPartitionId.get(
                        VolumesBuilder.VolumeType.BiosBoot):
                    self.parent.set_mbr(self.dev)
                elif partition.partid in (
                        DosPartitionId.get(VolumesBuilder.VolumeType.Lvm),
                        GptPartitionId.get(VolumesBuilder.VolumeType.Lvm)):
                    self.parent.lvm_size[partition.dev] = partition.size
                    self.parent.lvm_disk_type[partition.dev] = disk_type
                continue
            self.parent.disk_dev.append(partition.dev)
            self.parent.disk_mount.append(partition.mount)
            self.parent.disk_part.append(partition.part)
            self.parent.disk_size.append(partition.size)
            self.parent.disk_type.append(disk_type)
            if partition.format is None:
                self.parent.disk_format.append(self.parent.default_format)
            else:
                self.parent.disk_format.append(partition.format)
        return True

class VirtualDosDisk(VirtualDisk, DosDisk):
    parttype = "dos"
    header_size = Sizes().M

    def write(self):
        res = super().write()
        self.parent.set_mbr(self.dev)
        return res

class VirtualGptDisk(VirtualDisk, GptDisk):
    parttype = "gpt"
    header_size = Sizes().M + Sizes().Sector * 34

class VirtualLvm(SizeableDisk, LvmDisk):

    class Volume():
        def __init__(self, dev=None, mount=None, size=None):
            self.dev = dev
            self.mount = mount
            self.size = size
            self.format = ""

    def __init__(self, parent, devices, vgname, vgsize, pesize=None):
        self.parent = parent
        self.devices = devices
        self.disk_size = vgsize
        self.vgname = vgname
        self.volumes = []
        super().__init__(vgsize)

    volume_types_map = {}

    def get_partition_name(self, name):
        return "/dev/%s/%s" % (self.vgname, name)

    def create_lvm(self):
        self.volumes = []
        return True

    def create_logical_volume(self, lvname, size):
        real_size = self.size(size)
        self.occupe_size(real_size)
        self.volumes.append(
            self.Volume(
                dev=self.get_partition_name(lvname),
                mount="", size=real_size)
        )
        return True

    def _get_partition(self, name):
        if name is None:
            return self.volumes[-1]
        else:
            fullname = self.get_partition_name(name)
            for volume in self.volumes:
                if fullname == volume.dev:
                    return volume
            raise VolumesError(_("Failed to find %s volume") % name)

    def change_purpose(self, purpose, partnum=None):
        part = self._get_partition(partnum)

        part.mount = self.parent.get_mount_point(purpose)
        part.format = self.parent.get_format(purpose)

        if purpose is VolumesBuilder.Purpose.UpdateRoot:
            self.parent.dev_from = part.dev

    def write(self):
        self.check_free_size()
        devtype = self.parent.get_device_type(self.devices[0])
        for partition in self.volumes:
            self.parent.disk_dev.append(partition.dev)
            self.parent.disk_mount.append(partition.mount)
            self.parent.disk_part.append("")
            self.parent.disk_size.append(partition.size)
            self.parent.disk_type.append("%s-lvm" % devtype)
            self.parent.disk_format.append(partition.format)


class Fdisk(SizeableDisk, metaclass=ABCMeta):

    def __init__(self, dev, disk_size):
        super().__init__(disk_size)
        self.fdisk_process = process(self.fdisk_cmd, dev)
        self.fdisk = []
        self.dev = dev
        self.partnumber = 0

    volume_types_map = {}

    @abstractmethod
    def create_table(self):
        pass

    @property
    def fdisk_cmd(self):
        cmd = "/sbin/fdisk"
        _fdisk_cmd = getProgPath(cmd)
        if not _fdisk_cmd:
            raise VolumesError(_("Command not found '%s'") % cmd)
        return _fdisk_cmd

    def _fdisksize(self, size):
        if size is not self.AllFree:
            if size < 0:
                bytesize = self.free_size + size
            else:
                bytesize = size
            return "+%dK" % (Sizes().to_K(bytesize))
        else:
            return ""

    def change_partition_type(self, partid, partnum=None):
        if self.partnumber < 2:
            self.fdisk.append("t\n%s\n" % partid)
        else:
            if partnum is None:
                self.fdisk.append("t\n\n%s\n" % partid)
            else:
                self.fdisk.append("t\n%d\n%s\n" % (partnum, partid))

    def search_errors(self, errors):
        return re.search(r"Value out of range[.]|: unknown command",
                         errors)

    def write(self):
        self.fdisk_process.write("%sw\nq\n" % "".join(self.fdisk))
        self.fdisk_process.success()
        self.update_kernel_devfs()
        return not self.search_errors(self.fdisk_process.readerr().strip())

    def update_kernel_devfs(self):
        partprobe = getProgPath("/usr/sbin/partprobe")
        if partprobe:
            process(partprobe, self.dev).success()

    def get_partition_name(self, num):
        if self.dev[-1].isdigit():
            return ("%sp%%d" % self.dev) % num
        else:
            return ("%s%%d" % self.dev) % num


class DosFdisk(Fdisk, DosDisk):
    """
    Fdisk для создания dos таблицы разделов
    """
    header_size = Sizes().M

    def create_table(self):
        self.fdisk = []
        self.partnumber = 0
        self.fdisk.append("o\n")

    def create_primary_partition(self, size):
        end = self._fdisksize(size)
        self.occupe_size(size)
        self.fdisk.append("n\np\n\n\n%s\n" % end)
        self.partnumber += 1

    def create_logical_partition(self, size):
        end = self._fdisksize(size)
        self.occupe_size(size)
        self.fdisk.append("n\n\n%s\n" % end)
        self.partnumber += 1

    def create_extended_partition(self):
        self.fdisk.append("n\ne\n\n\n")
        self.partnumber += 1


class GptFdisk(Fdisk, GptDisk):
    """
    Fdisk для создания dos таблицы разделов
    """
    header_size = Sizes().M + Sizes().Sector * 34

    def create_table(self):
        self.fdisk = []
        self.fdisk.append("g\n")

    def create_partition(self, size):
        end = self._fdisksize(size)
        self.occupe_size(size)
        self.fdisk.append("n\n\n\n%s\n" % end)
        self.partnumber += 1


class LvmVolumeGroup(SizeableDisk, LvmDisk):

    def __init__(self, devices, vgname, vgsize, extsize=None):
        super().__init__(vgsize)
        self.devices = devices
        self.vgname = vgname
        self.vgsize = vgsize
        self.extsize = extsize

    @property
    def lvm_cmd(self):
        cmd = "/sbin/lvm"
        _lvm_cmd = getProgPath(cmd)
        if not _lvm_cmd:
            raise VolumesError(_("Command not found '%s'") % cmd)
        return _lvm_cmd

    def wipefs(self):
        cmd = "/sbin/wipefs"
        wipefs_cmd = getProgPath(cmd)
        if not wipefs_cmd:
            raise VolumesError(_("Command not found '%s'") % cmd)
        for dev in self.devices:
            process(wipefs_cmd, "-af", dev).success()

    def _kill_pvscan_processes(self):
        """
        Прибить lvm pvscan процессы, потому что они тормозят выполнением pvcreate
        """
        pvscan_pids = (int(pid) for pid, cmd in getRunCommands(withpid=True)
                       if "pvscan" in cmd)
        for pvscan_pid in pvscan_pids:
            os.kill(pvscan_pid, 9)

    def create_physical_volumes(self):
        self.wipefs()
        self._kill_pvscan_processes()
        p = process(self.lvm_cmd, "pvcreate", "-y", "-ff", *self.devices)
        return p.success()

    def create_volumes_group(self):
        if self.extsize:
            p = process(self.lvm_cmd, "vgcreate", self.vgname,
                        *(["-s%s" % self.extsize] + self.devices))
        else:
            p = process(self.lvm_cmd, "vgcreate", self.vgname,
                        *self.devices)
        return p.success()

    def create_lvm(self):
        return self.create_physical_volumes() and self.create_volumes_group()

    def create_logical_volume(self, lvname, size):
        size_param = self._lvmsize(size)
        self.occupe_size(size)
        return process(self.lvm_cmd, "lvcreate", size_param,
                       self.vgname, "-y", "-n", lvname).success()

    def get_partition_name(self, name):
        return "/dev/%s/%s" % (self.vgname, name)

    def _lvmsize(self, size):
        if size is not self.AllFree:
            if size < 0:
                bytesize = self.free_size + size
            else:
                bytesize = size
            return "-L%sk" % (Sizes().to_K(bytesize))
        else:
            return "-l100%FREE"

    def write(self):
        pass


class DosPartitionId():
    volume_types_map = {
        VolumesBuilder.VolumeType.Ntfs: "07",
        VolumesBuilder.VolumeType.Extended: "05",
        VolumesBuilder.VolumeType.Linux: "83",
        VolumesBuilder.VolumeType.LinuxSwap: "82",
        VolumesBuilder.VolumeType.Fat32: "0b",
        VolumesBuilder.VolumeType.Lvm: "8e",
        VolumesBuilder.VolumeType.Raid: "fd",
        VolumesBuilder.VolumeType.Generic: "83",
    }

    @classmethod
    def get(cls, _id, fallback=VolumesBuilder.VolumeType.Generic):
        return cls.volume_types_map.get(
            _id, cls.volume_types_map[fallback])


class GptPartitionId():
    volume_types_map = {
        VolumesBuilder.VolumeType.Ntfs: "ebd0a0a2-b9e5-4433-87c0-68b6b72699c7",
        VolumesBuilder.VolumeType.Fat32: "ebd0a0a2-b9e5-4433-87c0-68b6b72699c7",
        VolumesBuilder.VolumeType.Linux: "0fc63daf-8483-4772-8e79-3d69d8477de4",
        VolumesBuilder.VolumeType.LinuxSwap:
            "0657fd6d-a4ab-43c4-84e5-0933c84b4f4f",
        VolumesBuilder.VolumeType.Raid: "a19d880f-05fc-4d3b-a006-743f0f84911e",
        VolumesBuilder.VolumeType.Lvm: "e6d6d379-f507-44c2-a23c-238f2a3df928",
        VolumesBuilder.VolumeType.Efi: "c12a7328-f81f-11d2-ba4b-00a0c93ec93b",
        VolumesBuilder.VolumeType.BiosBoot:
            "21686148-6449-6e6f-744e-656564454649",
        VolumesBuilder.VolumeType.Generic:
            "0fc63daf-8483-4772-8e79-3d69d8477de4",
    }

    @classmethod
    def get(cls, _id, fallback=VolumesBuilder.VolumeType.Generic):
        return cls.volume_types_map.get(
            _id, cls.volume_types_map[fallback])


class RoundableVolumesBuilder(VolumesBuilder):
    rounding = 0.05
    free_size = 0
    last_partition = False
    sizes_map = {}
    minimal = 0

    def is_roundable_size(self, size):
        if (size is not VolumesBuilder.SizeAllFree and size and
                    abs(size - self.free_size) < min(size * self.rounding,
                                                     110 * Sizes.M)):
            return True
        return False

    def refined_size(self, size):
        if (size is VolumesBuilder.SizeAllFree or size < 0) and self.minimal:
            check_size = self.minimal
            if size is not VolumesBuilder.SizeAllFree:
                check_size -= size
            if (check_size > self.free_size and 
                not self.is_roundable_size(check_size)):
                return check_size
        if self.last_partition:
            if self.is_roundable_size(size):
                size = VolumesBuilder.SizeAllFree
        if size in self.sizes_map:
            return self.sizes_map[size]
        return size

    def create_last_volume(self, size, name,
                           purpose=VolumesBuilder.Purpose.Undefined):
        self.last_partition = True
        return self.create_volume(size, name, purpose)

    def set_minimal_volume_size(self, size):
        self.minimal = size


class LvmCalculator():
    def __init__(self, parent):
        self.parent = parent
        self.size = 0
        self.precreate_size = None

    def precreate(self, size, purpose):
        if purpose == VolumesBuilder.Purpose.Lvm:
            self.precreate_size = self.parent.free_size
        else:
            self.precreate_size = None

    def postcreate(self, size, purpose):
        if self.precreate_size and purpose == VolumesBuilder.Purpose.Lvm:
            self.size += self.precreate_size - self.parent.free_size
            self.precreate_size = None

class DosBuilder(RoundableVolumesBuilder):
    def __init__(self, dosdisk):
        self.dosdisk = dosdisk
        self.sizes_map = {VolumesBuilder.SizeAllFree: dosdisk.AllFree}
        self.next_part_number = 1
        self.lvm_data = LvmCalculator(self)

    def create(self):
        self.dosdisk.create_table()

    def create_volume(self, size, name,
                      purpose=VolumesBuilder.Purpose.Undefined):
        self.lvm_data.precreate(size, purpose)
        if self.next_part_number < 4:
            self.create_primary_partition(size)
        else:
            if self.next_part_number == 4:
                self.create_extended_partition()
                self.next_part_number += 1

            self.create_logical_partition(size)
        self.change_purpose(purpose)
        self.next_part_number += 1
        self.lvm_data.postcreate(size, purpose)
        return self.dosdisk.get_partition_name(self.next_part_number - 1)

    @property
    def free_size(self):
        return self.dosdisk.free_size

    def change_purpose(self, purpose):
        parttype = VolumesBuilder.purpose_type.get(
            purpose,
            VolumesBuilder.purpose_type[VolumesBuilder.Purpose.Undefined])
        volume_id = DosPartitionId.get(parttype)
        self.dosdisk.change_partition_type(volume_id)

    def create_primary_partition(self, size):
        self.dosdisk.create_primary_partition(self.refined_size(size))

    def create_logical_partition(self, size):
        self.dosdisk.create_logical_partition(self.refined_size(size))

    def create_extended_partition(self):
        self.dosdisk.create_extended_partition()

    def finish(self):
        if not self.dosdisk.write():
            raise VolumesError(_("Failed to create DOS partition table"))


class GptBuilder(RoundableVolumesBuilder):
    def __init__(self, gptdisk, table_rules):
        self.gptdisk = gptdisk
        self.table_rules = table_rules
        self.table_rules.parent = self
        self.sizes_map = {VolumesBuilder.SizeAllFree: self.gptdisk.AllFree}
        self.next_part_number = 1
        self.lvm_data = LvmCalculator(self)

    def create(self):
        self.gptdisk.create_table()

    def create_volume(self, size, name,
                      purpose=VolumesBuilder.Purpose.Undefined):
        return self.table_rules.create_volume(size, purpose)

    def get_last_partition_name(self):
        return self.gptdisk.get_partition_name(self.next_part_number - 1)

    @property
    def free_size(self):
        return self.gptdisk.free_size

    def create_partition(self, size, purpose):
        self.lvm_data.precreate(size, purpose)
        self.gptdisk.create_partition(self.refined_size(size))
        self.lvm_data.postcreate(size, purpose)
        self.change_purpose(purpose)
        self.next_part_number += 1

    def change_purpose(self, purpose):
        parttype = VolumesBuilder.purpose_type.get(
            purpose,
            VolumesBuilder.purpose_type[VolumesBuilder.Purpose.Undefined])
        volume_id = GptPartitionId.get(parttype)
        self.gptdisk.change_partition_type(volume_id)

    def finish(self):
        self.table_rules.finish()
        if not self.gptdisk.write():
            raise VolumesError(_("Failed to create GPT partition table"))

class GptRules():
    parent = None

    def finish(self):
        pass

class GptOnlyRules(GptRules):
    def create_volume(self, size, purpose):
        self.parent.create_partition(size, purpose)
        return self.parent.get_last_partition_name()

class GptFirstBootRules(GptRules):
    boot_purpose = VolumesBuilder.Purpose.BiosBoot

    def __init__(self, boot_size):
        self.boot_created = False
        self.boot_size = boot_size

    def create_boot(self, size=None):
        minimal = self.parent.minimal
        self.parent.minimal = 0
        self.parent.create_partition(size or self.boot_size,
                                     self.boot_purpose)
        self.boot_created = True
        self.parent.minimal = minimal

    def create_volume(self, size, purpose):
        if not self.boot_created:
            if purpose != self.boot_purpose:
                self.create_boot()
            self.boot_created = True
        self.parent.create_partition(size, purpose)
        return self.parent.get_last_partition_name()

def EfiGpt(obj, size=None):
    obj.boot_purpose = VolumesBuilder.Purpose.EfiBoot
    if size:
        obj.boot_size = size
    return obj

def BiosBoot(obj, size=None):
    obj.boot_purpose = VolumesBuilder.Purpose.BiosBoot
    if size:
        obj.boot_size = size
    return obj

class GptPosBootRules(GptFirstBootRules):
    def __init__(self, boot_size, part_pos=4):
        super().__init__(boot_size)
        self.part_pos = part_pos

    def create_volume(self, size, purpose):
        if purpose == self.boot_purpose:
            self.boot_created = True
        if not self.boot_created:
            if self.parent.next_part_number == self.part_pos:
                if purpose != self.boot_purpose:
                    self.create_boot()
            elif self.parent.last_partition:
                if (size is VolumesBuilder.SizeAllFree or
                        self.parent.is_roundable_size(size)):
                    self.parent.create_partition(-self.boot_size, purpose)
                    volname = self.parent.get_last_partition_name()
                    self.create_boot(VolumesBuilder.SizeAllFree)
                else:
                    self.parent.create_partition(size, purpose)
                    volname = self.parent.get_last_partition_name()
                    self.create_boot(self.boot_size)
                return volname
        self.parent.create_partition(size, purpose)
        return self.parent.get_last_partition_name()

    def finish(self):
        if not self.boot_created:
            self.create_boot(self.boot_size)


class LvmBuilder(RoundableVolumesBuilder):
    """
    Сборщик разделов для LVM
    """

    def __init__(self, lvmdisk):
        self.lvmdisk = lvmdisk
        self.sizes_map = {VolumesBuilder.SizeAllFree: lvmdisk.AllFree}

    def create(self):
        if not self.lvmdisk.create_lvm():
            raise VolumesError(_("Failed to create LVM"))

    @property
    def free_size(self):
        return self.lvmdisk.free_size

    def create_volume(self, size, name,
                      purpose=VolumesBuilder.Purpose.Undefined):
        if not self.lvmdisk.create_logical_volume(name,
                                                  self.refined_size(size)):
            raise VolumesError(_("Failed to create volume %s") % name)
        self.change_purpose(purpose)
        return self.lvmdisk.get_partition_name(name)

    def change_purpose(self, purpose):
        pass

    def finish(self):
        self.lvmdisk.write()


class VolumesSet():
    def __init__(self, builder):
        self.builder = builder
        self.params = []

    def create_volume(self, size, name, purpose, minimal_size=None):
        self.params.append((size, name, purpose, minimal_size))

    def __enter__(self):
        self.builder.create()
        return self

    def execute(self):
        for mark, data in classificate(self.params):
            size, name, purpose, minsize = data
            if minsize:
                self.builder.set_minimal_volume_size(minsize)
            if mark.last:
                self.builder.create_last_volume(size, name, purpose)
            else:
                self.builder.create_volume(size, name, purpose)

    def __exit__(self, exc_type, exc_val, exc_tb):
        self.execute()
        self.builder.finish()


class BuilderFactory(metaclass=ABCMeta):

    @abstractmethod
    def createDosBuilder(self, device, size):
        pass

    @abstractmethod
    def createGptBuilder(self, device, size, table_rules):
        pass

    @abstractmethod
    def createLvmBuilder(self, devices, vgname, vgsize, extsize=None):
        pass

class DiskFactory(BuilderFactory):
    def createDosBuilder(self, device, size):
        return DosBuilder(DosFdisk(device, size))

    def createGptBuilder(self, device, size, table_rules):
        return GptBuilder(GptFdisk(device, size),
                          table_rules)

    def createLvmBuilder(self, devices, vgname, vgsize, extsize=None):
        return LvmBuilder(LvmVolumeGroup(devices, vgname, vgsize,
                                         extsize=extsize))


class DeviceInfo(metaclass=ABCMeta):

    @abstractmethod
    def get_device_type(self, path=None, name=None):
        pass

class PurposedDosBuilder(DosBuilder):
    def change_purpose(self, purpose):
        super().change_purpose(purpose)
        self.dosdisk.change_purpose(purpose)

class PurposedGptBuilder(GptBuilder):
    def change_purpose(self, purpose):
        super().change_purpose(purpose)
        self.gptdisk.change_purpose(purpose)

class PurposedLvmBuilder(LvmBuilder):
    def change_purpose(self, purpose):
        super().change_purpose(purpose)
        self.lvmdisk.change_purpose(purpose)

class VariableFactory(BuilderFactory):
    def __init__(self, deviceinfo):
        self.disk_dev = []
        self.disk_mount = []
        self.disk_part = []
        self.disk_size = []
        self.disk_type = []
        self.dev_from = ""
        self.disk_format = []
        self.deviceinfo = deviceinfo
        self.lvm_size = {}
        self.lvm_disk_type = {}

        self.mbr = []
        self.efi = []

        self.purpose_mount = {
            VolumesBuilder.Purpose.Calculate: "/var/calculate",
            VolumesBuilder.Purpose.MainRoot: "/",
            VolumesBuilder.Purpose.Swap: "swap",
        }
        self.purpose_format = {
            VolumesBuilder.Purpose.Swap: "swap",
            VolumesBuilder.Purpose.EfiBoot: "vfat",
            VolumesBuilder.Purpose.Windows: "ntfs",
            VolumesBuilder.Purpose.UpdateRoot: ""
        }
        self.default_format = "ext4"

        self.pesize = None
        self.efinames = iter(
            traverse(("/boot/efi", ("/boot/efi%d" % x for x in range(2, 100)))))

    def get_mount_point(self, purpose):
        if purpose == VolumesBuilder.Purpose.EfiBoot:
            return next(self.efinames)
        return self.purpose_mount.get(purpose, '')

    def get_format(self, purpose):
        return self.purpose_format.get(purpose, self.default_format)

    def createDosBuilder(self, device, size):
        return PurposedDosBuilder(VirtualDosDisk(self, device, size))

    def createGptBuilder(self, device, size, table_rules):
        return PurposedGptBuilder(VirtualGptDisk(self, device, size),
                                  table_rules)

    def createLvmBuilder(self, devices, vgname, vgsize, extsize=None):
        return PurposedLvmBuilder(VirtualLvm(self, devices, vgname, vgsize,
                                             pesize=self.pesize))

    def get_device_type(self, name):
        if name in self.lvm_disk_type:
            return self.lvm_disk_type[name]
        return self.deviceinfo.get_device_type(name=name)

    def set_mbr(self, device):
        self.mbr.append(device)

    def set_efi(self, device):
        self.efi.append(device)


class SchemeError(Exception):
    pass

class DeviceSchemeError(SchemeError):
    pass

class SchemeBuilder():
    class PartitionTable():
        GPT = 0
        DOS = 1

    def __init__(self):
        self.efi = False
        self.partition_table = self.PartitionTable.DOS
        self.lvm = False
        self.devices = []
        self.swap_size = Sizes().G
        self.root_size = 10 * Sizes().G
        self.efi_size = 200 * Sizes().M
        self.biosboot_size = 50 * Sizes().M
        self.swap = False
        self.minimal_lvm_size = 1 * Sizes().G
        self.minimal_calculate_size = 500 * Sizes().M
        self.update = False
        self.calculate = False
        self.rootall = False
        self.rounding = 0.1
        self.vgname = "calculate"

        self.extsize = None

    def add_device(self, device, _type, size):
        self.devices.append((device, _type, size))

    def create_lvm_devices(self, volumes_factory):
        for dev, _type, size in self.devices:
            part_rules = GptFirstBootRules(self.biosboot_size)
            if self.efi:
                part_rules = EfiGpt(part_rules, size=self.efi_size)
            else:
                part_rules = BiosBoot(part_rules, size=self.biosboot_size)
            if _type == VolumesBuilder.DeviceType.RAID:
                yield dev, size
            else:
                if self.partition_table is SchemeBuilder.PartitionTable.DOS:
                    builder = volumes_factory.createDosBuilder(dev, size)
                else:
                    builder = volumes_factory.createGptBuilder(
                        dev, size, part_rules)
                builder.create()
                builder.set_minimal_volume_size(self.minimal_lvm_size)
                devname = builder.create_last_volume(
                    VolumesBuilder.SizeAllFree, "lvm",
                    VolumesBuilder.Purpose.Lvm)
                builder.finish()
                devsize = builder.lvm_data.size
                yield devname, devsize

    def get_volume_builder(self, volumes_factory):
        if not self.devices:
            raise DeviceSchemeError(
                _("Please select devices for partitions changing"))
        if self.lvm:
            lvm_devices, lvm_sizes = list(zip(
                *self.create_lvm_devices(volumes_factory)))
            return volumes_factory.createLvmBuilder(
                list(lvm_devices), self.vgname,
                sum(lvm_sizes), extsize=self.extsize)
        else:
            if len(self.devices) > 1:
                raise DeviceSchemeError(
                    _("You should use LVM to install on more that one device"))
            dev, devtype, size = self.devices[0]
            if self.partition_table is SchemeBuilder.PartitionTable.DOS:
                return volumes_factory.createDosBuilder(dev, size)
            else:
                if devtype is VolumesBuilder.DeviceType.RAID:
                    return volumes_factory.createGptBuilder(
                        dev, size, GptOnlyRules())
                else:
                    part_rules = GptPosBootRules("", 4)
                    if self.efi:
                        part_rules = EfiGpt(part_rules, size=self.efi_size)
                    else:
                        part_rules = BiosBoot(part_rules,
                                              size=self.biosboot_size)
                    return volumes_factory.createGptBuilder(dev, size,
                                                            part_rules)

    def process(self, volumes_factory):
        volume_builder = self.get_volume_builder(volumes_factory)
        volume_builder.rounding = self.rounding
        if self.rootall and (self.update or self.calculate):
            raise SchemeError(
                _("Root to all free size should not use with update partition "
                  "or calculate partition"))
        with VolumesSet(volume_builder) as builder:
            if self.swap:
                builder.create_volume(self.swap_size, "swap",
                                      VolumesBuilder.Purpose.Swap)
            if self.rootall:
                builder.create_volume(VolumesBuilder.SizeAllFree, "system1",
                                      VolumesBuilder.Purpose.MainRoot, None)
            else:
                builder.create_volume(self.root_size, "system1",
                                      VolumesBuilder.Purpose.MainRoot, None)
                if self.update:
                    builder.create_volume(self.root_size, "system2",
                                          VolumesBuilder.Purpose.UpdateRoot)
                if self.calculate:
                    builder.create_volume(VolumesBuilder.SizeAllFree, "calculate",
                                          VolumesBuilder.Purpose.Calculate,
                                          self.minimal_calculate_size)
