# -*- 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 calculate.lib.utils.files import process, getProgPath
from calculate.lib.utils.tools import Sizes, classificate
from calculate.lib.utils.device import humanreadableSize
import re

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

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

class VolumesError(Exception):
    pass

class VolumesBuilder(object):

    __metaclass__ = ABCMeta

    class DeviceType(object):
        Device = 0
        RAID = 1

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

    class Purpose(object):
        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(object):
    __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(object):
    __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(object):
    __metaclass__ = ABCMeta

    @abstractmethod
    def create_lvm(self):
        pass

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

    @abstractmethod
    def write(self):
        pass


class BlockDisk(object):
    pass

class VirtualDiskError(Exception):
    pass

class SizeableDisk(object):
    AllFree = None
    header_size = 0

    def __init__(self, size):
        self.disk_size = size
        self.free_size = 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
            avail = humanreadableSize(availbytes)
            need = humanreadableSize(needbytes)
            raise VirtualDiskError(
                _("There is not enough space on this device") + _(": ") +
                _("{avail} available, need at {need} more").format(
                    avail=avail,
                    need=need))


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

    class Partition(object):
        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(VirtualDisk, self).__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)
            self.parent.disk_format.append(partition.format or "ext4")
        return True

class VirtualDosDisk(VirtualDisk, DosDisk):
    parttype = "dos"

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

class VirtualGptDisk(VirtualDisk, GptDisk):
    parttype = "gpt"

class VirtualLvm(SizeableDisk, LvmDisk):

    class Volume(object):
        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(VirtualLvm, self).__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(Fdisk, self).__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 "+%d" % (Sizes().to_Sector(bytesize)-1)
        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("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(LvmVolumeGroup, self).__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 create_physical_volumes(self):
        self.wipefs()
        p = process(self.lvm_cmd, "pvcreate", "-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, "-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%ss" % (Sizes().to_Sector(bytesize)-1)
        else:
            return "-l100%FREE"

    def write(self):
        pass


class DosPartitionId(object):
    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(object):
    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) < size * self.rounding):
            return True
        return False

    def refined_size(self, size):
        if size is VolumesBuilder.SizeAllFree and self.minimal:
            if self.minimal > self.free_size:
                return self.minimal
        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(object):
    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(object):
    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(GptPosBootRules, self).__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(object):
    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(object):
    __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(object):
    __metaclass__ = ABCMeta

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

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

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

class PurposedLvmBuilder(LvmBuilder):
    def change_purpose(self, purpose):
        super(PurposedLvmBuilder, self).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",
        }
        self.default_format = "ext4"

        self.pesize = None

    def get_mount_point(self, purpose):
        if not self.efi and purpose == VolumesBuilder.Purpose.EfiBoot:
            return "/boot/efi"
        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(object):
    class PartitionTable(object):
        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.rounding = 0.01
        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 = 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
        with VolumesSet(volume_builder) as builder:
            if self.swap:
                builder.create_volume(self.swap_size, "swap",
                                      VolumesBuilder.Purpose.Swap)
            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)
