from __future__ import with_statement

import signal as cpy_signal
import sys
import os
import errno

from pypy.interpreter.error import OperationError, exception_from_saved_errno
from pypy.interpreter.executioncontext import (AsyncAction, AbstractActionFlag,
    PeriodicAsyncAction)
from pypy.interpreter.gateway import unwrap_spec

from rpython.rlib import jit, rposix, rgc
from rpython.rlib.objectmodel import we_are_translated
from rpython.rlib.rarithmetic import intmask
from rpython.rlib.rsignal import *
from rpython.rtyper.lltypesystem import lltype, rffi


WIN32 = sys.platform == 'win32'


class SignalActionFlag(AbstractActionFlag):
    # This class uses the C-level pypysig_counter variable as the tick
    # counter.  The C-level signal handler will reset it to -1 whenever
    # a signal is received.  This causes CheckSignalAction.perform() to
    # be called.

    def get_ticker(self):
        p = pypysig_getaddr_occurred()
        return p.c_value

    def reset_ticker(self, value):
        p = pypysig_getaddr_occurred()
        p.c_value = value

    def rearm_ticker(self):
        p = pypysig_getaddr_occurred()
        p.c_value = -1

    def decrement_ticker(self, by):
        p = pypysig_getaddr_occurred()
        value = p.c_value
        if self.has_bytecode_counter:    # this 'if' is constant-folded
            if jit.isconstant(by) and by == 0:
                pass     # normally constant-folded too
            else:
                value -= by
                p.c_value = value
        return value


class CheckSignalAction(PeriodicAsyncAction):
    """An action that is automatically invoked when a signal is received."""

    # Note that this is a PeriodicAsyncAction: it means more precisely
    # that it is called whenever the C-level ticker becomes < 0.
    # Without threads, it is only ever set to -1 when we receive a
    # signal.  With threads, it also decrements steadily (but slowly).

    def __init__(self, space):
        "NOT_RPYTHON"
        AsyncAction.__init__(self, space)
        self.pending_signal = -1
        self.fire_in_another_thread = False
        #
        @rgc.no_collect
        def _after_thread_switch():
            if self.fire_in_another_thread:
                if self.space.threadlocals.signals_enabled():
                    self.fire_in_another_thread = False
                    self.space.actionflag.rearm_ticker()
                    # this occurs when we just switched to the main thread
                    # and there is a signal pending: we force the ticker to
                    # -1, which should ensure perform() is called quickly.
        self._after_thread_switch = _after_thread_switch
        # ^^^ so that 'self._after_thread_switch' can be annotated as a
        # constant

    def startup(self, space):
        # this is translated
        if space.config.objspace.usemodules.thread:
            from rpython.rlib import rgil
            rgil.invoke_after_thread_switch(self._after_thread_switch)

    def perform(self, executioncontext, frame):
        self._poll_for_signals()

    @jit.dont_look_inside
    def _poll_for_signals(self):
        # Poll for the next signal, if any
        n = self.pending_signal
        if n < 0:
            n = pypysig_poll()
        while n >= 0:
            if self.space.threadlocals.signals_enabled():
                # If we are in the main thread, report the signal now,
                # and poll more
                self.pending_signal = -1
                report_signal(self.space, n)
                n = self.pending_signal
                if n < 0:
                    n = pypysig_poll()
            else:
                # Otherwise, arrange for perform() to be called again
                # after we switch to the main thread.
                self.pending_signal = n
                self.fire_in_another_thread = True
                break

    def set_interrupt(self):
        "Simulates the effect of a SIGINT signal arriving"
        if not we_are_translated():
            self.pending_signal = cpy_signal.SIGINT
            # ^^^ may override another signal, but it's just for testing
            self.fire_in_another_thread = True
        else:
            pypysig_pushback(cpy_signal.SIGINT)

# ____________________________________________________________


class Handlers:
    def __init__(self, space):
        self.handlers_w = {}
        for signum in range(1, NSIG):
            if WIN32 and signum not in signal_values:
                self.handlers_w[signum] = space.w_None
            else:
                self.handlers_w[signum] = space.wrap(SIG_DFL)

def _get_handlers(space):
    return space.fromcache(Handlers).handlers_w


def report_signal(space, n):
    handlers_w = _get_handlers(space)
    try:
        w_handler = handlers_w[n]
    except KeyError:
        return    # no handler, ignore signal
    if not space.is_true(space.callable(w_handler)):
        return    # w_handler is SIG_IGN or SIG_DFL?
    # re-install signal handler, for OSes that clear it
    pypysig_reinstall(n)
    # invoke the app-level handler
    ec = space.getexecutioncontext()
    w_frame = space.wrap(ec.gettopframe_nohidden())
    space.call_function(w_handler, space.wrap(n), w_frame)


@unwrap_spec(signum=int)
def getsignal(space, signum):
    """
    getsignal(sig) -> action

    Return the current action for the given signal.  The return value can be:
    SIG_IGN -- if the signal is being ignored
    SIG_DFL -- if the default action for the signal is in effect
    None -- if an unknown handler is in effect
    anything else -- the callable Python object used as a handler
    """
    check_signum_in_range(space, signum)
    handlers_w = _get_handlers(space)
    return handlers_w[signum]


