# Copyright 1999-2020 Gentoo Authors
# Distributed under the terms of the GNU General Public License v2

import errno
from portage.util import writemsg
from portage.data import secpass
import portage
from portage import os
import pickle


class BlockerCache(portage.cache.mappings.MutableMapping):
    """This caches blockers of installed packages so that dep_check does not
    have to be done for every single installed package on every invocation of
    emerge.  The cache is invalidated whenever it is detected that something
    has changed that might alter the results of dep_check() calls:
            1) the set of installed packages (including COUNTER) has changed
    """

    # Number of uncached packages to trigger cache update, since
    # it's wasteful to update it for every vdb change.
    _cache_threshold = 5

    class BlockerData:
        __slots__ = ("__weakref__", "atoms", "counter")

        def __init__(self, counter, atoms):
            self.counter = counter
            self.atoms = atoms

    def __init__(self, myroot, vardb):
        """myroot is ignored in favour of EROOT"""
        self._vardb = vardb
        self._cache_filename = os.path.join(
            vardb.settings["EROOT"], portage.CACHE_PATH, "vdb_blockers.pickle"
        )
        self._cache_version = "1"
        self._cache_data = None
        self._modified = set()
        self._load()

    def _load(self):
        try:
            f = open(self._cache_filename, mode="rb")
            mypickle = pickle.Unpickler(f)
            try:
                mypickle.find_global = None
            except AttributeError:
                # TODO: If py3k, override Unpickler.find_class().
                pass
            self._cache_data = mypickle.load()
            f.close()
            del f
        except (SystemExit, KeyboardInterrupt):
            raise
        except Exception as e:
            if isinstance(e, EnvironmentError) and getattr(e, "errno", None) in (
                errno.ENOENT,
                errno.EACCES,
            ):
                pass
            else:
                writemsg(
                    f"!!! Error loading '{self._cache_filename}': {str(e)}\n",
                    noiselevel=-1,
                )
            del e

        cache_valid = (
            self._cache_data
            and isinstance(self._cache_data, dict)
            and self._cache_data.get("version") == self._cache_version
            and isinstance(self._cache_data.get("blockers"), dict)
        )
        if cache_valid:
            # Validate all the atoms and counters so that
            # corruption is detected as soon as possible.
            invalid_items = set()
            for k, v in self._cache_data["blockers"].items():
                if not isinstance(k, str):
                    invalid_items.add(k)
                    continue
                try:
                    if portage.catpkgsplit(k) is None:
                        invalid_items.add(k)
                        continue
                except portage.exception.InvalidData:
                    invalid_items.add(k)
                    continue
                if not isinstance(v, tuple) or len(v) != 2:
                    invalid_items.add(k)
                    continue
                counter, atoms = v
                if not isinstance(counter, int):
                    invalid_items.add(k)
                    continue
                if not isinstance(atoms, (list, tuple)):
                    invalid_items.add(k)
                    continue
                invalid_atom = False
                for atom in atoms:
                    if not isinstance(atom, str):
                        invalid_atom = True
                        break
                    if atom[:1] != "!" or not portage.isvalidatom(
                        atom, allow_blockers=True
                    ):
                        invalid_atom = True
                        break
                if invalid_atom:
                    invalid_items.add(k)
                    continue

            for k in invalid_items:
                del self._cache_data["blockers"][k]
            if not self._cache_data["blockers"]:
                cache_valid = False

        if not cache_valid:
            self._cache_data = {"version": self._cache_version}
            self._cache_data["blockers"] = {}
        self._modified.clear()

    def flush(self):
        """If the current user has permission and the internal blocker cache has
        been updated, save it to disk and mark it unmodified.  This is called
        by emerge after it has processed blockers for all installed packages.
        Currently, the cache is only written if the user has superuser
        privileges (since that's required to obtain a lock), but all users
        have read access and benefit from faster blocker lookups (as long as
        the entire cache is still valid).  The cache is stored as a pickled
        dict object with the following format:

        {
                version : "1",
                "blockers" : {cpv1:(counter,(atom1, atom2...)), cpv2...},
        }
        """
        if len(self._modified) >= self._cache_threshold and secpass >= 2:
            try:
                with portage.util.atomic_ofstream(self._cache_filename, mode="wb") as f:
                    pickle.dump(self._cache_data, f, protocol=2)

                portage.util.apply_secpass_permissions(
                    self._cache_filename, gid=portage.portage_gid, mode=0o644
                )
            except OSError:
                pass
            self._modified.clear()

    def __setitem__(self, cpv, blocker_data):
        """
        Update the cache and mark it as modified for a future call to
        self.flush().

        @param cpv: Package for which to cache blockers.
        @type cpv: String
        @param blocker_data: An object with counter and atoms attributes.
        @type blocker_data: BlockerData
        """
        self._cache_data["blockers"][str(cpv)] = (
            blocker_data.counter,
            tuple(str(x) for x in blocker_data.atoms),
        )
        self._modified.add(cpv)

    def __iter__(self):
        if self._cache_data is None:
            # triggered by python-trace
            return iter([])
        return iter(self._cache_data["blockers"])

    def __len__(self):
        """This needs to be implemented in order to avoid
        infinite recursion in some cases."""
        return len(self._cache_data["blockers"])

    def __delitem__(self, cpv):
        del self._cache_data["blockers"][cpv]

    def __getitem__(self, cpv):
        """
        @rtype: BlockerData
        @return: An object with counter and atoms attributes.
        """
        return self.BlockerData(*self._cache_data["blockers"][cpv])
