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

import functools

from _emerge.AsynchronousLock import AsynchronousLock

import portage
from portage import os
from portage.exception import PortageException
from portage.util.SlotObject import SlotObject


class EbuildBuildDir(SlotObject):
    __slots__ = ("scheduler", "settings", "locked", "_catdir", "_lock_obj")

    def __init__(self, **kwargs):
        SlotObject.__init__(self, **kwargs)
        self.locked = False

    def _assert_lock(self, async_lock):
        if async_lock.returncode != os.EX_OK:
            # TODO: create a better way to propagate this error to the caller
            raise AssertionError(
                f"AsynchronousLock failed with returncode {async_lock.returncode}"
            )

    def clean_log(self):
        """Discard existing log. The log will not be be discarded
        in cases when it would not make sense, like when FEATURES=keepwork
        is enabled."""
        settings = self.settings
        if "keepwork" in settings.features:
            return
        log_file = settings.get("PORTAGE_LOG_FILE")
        if log_file is not None and os.path.isfile(log_file):
            try:
                os.unlink(log_file)
            except OSError:
                pass

    def async_lock(self):
        """
        Acquire the lock asynchronously. Notification is available
        via the add_done_callback method of the returned Future instance.

        This raises an AlreadyLocked exception if async_lock() is called
        while a lock is already held. In order to avoid this, call
        async_unlock() or check whether the "locked" attribute is True
        or False before calling async_lock().

        @returns: Future, result is None
        """
        if self._lock_obj is not None:
            raise self.AlreadyLocked((self._lock_obj,))

        dir_path = self.settings.get("PORTAGE_BUILDDIR")
        if not dir_path:
            raise AssertionError("PORTAGE_BUILDDIR is unset")
        catdir = os.path.dirname(dir_path)
        self._catdir = catdir
        catdir_lock = AsynchronousLock(path=catdir, scheduler=self.scheduler)
        builddir_lock = AsynchronousLock(path=dir_path, scheduler=self.scheduler)
        result = self.scheduler.create_future()

        def catdir_locked(catdir_lock):
            try:
                self._assert_lock(catdir_lock)
            except AssertionError as e:
                result.set_exception(e)
                return

            try:
                portage.util.ensure_dirs(
                    catdir, gid=portage.portage_gid, mode=0o70, mask=0
                )
            except PortageException as e:
                if not os.path.isdir(catdir):
                    result.set_exception(e)
                    return

            builddir_lock.addExitListener(builddir_locked)
            builddir_lock.start()

        def builddir_locked(builddir_lock):
            try:
                self._assert_lock(builddir_lock)
            except AssertionError as e:
                catdir_lock.async_unlock.add_done_callback(
                    functools.partial(catdir_unlocked, exception=e)
                )
                return

            self._lock_obj = builddir_lock
            self.locked = True
            self.settings["PORTAGE_BUILDDIR_LOCKED"] = "1"
            catdir_lock.async_unlock().add_done_callback(catdir_unlocked)

        def catdir_unlocked(future, exception=None):
            if not (exception is None and future.exception() is None):
                result.set_exception(exception or future.exception())
            else:
                result.set_result(None)

        try:
            portage.util.ensure_dirs(
                os.path.dirname(catdir), gid=portage.portage_gid, mode=0o70, mask=0
            )
        except PortageException:
            if not os.path.isdir(os.path.dirname(catdir)):
                raise

        catdir_lock.addExitListener(catdir_locked)
        catdir_lock.start()
        return result

    def async_unlock(self):
        """
        Release the lock asynchronously. Release notification is available
        via the add_done_callback method of the returned Future instance.

        @returns: Future, result is None
        """
        result = self.scheduler.create_future()

        def builddir_unlocked(future):
            if future.exception() is not None:
                result.set_exception(future.exception())
            else:
                self._lock_obj = None
                self.locked = False
                self.settings.pop("PORTAGE_BUILDDIR_LOCKED", None)
                catdir_lock = AsynchronousLock(
                    path=self._catdir, scheduler=self.scheduler
                )
                catdir_lock.addExitListener(catdir_locked)
                catdir_lock.start()

        def catdir_locked(catdir_lock):
            if catdir_lock.wait() != os.EX_OK:
                result.set_result(None)
            else:
                try:
                    os.rmdir(self._catdir)
                except OSError:
                    pass
                catdir_lock.async_unlock().add_done_callback(catdir_unlocked)

        def catdir_unlocked(future):
            if future.exception() is None:
                result.set_result(None)
            else:
                result.set_exception(future.exception())

        if self._lock_obj is None:
            self.scheduler.call_soon(result.set_result, None)
        else:
            self._lock_obj.async_unlock().add_done_callback(builddir_unlocked)
        return result

    class AlreadyLocked(portage.exception.PortageException):
        pass
