from __future__ import annotations

import pathlib
import typing
from typing import Any, Optional, Protocol, Union


class TestFailure(AssertionError): ...
    
# verb-based protocols define functions called at specific points in an experiment workflow
# so that their order is fixed: initialize() | start() | verify() | stop() | finalize()

@typing.runtime_checkable
class Initializable(Protocol):
    "Supports `initialize()`: runs setup or configuration to effectively reset the service for fresh use."
    def initialize() -> None: ...
@typing.runtime_checkable
class Configurable(Initializable, Protocol):
    "Supports `ensure_config()`: ensures all required parameters are set before use. Called in `initialize()`."
    def ensure_config() -> None: ...
    
@typing.runtime_checkable
class Testable(Protocol):
    "Supports `test()`: without creating new data, quickly confirms readiness for use, or raises `TestFailure`. Always called before first use. See `PreTestable` for comprehensive test."
    def test() -> None: ...
    
@typing.runtime_checkable
class Pretestable(Protocol):
    "Supports `pretest()`: comprehensively tests service functionality and code by calling every class method critical for use. Should be expected to fail."
    def pretest() -> None: ...
    
@typing.runtime_checkable
class Startable(Protocol):
    "Supports `start()`, `is_ready_to_start()`, `is_started()`, `latest_start`"
    def start() -> None: ...
    "Starts stimulus/recording after checking `self.is_ready_to_start()`."
    def is_ready_to_start() -> bool: ...
    "The body of start() will not execute unless `is_ready_to_start()` returns `True`"
    def is_started() -> bool: ...
    "Prevents service from being re-started once started"  
    latest_start: int
    "Store `time.time()` in each `start()` so we can find files created afterward."
@typing.runtime_checkable
class Primeable(Startable, Protocol): #? PreStartable 
    "Supports `prime()`: makes ready for imminent `start()` by re-arming, running checks, etc. Called before `start()`."
    def prime() -> None: ...
    #? auto-run at beginning of `start()`?
@typing.runtime_checkable
class Verifiable(Startable, Protocol): #? PostStartable 
    "Supports `verify()`: asserts service has started, e.g. stimulus is running, data file is increasing in size etc., or raises `AssertionError`. Called after `start()` and checking `self.is_started()`."
    def verify() -> None: ...
    #? auto-run at end of `start()`?
    
@typing.runtime_checkable
class Stoppable(Protocol):
    "Supports `stop()`: stops or pauses stimulus/recording. Called after `start()`."
    def stop() -> None: ...
@typing.runtime_checkable
class Finalizable(Protocol):
    "Supports `finalize()`: handle results of most-recent `start()` or `stop()`. Cleanup, file management etc."
    def finalize() -> None: ...
    #? if multiple start-stop loops: finalize altogether or individually?
    #? auto-run `finalize()` at end of `stop()`?
    
@typing.runtime_checkable
class Validatable(Protocol):
    "Supports `validate()`: asserts most-recent data are valid, or raises `AssertionError`.  Called after checking `self.is_started() is not True`."
    def validate(data: Optional[pathlib.Path] = None) -> None: ...

@typing.runtime_checkable
class Shutdownable(Protocol):
    "Supports `shutdown()`: gracefully closes service. Called after `finalize()`."
    def shutdown() -> None: ...
    
PreExperimentProtocols = Union[Initializable, Testable, Pretestable, Primeable, Startable]
PostExperimentProtocols = Union[Stoppable, Finalizable, Validatable, Shutdownable]
Service = Union[PreExperimentProtocols, PostExperimentProtocols]

# special methods - should be the only :

@typing.runtime_checkable
class Gettable(Protocol):
    def get(self, property: str) -> Any:
        return self.property
    
@typing.runtime_checkable
class Settable(Protocol):
    def set(self, property: str, value: Any) -> Any:
        if not hasattr(self, property):
            raise AttributeError(f"Service {self} has no property {property}")
        setattr(self, property, value)
    
# noun-based protocols are for more-specific functions/properties than verb-based protocols
# and may be checked at any time.. for example 'Recorder' captures data and therefore
# has a data path in the filesystem

@typing.runtime_checkable
class Recorder(Startable, Protocol):
    data_root: pathlib.Path
    raw_suffix: str # include leading dot

if __name__ == '__main__':
    
    class Test_1:
        _is_started: bool
        def initialize(self):
            self._is_started = False
            print(__class__, 'initialized')
            
        def is_started(self): return self._is_started
        def is_ready_to_start(self): return not self.is_started()
        def start():
            last_started = time.time()
            started = True
            print(__class__, 'started')
            
    class Test_2:
        def start():
            print(__class__, 'started')

    services = [Test_1, Test_2]
    
    def initialize_all():
        for service in services:
            if isinstance(service, Initializable):
                service.initialize()