from db import Base
from sqlalchemy import Column, Unicode, Table, Integer, ForeignKey, DateTime, Numeric
from sqlalchemy.orm import relation, backref
# from plugins.hardware_profile.model import HardwareProfile
import datetime
from decimal import Decimal

metadata = Base.metadata

class DiskProfile(Base):
    '''
    This class represents a profile for the disks of a computer.
    By now a disk profile is only a list of disks. In a future raid and
    lvm thing could be add.
    The computers are all computers that have this disk profile.
    The name is used by human identify.
    '''
    __tablename__ = 'diskprofile'

    id = Column(Integer, primary_key=True)
    name = Column(Unicode(256), unique=True)
    description = Column(Unicode(4096))
    last_modified_date = Column(DateTime())

    diskprofile_computer = Table(
            'diskprofile_computer', metadata,
            Column('diskprofile_id', Integer, ForeignKey('diskprofile.id')),
            Column('computer_id', Integer, ForeignKey('computer.id')))

    computers = relation('Computer', secondary = diskprofile_computer,
            backref='disk_profile')

    def __init__(self, name = ''):
        self.name = name
        self.last_modified_date = datetime.datetime.now()

    def add_empty_disk(self):
        '''
        Add a empty disk.
        '''
        number = len(self._disks) + 1
        disk = Disk(number, self.id)
        self._disks.append(disk)
        return disk

    def add_disk(self,disk):
        '''
        Add a disk.
        '''
        disk.number = len(self._disks) + 1
        disk.diskprofile_id = self.id
        self._disks.append(disk)

    def remove_disk(self, n_disk):
        '''
        Remove a disk. Rearranges the disks' number.
        '''
        self._disks.pop(n_disk - 1)
        for i in xrange(n_disk, len(self._disks)+1):
            self._disk[i].number = i
    def get_disks_number(self):
        '''
        Return how many disks there are.
        '''
        return len(self._disks)

    def get_disks(self):
        '''
        Return the disks.
        '''
        return self._disks

    def get_disk(self, number):
        '''
        Returns the disk with the given number.
        None is returned when disk was not found.
        '''
        if number > self.get_disks_number():
            return None
        elif number < 0:
            return None
        
        return self._disks[number - 1]

    def get_cache(self):
        '''
        Cache is in the first disk.
        See Disk.get_cache()
        '''
        disk = self.get_disk(1)
        if disk:
            return disk.get_cache()
        return None

    def get_max_cache(self):
        '''
        Cache is in the first disk.
        See Disk.get_max_cache()
        '''
        disk = self.get_disk(1)
        if disk:
            return disk.get_max_cache()
        return 0

    def set_cache(self,size):
        '''
        Cache is in the first disk.
        See Disk.set_cache(size)
        '''
        disk = self.get_disk(1)
        if disk:
            return disk.set_cache(size)
        return False

