import sys
from pathlib import Path
from typing import Dict, Optional, Union, Any
import clr_loader

__all__ = ["set_runtime", "set_runtime_from_env", "load"]

_RUNTIME: Optional[clr_loader.Runtime] = None
_LOADER_ASSEMBLY: Optional[clr_loader.Assembly] = None
_LOADED: bool = False


def set_runtime(runtime: Union[clr_loader.Runtime, str], **params: str) -> None:
    """Set up a clr_loader runtime without loading it

    :param runtime: Either an already initialised `clr_loader` runtime, or one
    of netfx, coreclr, mono, or default. If a string parameter is given, the
    runtime will be created."""

    global _RUNTIME
    if _LOADED:
        raise RuntimeError(f"The runtime {_RUNTIME} has already been loaded")

    if isinstance(runtime, str):
        runtime = _create_runtime_from_spec(runtime, params)

    _RUNTIME = runtime


def get_runtime_info() -> Optional[clr_loader.RuntimeInfo]:
    if _RUNTIME is None:
        return None
    else:
        return _RUNTIME.info()


def _get_params_from_env(prefix: str) -> Dict[str, str]:
    from os import environ

    full_prefix = f"PYTHONNET_{prefix.upper()}_"
    len_ = len(full_prefix)

    env_vars = {
        (k[len_:].lower()): v
        for k, v in environ.items()
        if k.upper().startswith(full_prefix)
    }

    return env_vars


def _create_runtime_from_spec(
    spec: str, params: Optional[Dict[str, Any]] = None
) -> clr_loader.Runtime:
    if spec == "default":
        if sys.platform == "win32":
            spec = "netfx"
        else:
            spec = "mono"

    params = params or _get_params_from_env(spec)

    if spec == "netfx":
        return clr_loader.get_netfx(**params)
    elif spec == "mono":
        return clr_loader.get_mono(**params)
    elif spec == "coreclr":
        return clr_loader.get_coreclr(**params)
    else:
        raise RuntimeError(f"Invalid runtime name: '{spec}'")


def set_runtime_from_env() -> None:
    """Set up the runtime using the environment

    This will use the environment variable PYTHONNET_RUNTIME to decide the
    runtime to use, which may be one of netfx, coreclr or mono. The parameters
    of the respective clr_loader.get_<runtime> functions can also be given as
    environment variables, named `PYTHONNET_<RUNTIME>_<PARAM_NAME>`. In
    particular, to use `PYTHONNET_RUNTIME=coreclr`, the variable
    `PYTHONNET_CORECLR_RUNTIME_CONFIG` has to be set to a valid
    `.runtimeconfig.json`.

    If no environment variable is specified, a globally installed Mono is used
    for all environments but Windows, on Windows the legacy .NET Framework is
    used.
    """
    from os import environ

    spec = environ.get("PYTHONNET_RUNTIME", "default")
    runtime = _create_runtime_from_spec(spec)
    set_runtime(runtime)


def load(
    runtime: Union[clr_loader.Runtime, str, None] = None, **params: str
) -> None:
    """Load Python.NET in the specified runtime

    The same parameters as for `set_runtime` can be used. By default,
    `set_default_runtime` is called if no environment has been set yet and no
    parameters are passed."""
    global _LOADED, _LOADER_ASSEMBLY

    if _LOADED:
        return

    if _RUNTIME is None:
        if runtime is None:
            set_runtime_from_env()
        else:
            set_runtime(runtime, **params)

    if _RUNTIME is None:
        raise RuntimeError("No valid runtime selected")

    dll_path = Path(__file__).parent / "runtime" / "Python.Runtime.dll"

    _LOADER_ASSEMBLY = assembly = _RUNTIME.get_assembly(str(dll_path))
    func = assembly.get_function("Python.Runtime.Loader.Initialize")

    if func(b"") != 0:
        raise RuntimeError("Failed to initialize Python.Runtime.dll")

    import atexit

    atexit.register(unload)


def unload() -> None:
    """Explicitly unload a laoded runtime and shut down Python.NET"""

    global _RUNTIME, _LOADER_ASSEMBLY
    if _LOADER_ASSEMBLY is not None:
        func = _LOADER_ASSEMBLY.get_function("Python.Runtime.Loader.Shutdown")
        if func(b"full_shutdown") != 0:
            raise RuntimeError("Failed to call Python.NET shutdown")

        _LOADER_ASSEMBLY = None

    if _RUNTIME is not None:
        _RUNTIME.shutdown()
        _RUNTIME = None

@@ -898,16 +898,74 @@
 eInfo%5D:%0A
+    %22%22%22Retrieve information on the configured runtime%22%22%22%0A%0A
     if _
@@ -973,32 +973,32 @@
 UNTIME is None:%0A
-
         return N
@@ -1475,16 +1475,40 @@
 untime:%0A
+    was_default = False%0A
     if s
@@ -1525,16 +1525,43 @@
 fault%22:%0A
+        was_default = True%0A
         
@@ -1707,16 +1707,29 @@
 (spec)%0A%0A
+    try:%0A    
     if s
@@ -1744,16 +1744,20 @@
 netfx%22:%0A
+    
         
@@ -1786,32 +1786,36 @@
 netfx(**params)%0A
+    
     elif spec ==
@@ -1823,32 +1823,36 @@
 %22mono%22:%0A        
+    
 return clr_loade
@@ -1868,24 +1868,28 @@
 o(**params)%0A
+    
     elif spe
@@ -1904,32 +1904,36 @@
 reclr%22:%0A        
+   
+ 
 return clr_loade
@@ -1952,24 +1952,28 @@
 r(**params)%0A
+    
     else:%0A  
@@ -1962,32 +1962,36 @@
 )%0A        else:%0A
+    
         raise Ru
@@ -2036,16 +2036,647 @@
 pec%7D'%22)%0A
+    except Exception as exc:%0A        if was_default:%0A            raise RuntimeError(%0A                f%22%22%22Failed to create a default .NET runtime, which would%0A                    have been %22%7Bspec%7D%22 on this system. Either install a%0A                    compatible runtime or configure it explicitly via%0A                    %60set_runtime%60 or the %60PYTHONNET_*%60 environment variables%0A                    (see set_runtime_from_env).%22%22%22%0A            ) from exc%0A        else:%0A            raise RuntimeError(%0A                f%22%22%22Failed to create a .NET runtime (%7Bspec%7D) using the%0A                parameters %7Bparams%7D.%22%22%22%0A            ) from exc%0A
 %0A%0Adef se
@@ -3555,21 +3555,16 @@
 ef load(
-%0A    
 runtime:
@@ -3622,17 +3622,16 @@
 ams: str
-%0A
 ) -%3E Non

Improve error message if the runtime fails to load