from abc import ABCMeta
from abc import abstractmethod
from bootstrapvz.common.sectors import Sectors
from bootstrapvz.common.tools import log_check_call
from bootstrapvz.common.fsm_proxy import FSMProxy


class AbstractPartition(FSMProxy):
    """Abstract representation of a partiton
    This class is a finite state machine and represents the state of the real partition
    """

    __metaclass__ = ABCMeta

    # Our states
    events = [{'name': 'create', 'src': 'nonexistent', 'dst': 'created'},
              {'name': 'format', 'src': 'created', 'dst': 'formatted'},
              {'name': 'mount', 'src': 'formatted', 'dst': 'mounted'},
              {'name': 'unmount', 'src': 'mounted', 'dst': 'formatted'},
              ]

    def __init__(self, size, filesystem, format_command, mountopts):
        """
        :param Bytes size: Size of the partition
        :param str filesystem: Filesystem the partition should be formatted with
        :param list format_command: Optional format command, valid variables are fs, device_path and size
        """
        self.size           = size
        self.filesystem     = filesystem
        self.format_command = format_command
        # List of mount options
        self.mountopts      = mountopts
        # Initialize the start & end padding to 0 sectors, may be changed later
        self.pad_start = Sectors(0, size.sector_size)
        self.pad_end = Sectors(0, size.sector_size)
        # Path to the partition
        self.device_path    = None
        # Dictionary with mount points as keys and Mount objects as values
        self.mounts         = {}

        # Create the configuration for our state machine
        cfg = {'initial': 'nonexistent', 'events': self.events, 'callbacks': {}}
        super(AbstractPartition, self).__init__(cfg)

    def get_uuid(self):
        """Gets the UUID of the partition

        :return: The UUID of the partition
        :rtype: str
        """
        [uuid] = log_check_call(['blkid', '-s', 'UUID', '-o', 'value', self.device_path])
        return uuid

    @abstractmethod
    def get_start(self):
        pass

    def get_end(self):
        """Gets the end of the partition

        :return: The end of the partition
        :rtype: Sectors
        """
        return self.get_start() + self.pad_start + self.size + self.pad_end

    def _before_format(self, e):
        """Formats the partition
        """
        # If there is no explicit format_command define we simply call mkfs.fstype
        if self.format_command is None:
            format_command = ['mkfs.{fs}', '{device_path}']
        else:
            format_command = self.format_command
        variables = {'fs': self.filesystem,
                     'device_path': self.device_path,
                     'size': self.size,
                     }
        command = map(lambda part: part.format(**variables), format_command)
        # Format the partition
        log_check_call(command)

    def _before_mount(self, e):
        """Mount the partition
        """
        if self.mountopts is None:
            mount_command = ['mount', '--types', self.filesystem, self.device_path, e.destination]
        else:
            mount_command = ['mount', '--options', ",".join(self.mountopts), '--types', self.filesystem, self.device_path, e.destination]
        # Mount the partition
        log_check_call(mount_command)
        self.mount_dir = e.destination

    def _after_mount(self, e):
        """Mount any mounts associated with this partition
        """
        # Make sure we mount in ascending order of mountpoint path length
        # This ensures that we don't mount /dev/pts before we mount /dev
        for destination in sorted(self.mounts.iterkeys(), key=len):
            self.mounts[destination].mount(self.mount_dir)

    def _before_unmount(self, e):
        """Unmount any mounts associated with this partition
        """
        # Unmount the mounts in descending order of mounpoint path length
        # You cannot unmount /dev before you have unmounted /dev/pts
        for destination in sorted(self.mounts.iterkeys(), key=len, reverse=True):
            self.mounts[destination].unmount()
        log_check_call(['umount', self.mount_dir])
        del self.mount_dir

    def add_mount(self, source, destination, opts=[]):
        """Associate a mount with this partition
        Automatically mounts it

        :param str,AbstractPartition source: The source of the mount
        :param str destination: The path to the mountpoint
        :param list opts: Any options that should be passed to the mount command
        """
        # Create a new mount object, mount it if the partition is mounted and put it in the mounts dict
        from mount import Mount
        mount = Mount(source, destination, opts)
        if self.fsm.current == 'mounted':
            mount.mount(self.mount_dir)
        self.mounts[destination] = mount

    def remove_mount(self, destination):
        """Remove a mount from this partition
        Automatically unmounts it

        :param str destination: The mountpoint path of the mount that should be removed
        """
        # Unmount the mount if the partition is mounted and delete it from the mounts dict
        # If the mount is already unmounted and the source is a partition, this will raise an exception
        if self.fsm.current == 'mounted':
            self.mounts[destination].unmount()
        del self.mounts[destination]