class Disk(Base):
    '''
    This class is used by DiskProfile. A disk has a number to identify it
    inside of the DiskProfile. For example is the number is 1 means
    /dev/sda (in linux nomeclature), 2 means /dev/sdb, etc.

    Disks are relative defined. Their size is defined like a 100%.
    _partitions is the list of the partitions that disk has.
    Partitions are defined like a percentage of the disk.
    '''
    __tablename__ = 'disk'

    id = Column(Integer, primary_key=True)
    number = Column(Integer)

    diskprofile_id = Column(Integer, ForeignKey('diskprofile.id'))
    diskprofile = relation(DiskProfile, backref=backref('_disks',
        cascade='all',
        passive_deletes=True))

    MAX_PRIMARY_PARTITIONS = 4
    CACHE_PARTITION = 4
    MAX_SIZE_CACHE = 50

    def __init__(self, number = 1, diskprofile_id = None):
        self.number = number
        self.diskprofile_id = diskprofile_id

    def get_partition(self, number):
        for p in self._partitions:
            if p.number == number:
                return p

        return None

    def get_partitions_order_by_position(self):
        '''
        Returns the partitions list order by the position, it means,
        by its appearance on the hard disk.
        The free space are ommited. See get_partitions_with_free_spaces()
        The extended partition is ommited too. See get_extended_partition()
        '''
        return sorted(self._partitions, key=lambda p: p.position)

    def get_free_spaces(self):
        '''
        Returns a list of partitions whose type is 'FREE' and number is 0.
        Obviously those "partitions" corresponds to the free hard disk
        spaces.
        '''
        partitions = self.get_partitions_order_by_position()

        # If there isn't partitions all is free space
        if not partitions:
            return [Partition(size = 100, position = 0,
                a_type = 'FREE')]

        free_spaces = []
        # Checking if there is free space at the begining.
        if partitions[0].position != Decimal(0):
            p = Partition(size = partitions[0].position, position = 0,
                    a_type = 'FREE')
            free_spaces.append(p)

        for i in xrange(0, len(partitions) - 1):
            end_partition = partitions[i].position + partitions[i].size
            space = partitions[i+1].position - end_partition
            if space != Decimal(0):
                p = Partition(size = space, position = end_partition,
                        a_type = 'FREE')
                free_spaces.append(p)

        # Checking if there is free space at the end
        end_partition = partitions[-1].position + partitions[-1].size
        if end_partition != Decimal(100):
            p = Partition(size = Decimal(100) - end_partition, position = end_partition,
                    a_type = 'FREE')
            free_spaces.append(p)

        return free_spaces

    def get_partitions_with_free_spaces(self):
        '''
        Returns the partitions list order by the position, it means,
        by its appearance on the hard disk.
        The free spaces are included in this functions. They are partitions
        with number = 0 and type = 'FREE'.
        The extended partition is ommited. See get_extended_partition()
        '''
        aux = self.get_partitions_order_by_position() + self.get_free_spaces()
        return sorted(aux, key=lambda p: p.position)

    def get_partitions_order_by_num(self):
        '''
        Returns the partitions list order by the number in the disk.
        Free spaces are ommited in this function.
        '''
        return sorted(self._partitions, key=lambda p: p.number)

    def get_primary_partitions(self):
        '''
        Returns only the primary partitions order by the number.
        '''
        return [p for p in self.get_partitions_order_by_num()
                if p.type == "PRIMARY"]

    def get_logic_partitions(self):
        '''
        Returns only the logic partitions order by the number.
        '''
        return [p for p in self.get_partitions_order_by_num()
                if p.type == "LOGIC"]

    def get_partitions_inside_extended(self):
        '''
        Returns a list of the logic partitions a free spaces inside
        of the extended partition order by position.
        '''
        logic_partitions = self.get_logic_partitions()
        if not logic_partitions:
            return []

        logic_partitions = sorted(logic_partitions, key=lambda p: p.position)
        # Getting size and position
        start = logic_partitions[0].position
        finish = logic_partitions[-1].final_position()

        free_spaces = [p for p in self.get_free_spaces() if p.position
                >= start and p.position < finish]
        
        return sorted(logic_partitions + free_spaces, key=lambda p: p.position)

    def get_extended_partition(self):
        '''
        Returns the extended partitions. The extended partition doesn't
        appear in the partition list to do the code easier. However this
        function solves this lack if the extended partition is needed.
        '''
        extended = Partition(a_type = 'EXTENDED')
        logic_partitions = self.get_partitions_inside_extended()
        if not logic_partitions:
            return None

        extended.position = logic_partitions[0].position
        extended.size = logic_partitions[-1].final_position() -\
                extended.position

        # Getting the number
        for i in xrange(1, Disk.MAX_PRIMARY_PARTITIONS + 1):
            if not self.get_partition(i):
                extended.number = i
                return extended

    def get_booteable_partitions(self):
        '''
        TODO
        '''
        return []

    def get_partitions_with_free_spaces_and_extended(self):
        '''
        Returns a list of the primary partitions and extended partition
        with free spaces ordered by position. Logic partitions and free
        spaces inside the extended partition are ommited.
        '''
        
        extended = self.get_extended_partition()
        primary_partitions = self.get_primary_partitions()
        free_spaces = []
        if extended:
            free_spaces = [p for p in self.get_free_spaces() if
                    p.position < extended.position or
                    p.position >= extended.final_position()]
            return sorted(primary_partitions + free_spaces + [extended],
                    key=lambda p: p.position)
        else:
            free_spaces = self.get_free_spaces()
            return sorted(primary_partitions + free_spaces,
                    key=lambda p: p.position)


    def can_add_primary_partition(self):
        '''
        Returns the free space avaliable to make a primary partition
        if possible or else returns 0.
        '''
        num_primary_partitions = len(self.get_primary_partitions())
        if self.get_extended_partition():
            num_primary_partitions += 1

        # Checking there are not the maximum primary partitions allowed
        if num_primary_partitions >= Disk.MAX_PRIMARY_PARTITIONS:
            return 0

        # If there are free spaces
        free_spaces = self.get_free_spaces()
        # which are not in extended partition
        extended = self.get_extended_partition()
        if extended:
            free_spaces = [p for p in free_spaces if p.position <
                    extended.position or p.position >= extended.final_position()]
        # returning the biggest size
        if free_spaces:
            return sorted(free_spaces, key=lambda p: p.size)[0].size

        # Else
        return 0

    def can_add_logic_partition(self):
        '''
        Returns the free space avaliable to make a logic partition
        if possible or else returns 0.
        '''

        # If there is the maximum number of primary partitions
        num_primary_partitions = len(self.get_primary_partitions())
        if num_primary_partitions >= Disk.MAX_PRIMARY_PARTITIONS:
            return 0

        # If there is not logic partitions returns the biggest size
        if not self.get_logic_partitions():
            free_spaces = self.get_free_spaces()
            if free_spaces:
                return sorted(free_spaces, key=lambda p: p.size)[0].size
            else:
                return 0

        free_space = 0
        # Looking for the biggest free space next to logic partitions
        partitions = self.get_partitions_with_free_spaces()
        for i in xrange(0, len(partitions)):
            if partitions[i].type != 'FREE':
                continue

            # Checking if left partition is a logic partition
            if i != 0:
                if partitions[i-1].type == 'LOGIC':
                    if partitions[i].size > free_space:
                        free_space = partitions[i].size


            if i < len(partitions) - 1:
                if partitions[i+1].type == 'LOGIC':
                    if partitions[i].size > free_space:
                        free_space = partitions[i].size

        return free_space

    def add_partition(self, size = 0, number = 0, primary = True, begining = True,
            filesystem=''):
        '''
        Adds a partition with the received parameters.
        Returns the partition if everything was ok, otherwise None.
        '''
        # Size MUST BE a Decimal
        size = Decimal(size)
        if primary and self.can_add_primary_partition() < size:
            return None
        elif not primary and self.can_add_logic_partition() < size:
            return None

        partition = Partition()
        if primary:
            partition.type = "PRIMARY"
        else:
            partition.type = "LOGIC"

        # Checking if there is a partition with that number
        if number != 0:
            for p in self._partitions:
                if p.number == number:
                    return None

            if primary and number > Disk.MAX_PRIMARY_PARTITIONS:
                return None
            elif not primary and number <= Disk.MAX_PRIMARY_PARTITIONS:
                return None

            partition.number = number

        # Putting a right number
        else:
            if primary:
                for i in xrange(1, Disk.MAX_PRIMARY_PARTITIONS + 1):
                    if not self.get_partition(i):
                        partition.number = i
                        break
            else:
                logic_partitions = self.get_logic_partitions()
                if logic_partitions:
                    partition.number = logic_partitions[-1].number + 1
                else:
                    partition.number = Disk.MAX_PRIMARY_PARTITIONS + 1

        # If size is 0 use all free space avaliable
        if size == Decimal(0):
            partition.size = self.get_free_space()

        else:
            partition.size = size

        free_spaces = self.get_free_spaces()
        free_space = None
        if begining:
            free_space = free_spaces[0]
        else:
            free_space = free_spaces[-1]

        if size == Decimal(0):
            partition.size = free_space.size
        elif size > free_space.size:
            return None

        if begining:
            partition.position = free_space.position
        else:
            partition.position = free_space.position + free_space.size -\
                    partition.size

        partition.filesystem=filesystem
        self._partitions.append(partition)
        return partition


    def remove_partition(self, number):
        '''
        Removes the partition with the given number.
        '''
        partition = self.get_partition(number)
        if partition:
            self._partitions.remove(partition)

    def get_cache(self):
        '''
        Returns the cache partition if exists
        '''
        cache = self.get_partition(Disk.CACHE_PARTITION)
        if cache and cache.filesystem == "CACHE":
            return cache

        return None

    def get_max_cache(self):
        '''
        Returns the maximum size for the cache partition in the disk.
        A cache partition must be at the end of the disk and it must be
        the number Disk.CACHE_PARITION.
        '''
        cache = self.get_partition(Disk.CACHE_PARTITION)

        # Getting the last free space
        free_spaces = self.get_free_spaces()
        last_free_space = None
        if free_spaces:
            last_free_space = free_spaces[-1]

        # If exists a fourth partition
        if cache:
            # And it is not a cache partition
            if not cache.filesystem == "CACHE":
                # Cache is not possible
                return 0

            # Checking if there is a free space next to cache
            if last_free_space.final_position() == cache.position:
                return min(last_free_space.size + cache.size,
                        Disk.MAX_SIZE_CACHE)

            return cache.size

        # If there is free space at the end
        if last_free_space.final_position() == Decimal(100):
            return min(last_free_space.size, Disk.MAX_SIZE_CACHE)

        return 0

    def set_cache(self, size):
        '''
        Creates or modifies the cache partition to the given size.
        If size is 0 the cache partition is removed.
        Returns True if the change was sucessfully, otherwise False.
        '''
        # Size MUST BE a Decimal
        size = Decimal(size)

        if self.get_max_cache() < size:
            return False

        if size == Decimal(0):
            cache = self.get_cache()
            if cache:
                self.remove_partition(Disk.CACHE_PARTITION)
                return

        self.remove_partition(Disk.CACHE_PARTITION)
        cache = self.add_partition(size=size, number=Disk.CACHE_PARTITION,
                begining=False, filesystem="CACHE")

        return True


