#!/usr/bin/env python -tt
#
# Copyright (c) 2009, 2010, 2011 Intel, Inc.
# Copyright (c) 2007, 2008 Red Hat, Inc.
# Copyright (c) 2008 Daniel P. Berrange
# Copyright (c) 2008 David P. Huff
#
# This program is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by the Free
# Software Foundation; version 2 of the License
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
# or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
# for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc., 59
# Temple Place - Suite 330, Boston, MA 02111-1307, USA.

import os
from wic import msger
from wic.utils.errors import ImageError
from wic.utils.oe.misc import exec_cmd, exec_native_cmd
from wic.filemap import sparse_copy

# Overhead of the MBR partitioning scheme (just one sector)
MBR_OVERHEAD = 1

# Overhead of the GPT partitioning scheme
GPT_OVERHEAD = 34

# Size of a sector in bytes
SECTOR_SIZE = 512

class Image():
    """
    Generic base object for an image.

    An Image is a container for a set of DiskImages and associated
    partitions.
    """
    def __init__(self, native_sysroot=None):
        self.disks = {}
        self.partitions = []
        self.partimages = []
        # Size of a sector used in calculations
        self.sector_size = SECTOR_SIZE
        self._partitions_layed_out = False
        self.native_sysroot = native_sysroot

    def __add_disk(self, disk_name):
        """ Add a disk 'disk_name' to the internal list of disks. Note,
        'disk_name' is the name of the disk in the target system
        (e.g., sdb). """

        if disk_name in self.disks:
            # We already have this disk
            return

        assert not self._partitions_layed_out

        self.disks[disk_name] = \
                {'disk': None,     # Disk object
                 'numpart': 0,     # Number of allocate partitions
                 'realpart': 0,    # Number of partitions in the partition table
                 'partitions': [], # Indexes to self.partitions
                 'offset': 0,      # Offset of next partition (in sectors)
                 # Minimum required disk size to fit all partitions (in bytes)
                 'min_size': 0,
                 'ptable_format': "msdos", # Partition table format
                 'identifier': None} # Disk system identifier

    def add_disk(self, disk_name, disk_obj, identifier):
        """ Add a disk object which have to be partitioned. More than one disk
        can be added. In case of multiple disks, disk partitions have to be
        added for each disk separately with 'add_partition()". """

        self.__add_disk(disk_name)
        self.disks[disk_name]['disk'] = disk_obj
        self.disks[disk_name]['identifier'] = identifier

    def __add_partition(self, part):
        """ This is a helper function for 'add_partition()' which adds a
        partition to the internal list of partitions. """

        assert not self._partitions_layed_out

        self.partitions.append(part)
        self.__add_disk(part['disk_name'])

    def add_partition(self, size, disk_name, mountpoint, source_file=None, fstype=None,
                      label=None, fsopts=None, boot=False, align=None, no_table=False,
                      part_type=None, uuid=None, system_id=None):
        """ Add the next partition. Partitions have to be added in the
        first-to-last order. """

        ks_pnum = len(self.partitions)

        # Converting kB to sectors for parted
        size = size * 1024 // self.sector_size

        part = {'ks_pnum': ks_pnum, # Partition number in the KS file
                'size': size, # In sectors
                'mountpoint': mountpoint, # Mount relative to chroot
                'source_file': source_file, # partition contents
                'fstype': fstype, # Filesystem type
                'fsopts': fsopts, # Filesystem mount options
                'label': label, # Partition label
                'disk_name': disk_name, # physical disk name holding partition
                'device': None, # kpartx device node for partition
                'num': None, # Partition number
                'boot': boot, # Bootable flag
                'align': align, # Partition alignment
                'no_table' : no_table, # Partition does not appear in partition table
                'part_type' : part_type, # Partition type
                'uuid': uuid, # Partition UUID
                'system_id': system_id} # Partition system id

        self.__add_partition(part)

    def layout_partitions(self, ptable_format="msdos"):
        """ Layout the partitions, meaning calculate the position of every
        partition on the disk. The 'ptable_format' parameter defines the
        partition table format and may be "msdos". """

        msger.debug("Assigning %s partitions to disks" % ptable_format)

        if self._partitions_layed_out:
            return

        self._partitions_layed_out = True

        # Go through partitions in the order they are added in .ks file
        for num in range(len(self.partitions)):
            part = self.partitions[num]

            if part['disk_name'] not in self.disks:
                raise ImageError("No disk %s for partition %s" \
                                 % (part['disk_name'], part['mountpoint']))

            if ptable_format == 'msdos' and part['part_type']:
                # The --part-type can also be implemented for MBR partitions,
                # in which case it would map to the 1-byte "partition type"
                # filed at offset 3 of the partition entry.
                raise ImageError("setting custom partition type is not " \
                                 "implemented for msdos partitions")

            # Get the disk where the partition is located
            disk = self.disks[part['disk_name']]
            disk['numpart'] += 1
            if not part['no_table']:
                disk['realpart'] += 1
            disk['ptable_format'] = ptable_format

            if disk['numpart'] == 1:
                if ptable_format == "msdos":
                    overhead = MBR_OVERHEAD
                elif ptable_format == "gpt":
                    overhead = GPT_OVERHEAD

                # Skip one sector required for the partitioning scheme overhead
                disk['offset'] += overhead

            if disk['realpart'] > 3:
                # Reserve a sector for EBR for every logical partition
                # before alignment is performed.
                if ptable_format == "msdos":
                    disk['offset'] += 1


            if part['align']:
                # If not first partition and we do have alignment set we need
                # to align the partition.
                # FIXME: This leaves a empty spaces to the disk. To fill the
                # gaps we could enlargea the previous partition?

                # Calc how much the alignment is off.
                align_sectors = disk['offset'] % (part['align'] * 1024 // self.sector_size)

                if align_sectors:
                    # If partition is not aligned as required, we need
                    # to move forward to the next alignment point
                    align_sectors = (part['align'] * 1024 // self.sector_size) - align_sectors

                    msger.debug("Realignment for %s%s with %s sectors, original"
                                " offset %s, target alignment is %sK." %
                                (part['disk_name'], disk['numpart'], align_sectors,
                                 disk['offset'], part['align']))

                    # increase the offset so we actually start the partition on right alignment
                    disk['offset'] += align_sectors

            part['start'] = disk['offset']
            disk['offset'] += part['size']

            part['type'] = 'primary'
            if not part['no_table']:
                part['num'] = disk['realpart']
            else:
                part['num'] = 0

            if disk['ptable_format'] == "msdos":
                if len(self.partitions) > 4:
                    if disk['realpart'] > 3:
                        part['type'] = 'logical'
                        part['num'] = disk['realpart'] + 1

            disk['partitions'].append(num)
            msger.debug("Assigned %s to %s%d, sectors range %d-%d size %d "
                        "sectors (%d bytes)." \
                            % (part['mountpoint'], part['disk_name'], part['num'],
                               part['start'], part['start'] + part['size'] - 1,
                               part['size'], part['size'] * self.sector_size))

        # Once all the partitions have been layed out, we can calculate the
        # minumim disk sizes.
        for disk in self.disks.values():
            disk['min_size'] = disk['offset']
            if disk['ptable_format'] == "gpt":
                disk['min_size'] += GPT_OVERHEAD

            disk['min_size'] *= self.sector_size

    def __create_partition(self, device, parttype, fstype, start, size):
        """ Create a partition on an image described by the 'device' object. """

        # Start is included to the size so we need to substract one from the end.
        end = start + size - 1
        msger.debug("Added '%s' partition, sectors %d-%d, size %d sectors" %
                    (parttype, start, end, size))

        cmd = "parted -s %s unit s mkpart %s" % (device, parttype)
        if fstype:
            cmd += " %s" % fstype
        cmd += " %d %d" % (start, end)

        return exec_native_cmd(cmd, self.native_sysroot)

    def __format_disks(self):
        self.layout_partitions()

        for dev in self.disks:
            disk = self.disks[dev]
            msger.debug("Initializing partition table for %s" % \
                        (disk['disk'].device))
            exec_native_cmd("parted -s %s mklabel %s" % \
                            (disk['disk'].device, disk['ptable_format']),
                            self.native_sysroot)

            if disk['identifier']:
                msger.debug("Set disk identifier %x" % disk['identifier'])
                with open(disk['disk'].device, 'r+b') as img:
                    img.seek(0x1B8)
                    img.write(disk['identifier'].to_bytes(4, 'little'))

        msger.debug("Creating partitions")

        for part in self.partitions:
            if part['num'] == 0:
                continue

            disk = self.disks[part['disk_name']]
            if disk['ptable_format'] == "msdos" and part['num'] == 5:
                # Create an extended partition (note: extended
                # partition is described in MBR and contains all
                # logical partitions). The logical partitions save a
                # sector for an EBR just before the start of a
                # partition. The extended partition must start one
                # sector before the start of the first logical
                # partition. This way the first EBR is inside of the
                # extended partition. Since the extended partitions
                # starts a sector before the first logical partition,
                # add a sector at the back, so that there is enough
                # room for all logical partitions.
                self.__create_partition(disk['disk'].device, "extended",
                                        None, part['start'] - 1,
                                        disk['offset'] - part['start'] + 1)

            if part['fstype'] == "swap":
                parted_fs_type = "linux-swap"
            elif part['fstype'] == "vfat":
                parted_fs_type = "fat32"
            elif part['fstype'] == "msdos":
                parted_fs_type = "fat16"
            elif part['fstype'] == "ontrackdm6aux3":
                parted_fs_type = "ontrackdm6aux3"
            else:
                # Type for ext2/ext3/ext4/btrfs
                parted_fs_type = "ext2"

            # Boot ROM of OMAP boards require vfat boot partition to have an
            # even number of sectors.
            if part['mountpoint'] == "/boot" and part['fstype'] in ["vfat", "msdos"] \
               and part['size'] % 2:
                msger.debug("Subtracting one sector from '%s' partition to " \
                            "get even number of sectors for the partition" % \
                            part['mountpoint'])
                part['size'] -= 1

            self.__create_partition(disk['disk'].device, part['type'],
                                    parted_fs_type, part['start'], part['size'])

            if part['part_type']:
                msger.debug("partition %d: set type UID to %s" % \
                            (part['num'], part['part_type']))
                exec_native_cmd("sgdisk --typecode=%d:%s %s" % \
                                         (part['num'], part['part_type'],
                                          disk['disk'].device), self.native_sysroot)

            if part['uuid'] and disk['ptable_format'] == "gpt":
                msger.debug("partition %d: set UUID to %s" % \
                            (part['num'], part['uuid']))
                exec_native_cmd("sgdisk --partition-guid=%d:%s %s" % \
                                (part['num'], part['uuid'], disk['disk'].device),
                                self.native_sysroot)

            if part['boot']:
                flag_name = "legacy_boot" if disk['ptable_format'] == 'gpt' else "boot"
                msger.debug("Set '%s' flag for partition '%s' on disk '%s'" % \
                            (flag_name, part['num'], disk['disk'].device))
                exec_native_cmd("parted -s %s set %d %s on" % \
                                (disk['disk'].device, part['num'], flag_name),
                                self.native_sysroot)
            if part['system_id']:
                exec_native_cmd("sfdisk --part-type %s %s %s" % \
                                (disk['disk'].device, part['num'], part['system_id']),
                                self.native_sysroot)

            # Parted defaults to enabling the lba flag for fat16 partitions,
            # which causes compatibility issues with some firmware (and really
            # isn't necessary).
            if parted_fs_type == "fat16":
                if disk['ptable_format'] == 'msdos':
                    msger.debug("Disable 'lba' flag for partition '%s' on disk '%s'" % \
                                (part['num'], disk['disk'].device))
                    exec_native_cmd("parted -s %s set %d lba off" % \
                                    (disk['disk'].device, part['num']),
                                    self.native_sysroot)

    def cleanup(self):
        if self.disks:
            for dev in self.disks:
                disk = self.disks[dev]
                try:
                    disk['disk'].cleanup()
                except:
                    pass
        # remove partition images
        for image in self.partimages:
            if os.path.isfile(image):
                os.remove(image)

    def assemble(self, image_file):
        msger.debug("Installing partitions")

        for part in self.partitions:
            source = part['source_file']
            if source:
                # install source_file contents into a partition
                sparse_copy(source, image_file, part['start'] * self.sector_size)

                msger.debug("Installed %s in partition %d, sectors %d-%d, "
                            "size %d sectors" % \
                            (source, part['num'], part['start'],
                             part['start'] + part['size'] - 1, part['size']))

                partimage = image_file + '.p%d' % part['num']
                os.rename(source, partimage)
                self.partimages.append(partimage)

    def create(self):
        for dev in self.disks:
            disk = self.disks[dev]
            disk['disk'].create()

        self.__format_disks()

        return
