"""
Motivation:
- Long running threads often need a signal to cancel
- This requires a bunch of boilerplate and can be bug-prone
- This utility wraps a threading.Thread for the thread and threading.Event for the stop signal to
simplify creating such threads
"""

import enum
import threading as th
from typing import Callable, cast

from typing_extensions import ParamSpec, Concatenate

import forcen_public_utils.checked as ch
from forcen_public_utils.sync_value import SyncValue


P = ParamSpec("P")

class MaskState(enum.Enum):
    MASK = enum.auto()
    UNMASK = enum.auto()


class InteractiveThread:
    """
    This class requires the target passed to a threading.Thread instance to take a threading.Event
    instance as its first argument

    Usage:
    
    def loop(stop_requested: threading.Event):
        while not stop_requested.isSet():
            do_some_work(...)
            time.sleep(0.001)

    handle = StoppableThread()
    handle.start(loop)

    # do things

    handle.stop()
    """
    def __init__(self):
        self._thread = None
        self._stop_request = th.Event()
        self._mask_request = None
    
    def start(
        self, 
        target: Callable[Concatenate[th.Event, P], None],
        *args: P.args,
        **kwargs: P.kwargs,
    ) -> ch.Bool:
        if self._thread is not None:
            return ch.bad(message="thread is already started")

        daemon = cast(bool, kwargs.pop('daemon', False))
        self._mask_request = None
        self._stop_request.clear()
        self._thread = th.Thread(target=target, daemon=daemon, args=(self._stop_request,) + args, kwargs=kwargs)
        self._thread.start()
        return ch.ok()

    def start_maskable(
        self, 
        target: Callable[Concatenate[th.Event, SyncValue[MaskState], P], None],
        *args: P.args,
        **kwargs:P.kwargs,
    ) -> ch.Bool:
        if self._thread is not None:
            return ch.bad(message="thread is already started")

        daemon = cast(bool, kwargs.pop('daemon', False))
        self._stop_request.clear()
        self._mask_request = SyncValue[MaskState]()
        self._thread = th.Thread(target=target, daemon=daemon, args=(self._stop_request, self._mask_request) + args, kwargs=kwargs)
        self._thread.start()
        return ch.ok()

    def is_alive(self) -> bool:
        if self._thread is None:
            return False
        return self._thread.is_alive()  

    def stop(self) -> ch.Bool:
        try:                
            self._stop_request.set()
            self._thread.join(timeout=10.0)
            if self._thread.is_alive():
                return ch.bad(message="timed out while waiting for thread to join")
            self._thread = None
            return ch.ok()
        except AttributeError:
            return ch.ok(message="thread was not started or already stopped")

    def mask(self, timeout: float) -> ch.Bool:
        if self._thread is None:
            return ch.bad(message="thread was not started")

        if self._mask_request is None:
            return ch.bad(message=f"thread doesn't support masking")

        if not self._mask_request.sync_send(MaskState.MASK, timeout):
            return ch.bad(message="thread never acknowledged mask request")
            
        return ch.ok()

    def unmask(self, timeout: float) -> ch.Bool:
        if self._thread is None:
            return ch.bad(message="thread was not started")

        if self._mask_request is None:
            return ch.bad(message=f"thread doesn't support masking")

        if not self._mask_request.sync_send(MaskState.UNMASK, timeout):
            return ch.bad(message="thread never acknowledged unmask request")
            
        return ch.ok()

    def is_started(self):
        return self._thread is not None
    