# -*- coding: utf-8 -*-
"""

    @author: Fabio Erculiani <lxnay@sabayon.org>
    @contact: lxnay@sabayon.org
    @copyright: Fabio Erculiani
    @license: GPL-2

    B{Entropy Package Manager Client Miscellaneous Interface}.

"""

import os
import sys
import shutil
import subprocess

from entropy.core.settings.base import SystemSettings
from entropy.const import etpConst, const_convert_to_rawstring, \
    const_convert_to_unicode, const_debug_write
from entropy.output import darkred, darkgreen, brown
from entropy.tools import getstatusoutput, rename_keep_permissions
from entropy.i18n import _

class ConfigurationFiles(dict):

    """
    Configuration Files Updates descriptor.
    Each configuration file update action is described
    as a mapping between the source file and its destination
    target.
    Each key (a string representing a source file) points to
    a dictionary, containing the following items:
        "destination": path to destination file (string)
        "automerge": if source can be automerged to destination (bool)

    This API is process and thread safe with regards to the Installed
    Packages Repository. There is no need to do external locking on it.
    """

    def __init__(self, entropy_client, quiet=False):
        self._quiet = quiet
        self._entropy = entropy_client
        self._settings = SystemSettings()
        dict.__init__(self)
        self._load()

    @property
    def _repository_ids(self):
        """
        Return a the list of repository identifiers the object
        is using.
        """
        inst_repo = self._entropy.installed_repository()
        return [inst_repo.repository_id()]

    @staticmethod
    def root():
        """
        Return the current ROOT ("/") prefix
        """
        return etpConst['systemroot']

    def _get_config_protect(self, mask=False):
        """
        Get CONFIG_PROTECT or CONFIG_PROTECT_MASK values from
        _repository_ids.
        """
        misc_data = self._entropy.ClientSettings()['misc']
        config_protect = set()
        # also ask to Source Package Manager
        spm = self._entropy.Spm()

        if mask:
            config_protect |= misc_data['configprotectmask']
            # also read info from environment and merge here
            config_protect |= set(spm.get_merge_protected_paths_mask())
        else:
            config_protect |= misc_data['configprotect']
            # also read info from environment and merge here
            config_protect |= set(spm.get_merge_protected_paths())

        # get from our repositories
        for repository_id in self._repository_ids:
            # assume that all the repositories need separate locking.
            # this might be true in future.
            repo = self._entropy.open_repository(repository_id)
            with repo.shared():
                if mask:
                    _mask = repo.listConfigProtectEntries(mask = True)
                else:
                    _mask = repo.listConfigProtectEntries()

            config_protect |= set(_mask)

        root = ConfigurationFiles.root()
        config_protect = [root + x for x in config_protect]
        config_protect.sort()
        return config_protect

    def _encode_path(self, path):
        """
        Encode path using proper encoding for use with os functions.
        """
        try:
            path = const_convert_to_rawstring(
                path, from_enctype=etpConst['conf_encoding'])
        except (UnicodeEncodeError,):
            path = const_convert_to_rawstring(
                path, from_enctype=sys.getfilesystemencoding())
        return path

    def _unicode_path(self, path):
        """
        Convert a potentially raw string into a well formed unicode
        one. Usually, this method is called on string that went through
        _encode_path()
        """
        try:
            path = const_convert_to_unicode(
                path, enctype=etpConst['conf_encoding'])
        except (UnicodeDecodeError,):
            path = const_convert_to_unicode(
                path, enctype=sys.getfilesystemencoding())
        return path

    def _strip_root(self, path):
        """
        Strip root prefix from path
        """
        root = ConfigurationFiles.root()
        new_path = path[len(root):]
        return os.path.normpath(new_path)

    def _load_can_automerge(self, source, destination):
        """
        Determine if source file path equals destination file path,
        thus it can be automerged.
        """
        def _vanished():
            # file went away? not really needed, but...
            if not os.path.lexists(source):
                return True
            # broken symlink
            if os.path.islink(source) and not os.path.exists(source):
                return True
            return False

        if _vanished():
            return True

        # first diff test
        try:
            exit_st = getstatusoutput(
                'diff -Nua "%s" "%s" | grep '
                '"^[+-][^+-]" | grep -v \'# .Header:.*\'' % (
                    source, destination,))[1]
        except (OSError, IOError):
            exit_st = 1
        if exit_st == os.EX_OK:
            return True
        elif _vanished():
            return True

        # second diff test
        try:
            exit_st = subprocess.call(
                'diff -Bbua "%s" "%s" | '
                'egrep \'^[+-]\' | '
                'egrep -v \'^[+-][\t ]*#|^--- |^\+\+\+ \' | '
                'egrep -qv \'^[-+][\t ]*$\'' % (
                    source, destination,), shell = True)
        except (IOError, OSError,):
            exit_st = 0
        if exit_st == 1:
            return True

        if _vanished():
            return True
        # requires manual merge
        return False

    def _load_maybe_add(self, currentdir, item, filepath, number):
        """
        Scan given path and store config file update information
        if needed.
        """
        try:
            tofile = item[10:]
            number = item[5:9]
        except IndexError as err:
            const_debug_write(
                __name__, "load_maybe_add, IndexError: "
                "%s, locals: %s" % (
                    repr(err), locals()))
            return

        try:
            int(number)
        except ValueError as err:
            # not a number
            const_debug_write(
                __name__, "load_maybe_add, ValueError: "
                "%s, locals: %s" % (
                    repr(err), locals()))
            return

        tofilepath = os.path.join(currentdir, tofile)
        # tofile is the target filename now
        # before adding, determine if we should automerge it
        if self._load_can_automerge(filepath, tofilepath):
            if not self._quiet:
                self._entropy.output(
                    darkred("%s: %s") % (
                        _("Automerging file"),
                        darkgreen(filepath),
                        ),
                    importance = 0,
                    level = "info"
                )
            try:
                rename_keep_permissions(
                    filepath, tofilepath)
            except OSError as err:
                const_debug_write(
                    __name__, "load_maybe_add, OSError: "
                    "%s, locals: %s" % (
                        repr(err), locals()))
            except IOError as err:
                const_debug_write(
                    __name__, "load_maybe_add, IOError: "
                    "%s, locals: %s" % (
                        repr(err), locals()))
            return

        # store
        save_filepath = self._strip_root(
            self._unicode_path(filepath))
        obj = {
            'destination': self._strip_root(
                self._unicode_path(tofilepath)),
            'automerge': False, # redundant but backward compat
        }
        self[save_filepath] = obj

        if not self._quiet:
            self._entropy.output(
                "%s: %s" % (
                    brown(_("Found update")),
                    self._unicode_path(
                        darkgreen(filepath)),),
                importance = 0,
                level = "info"
            )

    def _load(self):
        """
        Load configuration file updates reading from disk.
        """
        name_cache = set()
        client_conf_protect = self._get_config_protect()
        # NOTE: with Python 3.x we can remove const_convert...
        # and avoid using _encode_path.
        cfg_pfx = const_convert_to_rawstring("._cfg")

        for path in client_conf_protect:
            path = self._encode_path(path)

            # is it a file?
            scanfile = False
            if os.path.isfile(path):
                # find inside basename
                path = os.path.dirname(path)
                scanfile = True

            for currentdir, _subdirs, files in os.walk(path):
                for item in files:
                    if scanfile:
                        if path != item:
                            continue

                    if not item.startswith(cfg_pfx):
                        continue

                    # further check then
                    number = item[5:9]
                    try:
                        int(number)
                    except ValueError:
                        continue # not a valid etc-update file
                    if item[9] != "_": # no valid format provided
                        continue

                    filepath = os.path.join(currentdir, item)
                    if filepath in name_cache:
                        continue # skip, already done
                    name_cache.add(filepath)

                    self._load_maybe_add(
                        currentdir, item, filepath, number)

    def _backup(self, dest_path):
        """
        Execute a backup of the given path if User enabled
        the feature through Entropy Client configuration.
        """
        client_settings = self._entropy.ClientSettings()
        files_backup = client_settings['misc']['filesbackup']
        if not files_backup:
            return

        dest_path = self._encode_path(dest_path)
        if not os.path.isfile(dest_path):
            return

        backup_pfx = self._encode_path("._entropy_backup.")
        sep = self._encode_path("_")
        dirname, basename = os.path.split(dest_path)
        bcount = 0

        bcount_str = self._encode_path("%d" % (bcount,))
        backup_path = os.path.join(
            dirname, backup_pfx + bcount_str + sep + basename)
        while os.path.lexists(backup_path):
            bcount += 1
            bcount_str = self._encode_path("%d" % (bcount,))
            backup_path = os.path.join(
                dirname, backup_pfx + bcount_str + sep + basename)

        # I don't know if copy2 likes bytes()
        # time will tell!
        try:
            shutil.copy2(dest_path, backup_path)
        except OSError as err:
            const_debug_write(
                __name__, "_backup, OSError: "
                "%s, locals: %s" % (
                    repr(err), locals()))
        except IOError as err:
            const_debug_write(
                __name__, "_backup, IOError: "
                "%s, locals: %s" % (
                    repr(err), locals()))

    def remove(self, source):
        """
        Remove proposed source configuration file.
        "source" must be a key of this dictionary, if
        not, True is returned. If file pointed at source
        doesn't exist or removal fails, False is returned.
        """
        obj = self.pop(source, None)
        if obj is None:
            return True

        root = ConfigurationFiles.root()
        source_file = root + source
        source_file = self._encode_path(source_file)
        try:
            os.remove(source_file)
        except OSError as err:
            const_debug_write(
                __name__, "remove, OSError: "
                "%s, locals: %s" % (
                    repr(err), locals()))
            return False
        return True

    def merge(self, source):
        """
        Merge proposed source configuration file.
        "source" must be a key of this dictionary, if
        not, True is returned. If file pointed at source
        doesn't exist or merge fails, False is returned.
        """
        obj = self.pop(source, None)
        if obj is None:
            return True

        root = ConfigurationFiles.root()
        source_file = root + source
        dest_file = root + obj['destination']
        self._backup(dest_file)
        source_file = self._encode_path(source_file)
        dest_file = self._encode_path(dest_file)
        try:
            rename_keep_permissions(
                source_file, dest_file)
        except OSError as err:
            const_debug_write(
                __name__, "merge, OSError: "
                "%s, locals: %s" % (
                    repr(err), locals()))
            return False
        return True

    def exists(self, path):
        """
        Return True if path exists.
        This methods automatically appends the ROOT
        prefix and handles unicode correctly
        """
        root = ConfigurationFiles.root()
        source_file = root + path
        source_file = self._encode_path(source_file)
        return os.path.lexists(source_file)


class ConfigurationUpdates:

    """
    Entropy Configuration File Updates management class.
    """

    def __init__(self, entropy_client, _config_class=None):
        if _config_class is None:
            self._config_class = ConfigurationFiles
        else:
            self._config_class = _config_class
        self._entropy = entropy_client
        self._settings = self._entropy.Settings()

    def get(self, quiet=False):
        """
        Return a new ConfigurationFiles object.
        """
        return self._config_class(self._entropy)