class Image(Base):
    '''
    '''
    __tablename__ = 'image'

    id = Column(Integer, primary_key=True)
    name = Column(Unicode(256), unique=True)
    description = Column(Unicode(2048))

    creation_date = Column(DateTime())
    size = Column(Integer)
    size_in_disk = Column(Integer)
    filesystem = Column(Unicode(128))
    mount_point = Column(Unicode(256))

    image_repo = Table(
            'image_repo', metadata,
            Column('image_id', Integer, ForeignKey('image.id')),
            Column('computer_id', Integer, ForeignKey('computer.id')))
    repos = relation('Computer', secondary = image_repo, backref='images')

    '''
    image_hardware = Table(
            'image_hardware', metadata,
            Column('image_id', Integer, ForeignKey('image.id')),
            Column('hardware_profile_id', Integer,
                ForeignKey('hardware_profile.id')))
    hardware = relation('HardwareProfile', secondary = image_hardware,
            backref='images')
            '''

    def __init__(self, filesystem = '', size = 0, used = 0,
            mount_point = '', so = '', arch = ''):
        self.filesystem = filesystem
        self.size = size
        self.used = used
        self.mount_point = mount_point
        self.so = so
        self.arch = arch
        # self.software_inventory = None


class Partition(Base):
    '''
    This class is used by Disk. A partition has a number to identify it
    insise of the Disk. For example is the disk is number 1 and the
    partition is the number 2 means /dev/sda2 (in linux nomeclature) or
    if the disk is 2 and partition is 6 means /dev/sdb6.
    The position and the size are relative to the disk. Their value are
    between 0 and 100. For example, if the position is 0 and the size is
    50, when the partition would be restored in a hard disk whose size
    is 80GB, a 40GB partition will be created at the begining of the disk.
    Both are fixed to two decimals. If the next partition is defined with
    position 50 and size 25, a 20GB partition will be created next to the
    first.
    The type is like enum type whose values can be:
    'PRIMARY', 'LOGIC', 'EXTENDED' and 'FREE'.
    Only primary and logic partitions will be saved because with that
    information we can now the free spaces and the extended partition.
    Flags is unused right now. An image is can be associate to the
    partitions. After partitioning this image will be restorated in
    the partition.
    '''

    __tablename__ = 'partition'

    id = Column(Integer, primary_key=True)
    disk_id = Column(Integer, ForeignKey('disk.id'))
    disk = relation(Disk, backref=backref('_partitions',
        cascade='all',
        passive_deletes=True))

    image_id = Column(Integer, ForeignKey('image.id'))
    image = relation(Image, backref=backref('partitions',
        cascade='all',
        passive_deletes=False))

    filesystem = Column(Unicode(128))

    number = Column(Integer)
    position = Column(Numeric(2))
    size = Column(Numeric(2))
    type = Column(Unicode(32))
    flags = Column(Unicode(256))

    filesystems = ('FAT', 'NTFS', 'EXT2', 'EXT3', 'EXT4', 'REISERFS',
            'XFS', 'JFS', 'HFS', 'HFS+', 'SWAP')
    def __init__(self, size = 0, number = 0, position = 0, a_type = '', flags = '', image_id = None):
        self.number = number
        self.size = size
        self.position = position
        self.flags = flags
        self.type = a_type
        self.image_id = image_id

    def final_position(self):
        return self.position + self.size
