#!/usr/bin/env python3
"""hos-delta delta module: Support for update deltas

This module provides support for handling update deltas.
"""

import gzip
import json
import os
import shutil

from hos_delta import utils

class Delta:
    """Class for representing a delta update object.

    Parameters
    ----------
    path : str
        The full filesystem path to the unpacked delta payload.

    Attributes
    ----------
    __fingerprint_path : str
        The file path to the fingerprint file.
    __approx_rootfs_size : int
        The size of the root filesystem for which this delta was generated. It
        doesn't include the fingerprint.
    __entries : dict
        A dictionary where each key is a filesystem entry and each value is a
        dict which describes the respective filesystem entry.
    __version : str
        The HanoverOS version for which this delta was generated.
    __payload : str
        The full filesystem path to the unpacked delta payload.

    """
    def __init__(self, path):
        self.__fingerprint_path = utils.search_file(path, 'fingerprint',
                                                    ['json', 'json.gz'])

        # Parse the delta fingerprint file
        with open(self.__fingerprint_path, 'rb') as f_json:
            gzipped = f_json.read(2) == utils.GZIP_MAGIC_NUMBER
        if gzipped:
            with gzip.GzipFile(self.__fingerprint_path, 'r') as f_json:
                fingerprint_bytes = f_json.read()
            fingerprint_str = fingerprint_bytes.decode('utf-8')
            fingerprint = json.loads(fingerprint_str)
        else:
            with open(self.__fingerprint_path, 'r') as f_json:
                fingerprint = json.load(f_json)

        self.__approx_rootfs_size = int(fingerprint['approx_rootfs_size'])
        self.__entries = fingerprint['entries']
        self.__version = fingerprint['hanoveros_version']
        self.__payload = path

    @property
    def entries(self):
        """dict: filesystem entries"""
        return self.__entries

    @property
    def version(self):
        """str: delta version"""
        return self.__version

    def apply(self, active, inactive, log):
        """Apply an update delta.

        The method applies an update delta by reconstructing the root
        filesystem using the active partition and the delta payload.

        Parameters
        ----------
        active : str
            Path to the active root filesystem mountpoint.
        inactive : str
            Path to the inactive root filesystem mountpoint. Be aware that the
            content will be cleaned before applying the delta.
        log : object
            A logging object.

        Returns
        -------
        None

        Raises
        ------
        ValueError
            Invalid arguments for active or inactive mount points.
        NotImplementedError
            Found a filesystem entry which the tool doesn't know how to handle.
        ImportError
            Can't apply a regular file entry as the blob is not availble in the
            delta payload nor on the active partition.

        """
        if not os.path.isdir(active) or not os.path.isdir(inactive):
            raise ValueError("active or inactive mount points can't be accessed")

        log.debug("Cleaning up inactive mount point...")
        utils.clean_dir(inactive)

        log.info("Applying delta to %s for version %s...", inactive, self.version)
        for entry in self.entries:
            entry_type = self.entries[entry]['type']
            if entry_type == 'd':
                # Directory
                path = os.path.join(inactive, entry)
                if not os.path.isdir(path):
                    os.makedirs(path)
            elif entry_type == 'l':
                # Symlink
                path = os.path.join(inactive, entry)
                target = self.entries[entry]['l_target']
                if not os.path.isdir(os.path.dirname(path)):
                    os.makedirs(os.path.dirname(path))
                os.symlink(target, path)
            elif entry_type == 'f':
                # Regular file
                path = os.path.join(inactive, entry)
                entry_hash = self.entries[entry]['f_hash']
                entry_payload_path = os.path.join(self.__payload, entry_hash[0:2], entry_hash)
                entry_active_path = os.path.join(active, entry)
                if not os.path.isdir(os.path.dirname(path)):
                    os.makedirs(os.path.dirname(path))
                if not os.path.islink(entry_payload_path) and os.path.isfile(entry_payload_path):
                    shutil.copyfile(entry_payload_path, path)
                elif not os.path.islink(entry_active_path) and os.path.isfile(entry_active_path):
                    shutil.copyfile(entry_active_path, path)
                else:
                    raise ImportError("can't apply %s" % entry)
            else:
                raise NotImplementedError

            # Set entry metadata
            self.__apply_metadata(entry, path)

        self.__deploy_fingerprint(inactive)

    def __apply_metadata(self, entry, path):
        mode = int(self.entries[entry]['perm'], 8)
        uid = self.entries[entry]['uid']
        gid = self.entries[entry]['gid']
        if not os.path.islink(path):
            os.chmod(path, mode)
        os.chown(path, uid, gid, follow_symlinks=False)

    def __deploy_fingerprint(self, path):
        """Deploy the fingerprint.

        The method copies the fingerprint to a specific path appying some
        default file meta data.

        Parameters
        ----------
        path : str
            Path to the root filesystem mountpoint where the method is to
            deploy the fingerprint.  inactive : str

        Returns
        -------
        None
        """
        shutil.copy(self.__fingerprint_path, path)
        fingerprint_filename = os.path.basename(self.__fingerprint_path)
        os.chmod(os.path.join(path, fingerprint_filename), 0o644)
        os.chown(os.path.join(path, fingerprint_filename), 0, 0)