def default_int_handler(space, w_signum, w_frame):
    """
    default_int_handler(...)

    The default handler for SIGINT installed by Python.
    It raises KeyboardInterrupt.
    """
    raise OperationError(space.w_KeyboardInterrupt,
                         space.w_None)


@jit.dont_look_inside
@unwrap_spec(timeout=int)
def alarm(space, timeout):
    return space.wrap(c_alarm(timeout))


@jit.dont_look_inside
def pause(space):
    c_pause()
    return space.w_None


def check_signum_in_range(space, signum):
    if 1 <= signum < NSIG:
        return
    raise OperationError(space.w_ValueError,
                         space.wrap("signal number out of range"))


@jit.dont_look_inside
@unwrap_spec(signum=int)
def signal(space, signum, w_handler):
    """
    signal(sig, action) -> action

    Set the action for the given signal.  The action can be SIG_DFL,
    SIG_IGN, or a callable Python object.  The previous action is
    returned.  See getsignal() for possible return values.

    *** IMPORTANT NOTICE ***
    A signal handler function is called with two arguments:
    the first is the signal number, the second is the interrupted stack frame.
    """
    if WIN32 and signum not in signal_values:
        raise OperationError(space.w_ValueError,
                             space.wrap("invalid signal value"))
    if not space.threadlocals.signals_enabled():
        raise OperationError(space.w_ValueError,
                             space.wrap("signal only works in main thread "
                                 "or with __pypy__.thread.enable_signals()"))
    check_signum_in_range(space, signum)

    if space.eq_w(w_handler, space.wrap(SIG_DFL)):
        pypysig_default(signum)
    elif space.eq_w(w_handler, space.wrap(SIG_IGN)):
        pypysig_ignore(signum)
    else:
        if not space.is_true(space.callable(w_handler)):
            raise OperationError(space.w_TypeError,
                                 space.wrap("'handler' must be a callable "
                                            "or SIG_DFL or SIG_IGN"))
        pypysig_setflag(signum)

    handlers_w = _get_handlers(space)
    old_handler = handlers_w[signum]
    handlers_w[signum] = w_handler
    return old_handler


@jit.dont_look_inside
@unwrap_spec(fd=int)
def set_wakeup_fd(space, fd):
    """Sets the fd to be written to (with '\0') when a signal
    comes in.  Returns the old fd.  A library can use this to
    wakeup select or poll.  The previous fd is returned.

    The fd must be non-blocking.
    """
    if not space.threadlocals.signals_enabled():
        raise OperationError(
            space.w_ValueError,
            space.wrap("set_wakeup_fd only works in main thread "
                       "or with __pypy__.thread.enable_signals()"))
    if fd != -1:
        try:
            os.fstat(fd)
        except OSError, e:
            if e.errno == errno.EBADF:
                raise OperationError(space.w_ValueError, space.wrap("invalid fd"))
    old_fd = pypysig_set_wakeup_fd(fd)
    return space.wrap(intmask(old_fd))


@jit.dont_look_inside
@unwrap_spec(signum=int, flag=int)
def siginterrupt(space, signum, flag):
    check_signum_in_range(space, signum)
    if rffi.cast(lltype.Signed, c_siginterrupt(signum, flag)) < 0:
        errno = rposix.get_saved_errno()
        raise OperationError(space.w_RuntimeError, space.wrap(errno))


#__________________________________________________________

def timeval_from_double(d, timeval):
    rffi.setintfield(timeval, 'c_tv_sec', int(d))
    rffi.setintfield(timeval, 'c_tv_usec', int((d - int(d)) * 1000000))


def double_from_timeval(tv):
    return rffi.getintfield(tv, 'c_tv_sec') + (
        rffi.getintfield(tv, 'c_tv_usec') / 1000000.0)


def itimer_retval(space, val):
    w_value = space.wrap(double_from_timeval(val.c_it_value))
    w_interval = space.wrap(double_from_timeval(val.c_it_interval))
    return space.newtuple([w_value, w_interval])


class Cache:
    def __init__(self, space):
        self.w_itimererror = space.new_exception_class("signal.ItimerError",
                                                       space.w_IOError)


def get_itimer_error(space):
    return space.fromcache(Cache).w_itimererror


@jit.dont_look_inside
@unwrap_spec(which=int, first=float, interval=float)
def setitimer(space, which, first, interval=0):
    """setitimer(which, seconds[, interval])
    Sets given itimer (one of ITIMER_REAL, ITIMER_VIRTUAL

    or ITIMER_PROF) to fire after value seconds and after
    that every interval seconds.
    The itimer can be cleared by setting seconds to zero.

    Returns old values as a tuple: (delay, interval).
    """
    with lltype.scoped_alloc(itimervalP.TO, 1) as new:

        timeval_from_double(first, new[0].c_it_value)
        timeval_from_double(interval, new[0].c_it_interval)

        with lltype.scoped_alloc(itimervalP.TO, 1) as old:

            ret = c_setitimer(which, new, old)
            if ret != 0:
                raise exception_from_saved_errno(space, get_itimer_error(space))

            return itimer_retval(space, old[0])


@jit.dont_look_inside
@unwrap_spec(which=int)
def getitimer(space, which):
    """getitimer(which)

    Returns current value of given itimer.
    """
    with lltype.scoped_alloc(itimervalP.TO, 1) as old:

        c_getitimer(which, old)

        return itimer_retval(space, old[0])
