# Copyright 1999-2012 Gentoo Foundation
# Distributed under the terms of the GNU General Public License v2

import functools
import logging
import os

import portage
from portage.dbapi._MergeProcess import MergeProcess
from portage.exception import UnsupportedAPIException
from portage.util._async.AsyncTaskFuture import AsyncTaskFuture

from _emerge.CompositeTask import CompositeTask
from _emerge.EbuildBuildDir import EbuildBuildDir
from _emerge.emergelog import emergelog
from _emerge.unmerge import _unmerge_display


class PackageUninstall(CompositeTask):
    """
    Uninstall a package asynchronously in a subprocess. When
    both parallel-install and ebuild-locks FEATURES are enabled,
    it is essential for the ebuild-locks code to execute in a
    subprocess, since the portage.locks module does not behave
    as desired if we try to lock the same file multiple times
    concurrently from the same process for ebuild-locks phases
    such as pkg_setup, pkg_prerm, and pkg_postrm.
    """

    __slots__ = (
        "_builddir_lock",
        "ldpath_mtimes",
        "opts",
        "pkg",
        "settings",
        "world_atom",
    )

    def _start(self):
        vardb = self.pkg.root_config.trees["vartree"].dbapi
        dbdir = vardb.getpath(self.pkg.cpv)
        if not os.path.exists(dbdir):
            # Apparently the package got uninstalled
            # already, so we can safely return early.
            self.returncode = os.EX_OK
            self._async_wait()
            return

        self.settings.setcpv(self.pkg)
        cat, pf = portage.catsplit(self.pkg.cpv)
        myebuildpath = os.path.join(dbdir, pf + ".ebuild")

        try:
            portage.doebuild_environment(
                myebuildpath, "prerm", settings=self.settings, db=vardb
            )
        except UnsupportedAPIException:
            # This is safe to ignore since this function is
            # guaranteed to set PORTAGE_BUILDDIR even though
            # it raises UnsupportedAPIException. The error
            # will be logged when it prevents the pkg_prerm
            # and pkg_postrm phases from executing.
            pass

        self._builddir_lock = EbuildBuildDir(
            scheduler=self.scheduler, settings=self.settings
        )
        self._start_task(
            AsyncTaskFuture(future=self._builddir_lock.async_lock()),
            self._start_unmerge,
        )

    def _start_unmerge(self, lock_task):
        self._assert_current(lock_task)
        if lock_task.cancelled:
            self._default_final_exit(lock_task)
            return

        lock_task.future.result()
        portage.prepare_build_dirs(settings=self.settings, cleanup=True)

        # Output only gets logged if it comes after prepare_build_dirs()
        # which initializes PORTAGE_LOG_FILE.
        retval, _ = _unmerge_display(
            self.pkg.root_config,
            self.opts,
            "unmerge",
            [self.pkg.cpv],
            clean_delay=0,
            writemsg_level=self._writemsg_level,
        )

        if retval != os.EX_OK:
            self._async_unlock_builddir(returncode=retval)
            return

        self._writemsg_level(f">>> Unmerging {self.pkg.cpv}...\n", noiselevel=-1)
        self._emergelog(f"=== Unmerging... ({self.pkg.cpv})")

        cat, pf = portage.catsplit(self.pkg.cpv)
        unmerge_task = MergeProcess(
            mycat=cat,
            mypkg=pf,
            settings=self.settings,
            treetype="vartree",
            vartree=self.pkg.root_config.trees["vartree"],
            scheduler=self.scheduler,
            background=self.background,
            mydbapi=self.pkg.root_config.trees["vartree"].dbapi,
            prev_mtimes=self.ldpath_mtimes,
            logfile=self.settings.get("PORTAGE_LOG_FILE"),
            unmerge=True,
        )

        self._start_task(unmerge_task, self._unmerge_exit)

    def _unmerge_exit(self, unmerge_task):
        if self._final_exit(unmerge_task) != os.EX_OK:
            self._emergelog(f" !!! unmerge FAILURE: {self.pkg.cpv}")
        else:
            self._emergelog(f" >>> unmerge success: {self.pkg.cpv}")
            self.world_atom(self.pkg)
        self._async_unlock_builddir(returncode=self.returncode)

    def _async_unlock_builddir(self, returncode=None):
        """
        Release the lock asynchronously, and if a returncode parameter
        is given then set self.returncode and notify exit listeners.
        """
        if returncode is not None:
            # The returncode will be set after unlock is complete.
            self.returncode = None
        self._start_task(
            AsyncTaskFuture(future=self._builddir_lock.async_unlock()),
            functools.partial(self._unlock_builddir_exit, returncode=returncode),
        )

    def _unlock_builddir_exit(self, unlock_task, returncode=None):
        self._assert_current(unlock_task)
        if unlock_task.cancelled and returncode is not None:
            self._default_final_exit(unlock_task)
            return

        # Normally, async_unlock should not raise an exception here.
        unlock_task.future.cancelled() or unlock_task.future.result()
        if returncode is not None:
            self.returncode = returncode
            self._async_wait()

    def _emergelog(self, msg):
        emergelog("notitles" not in self.settings.features, msg)

    def _writemsg_level(self, msg, level=0, noiselevel=0):
        log_path = self.settings.get("PORTAGE_LOG_FILE")
        background = self.background

        if log_path is None:
            if not (background and level < logging.WARNING):
                portage.util.writemsg_level(msg, level=level, noiselevel=noiselevel)
        else:
            self.scheduler.output(
                msg, log_path=log_path, level=level, noiselevel=noiselevel
            )
