#               _         _      _               ____   _  _        ___      _   ___
#   ___   __ _ | |  __ _ | |__  (_) _ __ ___    |___ \ | || |      / _ \    / | ( _ )
#  / __| / _` || | / _` || '_ \ | || '_ ` _ \     __) || || |_    | | | |   | | / _ \
#  \__ \| (_| || || (_| || |_) || || | | | | |   / __/ |__   _| _ | |_| | _ | || (_) |
#  |___/ \__,_||_| \__,_||_.__/ |_||_| |_| |_|  |_____|   |_|  (_) \___/ (_)|_| \___/
#                    discrete event simulation
#
#  see www.salabim.org for more information, the documentation and license information

__version__ = "24.0.18"

import heapq
import random
import time
import math
import array
import collections
import os
import inspect
import sys
import itertools
import io
import pickle
import logging
import types
import bisect
import operator
import ctypes
import shutil
import subprocess
import tempfile
import struct
import binascii
import copy
import numbers
import platform
import functools
import traceback
import contextlib
import datetime
import urllib.request
import urllib.error
import base64
import zipfile

from pathlib import Path

from typing import Any, Union, Iterable, Tuple, List, Callable, TextIO, Dict, Set, Type, Hashable, Optional

# module = salabim  # for PythonInExcel runner

dataframe = None  # to please PyLance

ColorType = Union[str, Iterable[float]]

Pythonista = sys.platform == "ios"
Windows = sys.platform.startswith("win")
PyDroid = sys.platform == "linux" and any("pydroid" in v for v in os.environ.values())
PyPy = platform.python_implementation() == "PyPy"
Chromebook = "penguin" in platform.uname()
PythonInExcel = not ("__file__" in globals())
AnacondaCode = sys.platform == "emscripten"


def a_log(*args):
    if not hasattr(a_log, "a_logfile_name"):
        a_logfile_name = "a_log.txt"
        with open(a_logfile_name, "w"):
            ...

    with open(a_logfile_name, "a") as a_logfile:
        print(*args, file=a_logfile)


class g: ...


if PythonInExcel or AnacondaCode:
    _pie_result = []

    def pie_result():
        return _pie_result

    class b64_file_handler(io.IOBase):
        """
        special purpose file handler for Python in Excel

        Parameters
        ----------
            name : str
                name of file

            result : list
                b64 information will be added as string(s)

            blocksize : int
                b64 blocksize (default: 30000)

            mode : str
                if 't' (default): open for test
                if 'b': open for binary

        Note
        ----
        can be used as a file or in a context manager
        """

        def __init__(self, name, result, blocksize=30000, mode="t"):
            self._name = name
            self._result = result
            self._blocksize = blocksize
            if mode not in ("t", "b"):
                raise ValueError(f"wrong mode ({mode})")
            self._mode = mode
            self._buffer = []

        def __enter__(self):
            return self

        def __exit__(self, *args):
            self.close()

        def write(self, s):
            self._buffer.append(s)

        def close(self):
            if self._mode == "b":
                b = b"".join(self._buffer)
            else:
                b = "".join(self._buffer).encode("utf-8")
            n = self._blocksize
            b64 = base64.b64encode(b).decode("utf-8")
            self._result.append(f"<file name={self._name}>")
            while b64:
                b64_n = b64[:n]
                self._result.append(b64_n)
                b64 = b64[n:]
            self._result.append("</file>")
            super().close()


if Pythonista:
    try:
        import scene  # type: ignore
        import ui  # type: ignore
        import objc_util  # type: ignore
    except ModuleNotFoundError:
        Pythonista = False  # for non Pythonista implementation on iOS

inf = float("inf")
nan = float("nan")

if Pythonista or AnacondaCode or PythonInExcel:
    _yieldless = False
else:
    _yieldless = True


class QueueFullError(Exception):
    pass


class SimulationStopped(Exception):
    pass


def yieldless(value: bool = None) -> bool:
    """
    sets/ get default yieldless status

    Parameters
    ----------
    value : bool
        sets the new default yieldless status

        if omitted, no change

    Returns
    -------
    default yieldless status: bool

    Note
    ----
    At start up, the default yieldless status is False

    The default yieldless status can be used in subsequent Environment instantations.
    """

    global _yieldless
    if value is not None:
        _yieldless = value
    return _yieldless


class ItemFile:
    """
    define an item file to be used with read_item, read_item_int, read_item_float and read_item_bool

    Parameters
    ----------
    filename : str
        file to be used for subsequent read_item, read_item_int, read_item_float and read_item_bool calls

        or

        content to be interpreted used in subsequent read_item calls. The content should have at least one linefeed
        character and will be usually  triple quoted.

    Note
    ----
    It is advised to use ItemFile with a context manager, like ::

        with sim.ItemFile("experiment0.txt") as f:
            run_length = f.read_item_float()

            run_name = f.read_item()


    Alternatively, the file can be opened and closed explicitely, like ::

        f = sim.ItemFile("experiment0.txt")
        run_length = f.read_item_float()
        run_name = f.read_item()
        f.close()

    Item files consist of individual items separated by whitespace (blank or tab)

    If a blank or tab is required in an item, use single or double quotes

    All text following # on a line is ignored

    All texts on a line within curly brackets {} is ignored and considered white space.

    Curly braces cannot spawn multiple lines and cannot be nested.

    Example ::

        Item1
        "Item 2"
            Item3 Item4 # comment
        Item5 {five} Item6 {six}
        'Double quote" in item'
        "Single quote' in item"
        True
    """

    def __init__(self, filename: str):
        self.iter = self._nextread()
        if "\n" in filename:
            self.open_file = io.StringIO(filename)
        else:
            self.open_file = open(filename, "r")

    def __enter__(self):
        return self

    def __exit__(self, *args):
        self.open_file.close()

    def close(self):
        self.open_file.close()

    def read_item_int(self) -> int:
        """
        read next field from the ItemFile as int.h

        if the end of file is reached, EOFError is raised
        """
        return int(self.read_item().replace(",", "."))

    def read_item_float(self) -> float:
        """
        read next item from the ItemFile as float

        if the end of file is reached, EOFError is raised
        """

        return float(self.read_item().replace(",", "."))

    def read_item_bool(self) -> bool:
        """
        read next item from the ItemFile as bool

        A value of False (not case sensitive) will return False

        A value of 0 will return False

        The null string will return False

        Any other value will return True

        if the end of file is reached, EOFError is raised
        """
        result = self.read_item().strip().lower()
        if result == "false":
            return False
        try:
            if float(result) == 0:
                return False
        except (ValueError, TypeError):
            pass
        if result == "":
            return False
        return True

    def read_item(self) -> Any:
        """
        read next item from the ItemFile

        if the end of file is reached, EOFError is raised
        """
        try:
            return next(self.iter)
        except StopIteration:
            raise EOFError

    def _nextread(self):
        remove = "\r\n"
        quotes = "'\""

        for line in self.open_file:
            mode = "."
            result = ""
            for c in line:
                if c not in remove:
                    if mode in quotes:
                        if c == mode:
                            mode = "."
                            yield result  # even return the null string
                            result = ""
                        else:
                            result += c
                    elif mode == "{":
                        if c == "}":
                            mode = "."
                    else:
                        if c == "#":
                            break
                        if c in quotes:
                            if result:
                                yield result
                            result = ""
                            mode = c
                        elif c == "{":
                            if result:
                                yield result
                            result = ""
                            mode = c

                        elif c in (" ", "\t"):
                            if result:
                                yield result
                            result = ""
                        else:
                            result += c
            if result:
                yield result


class Monitor:
    """
    Monitor object

    Parameters
    ----------
    name : str
        name of the monitor

        if the name ends with a period (.),
        auto serializing will be applied

        if the name end with a comma,
        auto serializing starting at 1 will be applied

        if omitted, the name will be derived from the class
        it is defined in (lowercased)

    monitor : bool
        if True (default), monitoring will be on.

        if False, monitoring is disabled

        it is possible to control monitoring later,
        with the monitor method

    level : bool
        if False (default), individual values are tallied, optionally with weight

        if True, the tallied vslues are interpreted as levels

    initial_tally : any, preferably int, float or translatable into int or float
        initial value for the a level monitor

        it is important to set the value correctly.
        default: 0

        not available for non level monitors

    type : str
        specifies how tallied values are to be stored
            - "any" (default) stores values in a list. This allows
               non numeric values. In calculations the values are
               forced to a numeric value (0 if not possible)
            - "bool" (True, False) Actually integer >= 0 <= 255 1 byte
            - "int8" integer >= -128 <= 127 1 byte
            - "uint8" integer >= 0 <= 255 1 byte
            - "int16" integer >= -32768 <= 32767 2 bytes
            - "uint16" integer >= 0 <= 65535 2 bytes
            - "int32" integer >= -2147483648<= 2147483647 4 bytes
            - "uint32" integer >= 0 <= 4294967295 4 bytes
            - "int64" integer >= -9223372036854775808 <= 9223372036854775807 8 bytes
            - "uint64" integer >= 0 <= 18446744073709551615 8 bytes
            - "float" float 8 bytes

    weight_legend : str
        used in print_statistics and print_histogram to indicate the dimension of weight or duration (for
        level monitors, e.g. minutes. Default: weight for non level monitors, duration for level monitors.

    stats_only : bool
        if True, only statistics will be collected (using less memory, but also less functionality)

        if False (default), full functionality


    fill : list or tuple
        can be used to fill the tallied values (all at time now).

        fill is only available for non level and not stats_only monitors.


    env : Environment
        environment where the monitor is defined

        if omitted, default_env will be used
    """

    def __init__(
        self,
        name: str = None,
        monitor: bool = True,
        level: bool = False,
        initial_tally: Any = None,
        type: str = None,
        weight_legend: str = None,
        fill: Iterable = None,
        stats_only: bool = False,
        env: "Environment" = None,
        *args,
        **kwargs,
    ):
        self.env = _set_env(env)

        if isinstance(self.env, Environment):
            _set_name(name, self.env._nameserializeMonitor, self)
        else:
            self._name = name
        self._level = level
        self._weight_legend = ("duration" if self._level else "weight") if weight_legend is None else weight_legend
        if self._level:
            if weight_legend is None:
                self.weight_legend = "duration"
            else:
                self.weight_legend = weight_legend
            if initial_tally is None:
                self._tally = 0
            else:
                self._tally = initial_tally
            self._ttally = self.env._now
        else:
            if initial_tally is not None:
                raise TypeError("initial_tally not available for non level monitors")
            if weight_legend is None:
                self.weight_legend = "weight"
            else:
                self.weight_legend = weight_legend

        if type is None:
            type = "any"
        try:
            self.xtypecode, self.off = type_to_typecode_off(type)
        except KeyError:
            raise ValueError("type '" + type + "' not recognized")
        self.xtype = type
        self._stats_only = stats_only
        self.isgenerated = False
        self.cached_xweight = {}
        self.reset(monitor)
        if fill is not None:
            if self._level:
                raise ValueError("fill is not supported for level monitors")
            if self._stats_only:
                raise ValueError("fill is not supported for stats_only monitors")
            self._x.extend(fill)
            self._t.extend(len(fill) * [self.env._now])

        self.setup(*args, **kwargs)

    def __eq__(self, other):
        if isinstance(other, Monitor):
            return super().__eq__(other)
        raise TypeError(f"Not allowed to compare Monitor with {type(other).__name__} . Add parentheses?")

    def __add__(self, other):
        self._block_stats_only()
        if not isinstance(other, Monitor):
            return NotImplemented
        other._block_stats_only()
        return self.merge(other)

    def __radd__(self, other):
        self._block_stats_only()
        if other == 0:  # to be able to use sum
            return self
        if not isinstance(other, Monitor):
            return NotImplemented
        other._block_stats_only()
        return self.merge(other)

    def __mul__(self, other):
        self._block_stats_only()
        try:
            other = float(other)
        except Exception:
            return NotImplemented
        return self.multiply(other)

    def __rmul__(self, other):
        self._block_stats_only()
        return self * other

    def __truediv__(self, other):
        self._block_stats_only()
        try:
            other = float(other)
        except Exception:
            return NotImplemented
        return self * (1 / other)

    def _block_stats_only(self):
        if self._stats_only:
            frame = inspect.stack()[1][0]
            function = inspect.getframeinfo(frame).function
            if function == "__init__":
                function = frame.f_locals["self"].__class__.__name__
            raise NotImplementedError(function + " not available for " + self.name() + " because it is stats_only")

    def stats_only(self) -> bool:
        return self._stats_only

    def merge(self, *monitors, **kwargs) -> "Monitor":
        """
        merges this monitor with other monitor(s)

        Parameters
        ----------
        monitors : sequence
           zero of more monitors to be merged to this monitor

        name : str
            name of the merged monitor

            default: name of this monitor + ".merged"

        Returns
        -------
        merged monitor : Monitor

        Note
        ----
        Level monitors can only be merged with level monitors

        Non level monitors can only be merged with non level monitors

        Only monitors with the same type can be merged

        If no monitors are specified, a copy is created.

        For level monitors, merging means summing the available x-values

        """
        self._block_stats_only()
        name = kwargs.pop("name", None)
        if kwargs:
            raise TypeError("merge() got an unexpected keyword argument '" + tuple(kwargs)[0] + "'")
        new_xtype = self.xtype

        for m in monitors:
            m._block_stats_only()
            if not isinstance(m, Monitor):
                raise TypeError("not possible to merge monitor with " + object_to_str(m, True) + " type")
            if self._level != m._level:
                raise TypeError("not possible to mix level monitor with non level monitor")
            if self.xtype != m.xtype:
                new_xtype = "any"
            if self.env != m.env:
                raise TypeError("not possible to mix environments")
            if m.xtype != new_xtype:
                new_xtype = "any"  # to allow mixed xtypes

        if name is None:
            if self.name().endswith(".merged"):
                # this to avoid multiple .merged (particularly when merging with the + operator)
                name = self.name()
            else:
                name = self.name() + ".merged"

        new = _SystemMonitor(name=name, type=new_xtype, level=self._level, env=self.env)

        merge = [self] + list(monitors)

        if new._level:
            if new.xtypecode:
                new._x = array.array(self.xtypecode)
            else:
                new._x = []

            curx = [new.off] * len(merge)
            new._t = array.array("d")
            for t, index, x in heapq.merge(*[zip(merge[index]._t, itertools.repeat(index), merge[index]._x) for index in range(len(merge))]):
                if new.xtypecode:
                    curx[index] = x
                else:
                    try:
                        curx[index] = float(x)
                    except (ValueError, TypeError):
                        curx[index] = 0

                sum = 0
                for xi in curx:
                    if xi == new.off:
                        sum = new.off
                        break
                    sum += xi

                if new._t and (t == new._t[-1]):
                    new._x[-1] = sum
                else:
                    new._t.append(t)
                    new._x.append(sum)
            new.start = new._t[0]
        else:
            for t, _, x, weight in heapq.merge(
                *[
                    zip(
                        merge[index]._t, itertools.repeat(index), merge[index]._x, merge[index]._weight if merge[index]._weight else (1,) * len(merge[index]._x)
                    )
                    for index in range(len(merge))
                ]
            ):
                if weight == 1:
                    if new._weight:
                        new._weight.append(weight)
                else:
                    if not new._weight:
                        new._weight = array.array("d", (1,) * len(new._x))
                    new._weight.append(weight)
                new._t.append(t)
                new._x.append(x)
        new.monitor(False)
        new.isgenerated = True
        return new

    def t_multiply(self, factor, name=None):
        if name is None:
            name = "mapped"

        if not self._level:
            raise TypeError("t_multiply can't be applied to non level monitors")

        if factor <= 0:
            raise TypeError(f"factor {factor} <= 0")

        new = _SystemMonitor(name=name, type=self.xtype, level=self._level, env=self.env)
        new._x = []
        new._t = []
        for x in self._x:
            new._x.append(x)
        for t in self._t:
            new._t.append(t * factor)

        new.start = self.start * factor
        new.monitor(False)
        new._t[-1] = new._t[-1] * factor
        new.isgenerated = True
        return new

    def x_map(self, func: Callable, monitors: List["Monitor"] = [], name: str = None) -> "Monitor":
        """
        maps a function to the x-values of the given monitors (static method)

        Parameters
        ----------
        func : function
           a function that accepts n x-values, where n is the number of monitors
           note that the function will not be called during the time any of the monitors is off

        monitors : list/tuple of additional monitors
           monitor(s) to be mapped

           only allowed for level monitors-

        name : str
            name of the mapped monitor

            default: "mapped"

        Returns
        -------
        mapped monitor : Monitor, type 'any'
        """
        if name is None:
            name = "mapped"

        if monitors is not None:
            monitors = [self] + monitors
        else:
            monitors = [self]
        if not all(m._level == self._level for m in monitors):
            raise TypeError("not possible to mix level and non level monitors")
        if not all(m.env == self.env for m in monitors):
            raise TypeError("not all monitors have this environment")

        new = _SystemMonitor(name=name, type="any", level=self._level, env=self.env)

        for m in monitors:
            m._x_any = []
            for x in m._x:
                m._x_any.append(new.off if x == m.off else x)

        if new._level:
            new._x = []

            curx = [new.off] * len(monitors)
            new._t = array.array("d")
            for t, index, x in heapq.merge(*[zip(monitors[index]._t, itertools.repeat(index), monitors[index]._x_any) for index in range(len(monitors))]):
                curx[index] = x

                if any(val == new.off for val in curx):
                    result = new.off
                else:
                    result = func(*curx)

                if new._t and (t == new._t[-1]):
                    new._x[-1] = result
                else:
                    new._t.append(t)
                    new._x.append(result)
            new.start = new._t[0]
        else:
            new._x = []
            new._t = array.array("d")

            for x, t in zip(monitors[0]._x_any, monitors[0]._t):
                if x == new.off:
                    new._x.append(new.off)
                else:
                    new._x.append(func(x))
                new._t.append(t)

        for m in monitors:
            del m._x_any

        new.monitor(False)
        new.isgenerated = True
        return new

    def __getitem__(self, key):
        if isinstance(key, slice):
            return self.slice(key.start, key.stop, key.step)
        else:
            return self.slice(key)

    def freeze(self, name: str = None) -> "Monitor":
        """
        freezes this monitor (particularly useful for pickling)

        Parameters
        ----------
        name : str
            name of the frozen monitor

            default: name of this monitor + ".frozen"

        Returns
        -------
        frozen monitor : Monitor

        Notes
        -----
        The env attribute will become a partial copy of the original environment, with the name
        of the original environment, padded with '.copy.<serial number>'
        """
        self._block_stats_only()
        self_env = self.env
        self.env = Environment(to_freeze=True, name=self.env.name() + ".copy.", time_unit=self.env.get_time_unit())
        m = copy.deepcopy(self)
        self.env = self_env
        m.isgenerated = True
        m._name = self.name() + ".frozen" if name is None else name
        m.env._animate = False
        m.env._now = self.env._now
        m.env._offset = self.env._offset
        m.env._t = self.env._t
        return m

    def slice(self, start: float = None, stop: float = None, modulo: float = None, name: str = None) -> "Monitor":
        """
        slices this monitor (creates a subset)

        Parameters
        ----------
        start : float
           if modulo is not given, the start of the slice

           if modulo is given, this is indicates the slice period start (modulo modulo)

        stop : float
           if modulo is not given, the end of the slice

           if modulo is given, this is indicates the slice period end (modulo modulo)

           note that stop is excluded from the slice (open at right hand side)

        modulo : float
            specifies the distance between slice periods

            if not specified, just one slice subset is used.

        name : str
            name of the sliced monitor

            default: name of this monitor + ".sliced"

        Returns
        -------
        sliced monitor : Monitor

        Note
        ----
        It is also possible to use square bracktets to slice, like m[0:1000].
        """
        self._block_stats_only()
        if name is None:
            name = self.name() + ".sliced"
        new = _SystemMonitor(level=self._level, type=self.xtype, name=name, env=self.env)
        actions = []
        if modulo is None:
            if start is None:
                start_ = -inf
            else:
                start_ = start + self.env._offset
            start_ = max(start_, self.start)
            if stop is None:
                stop = inf
                stop_action = "z"  # inclusive
            else:
                stop += self.env._offset
                stop_action = "b"  # non inclusive
            if self.env._animate:
                stop = min(stop, self.env._t)
            else:
                stop = min(stop, self.env._now - self.env._offset)  # not self.now() in order to support frozen monitors
            actions.append((start, "a", 0, 0))
            actions.append((stop, stop_action, 0, 0))
        else:
            if start is None:
                raise TypeError("modulo specified, but no start specified. ")
            if stop is None:
                raise TypeError("modulo specified, but no stop specified")
            if stop <= start:
                raise ValueError("stop must be > start")
            if stop - start >= modulo:
                raise ValueError("stop must be < start + modulo")
            start_ = start % modulo
            stop = stop % modulo
            start1 = self._t[0] - (self._t[0] % modulo) + start_
            len1 = (stop - start_) % modulo
            while start1 < self.env._now:
                actions.append((start1, "a", 0, 0))
                actions.append((start1 + len1, "b", 0, 0))  # non inclusive
                start1 += modulo

        if new._level:
            if new.xtypecode:
                new._x = array.array(self.xtypecode)
            else:
                new._x = []
            new._t = array.array("d")
            curx = new.off
            new._t.append(self.start)
            new._x.append(curx)

        enabled = False
        if self.env._animate:
            _x = self._x[:]
            _t = self._t[:]
            if self._x:
                _x.append(self._x[-1])
                _t.append(self.env._t)
            if isinstance(self._weight, bool):
                _weight = self._weight
            else:
                _weight = self._weight[:]
                _weight.append(self._weight[-1])
        else:
            _x = self._x
            _t = self._t
            _weight = self._weight

        if self.env._animate:
            if self._x:
                _x.append(_x[-1])
                _t.append(self.env._t)
            try:
                _weight.append(self.weight[-1])
            except AttributeError:
                ...  # ignore if bool
        for t, type, x, weight in heapq.merge(actions, zip(self._t, itertools.repeat("c"), _x, _weight if (_weight and not self._level) else (1,) * len(_x))):
            if new._level:
                if type == "a":
                    enabled = True
                    if new._t[-1] == t:
                        new._x[-1] = curx
                    else:
                        if new._x[-1] == curx:
                            new._t[-1] = t
                        else:
                            new._t.append(t)
                            new._x.append(curx)
                elif type in ("b", "z"):
                    enabled = False
                    if new._t[-1] == t:
                        new._x[-1] = self.off
                    else:
                        if new._x[-1] == self.off:
                            new._t[-1] = t
                        else:
                            new._t.append(t)
                            new._x.append(self.off)
                else:
                    if enabled:
                        if curx != x:
                            if new._t[-1] == t:
                                new._x[-1] = x
                            else:
                                if x == new._x[-1]:
                                    new._t[-1] = t
                                else:
                                    new._t.append(t)
                                    new._x.append(x)
                    curx = x
            else:
                if type == "a":
                    enabled = True
                elif type in ("b", "z"):
                    enabled = False
                else:
                    if enabled:
                        if weight == 1:
                            if new._weight:
                                new._weight.append(weight)
                        else:
                            if not new._weight:
                                new._weight = array.array("d", (1,) * len(new._x))
                            new._weight.append(weight)
                        new._t.append(t)
                        new._x.append(x)
        new.monitor(False)
        new.isgenerated = True
        return new

    def setup(self) -> None:
        """
        called immediately after initialization of a monitor.

        by default this is a dummy method, but it can be overridden.

        only keyword arguments are passed
        """
        pass

    def register(self, registry: List) -> "Monitor":
        """
        registers the monitor in the registry

        Parameters
        ----------
        registry : list
            list of (to be) registered objects

        Returns
        -------
        monitor (self) : Monitor

        Note
        ----
        Use Monitor.deregister if monitor does not longer need to be registered.
        """
        if not isinstance(registry, list):
            raise TypeError("registry not list")
        if self in registry:
            raise ValueError(self.name() + " already in registry")
        registry.append(self)
        return self

    def deregister(self, registry: List) -> "Monitor":
        """
        deregisters the monitor in the registry

        Parameters
        ----------
        registry : list
            list of registered objects

        Returns
        -------
        monitor (self) : Monitor
        """
        if not isinstance(registry, list):
            raise TypeError("registry not list")
        if self not in registry:
            raise ValueError(self.name() + " not in registry")
        registry.remove(self)
        return self

    def __repr__(self):
        return (
            object_to_str(self)
            + ("[level]" if self._level else "")
            + " ("
            + self.name()
            + ")"
            + ("[stats_only]" if self._stats_only else "")
            + " ("
            + self.name()
            + ")"
        )

    def __call__(self, t=None):
        # direct moneypatching __call__ doesn't work
        if not self._level:
            raise TypeError("get not available for non level monitors")
        if t is None:
            return self._tally
        t += self.env._offset
        if t == self.env._now:
            return self._tally  # even if monitor is off, the current value is valid
        if self._stats_only:
            raise NotImplementedError("__call__(t) not supported for stats_only monitors")
        if t < self._t[0]:
            return self.off
        i = bisect.bisect_left(list(zip(self._t, itertools.count())), (t, float("inf")))
        return self._x[i - 1]

    def get(self, t: float = None) -> Any:
        """
        get the value of a level monitor

        Parameters
        ----------
        t : float
            time at which the value of the level is to be returned

            default: now

        Returns
        -------
        last tallied value : any, usually float

            Instead of this method, the level monitor can also be called directly, like


            level = sim.Monitor("level", level=True)

            ...

            print(level())

            print(level.get())  # identical


        Note
        ----
        If the value is not available, self.off will be returned.

        Only available for level monitors
        """
        return self.__call__(t)

    @property
    def value(self) -> Any:
        """
        get/set the value of a level monitor

        :getter:
            gets the last tallied value : any (often float)

        :setter:
            equivalent to m.tally()

        Note
        ----
        value is only available for level monitors

        value is available even if the monitor is turned off
        """
        if self._level:
            return self._tally
        raise TypeError("non level monitors are not supported")

    @value.setter
    def value(self, value: Any) -> None:
        if self._level:
            self.tally(value)
        else:
            raise TypeError("non level monitors are not supported")

    def t(self) -> float:
        """
        get the time of last tally of a level monitor

        :getter:
            gets the time of the last tallied value : float

        Note
        ----
        t is only available for level monitors

        t is available even if the monitor is turned off
        """
        if self._level:
            return self._ttally
        raise TypeError("non level monitors are not supported")

    def reset_monitors(self, monitor: bool = None, stats_only: bool = None) -> None:
        """
        resets monitor

        Parameters
        ----------
        monitor : bool
            if True (default), monitoring will be on.

            if False, monitoring is disabled

            if omitted, the monitor state remains unchanged

        stats_only : bool
            if True, only statistics will be collected (using less memory, but also less functionality)

            if False, full functionality

            if omittted, no change of stats_only

        Note
        ----
        Exactly same functionality as Monitor.reset()
        """
        self.reset(monitor=monitor, stats_only=stats_only)

    def reset(self, monitor: bool = None, stats_only: bool = None) -> None:
        """
        resets monitor

        Parameters
        ----------
        monitor : bool
            if True, monitoring will be on.

            if False, monitoring is disabled
            if omitted, no change of monitoring state

        stats_only : bool
            if True, only statistics will be collected (using less memory, but also less functionality)

            if False, full functionality

            if omittted, no change of stats_only
        """
        if self.isgenerated:
            raise TypeError("sliced, merged or frozen monitors cannot be reset")
        if monitor is not None:
            self._monitor = monitor
        if stats_only is not None:
            self._stats_only = stats_only
        self.start = self.env._now
        if self._stats_only:  # all values for ex0=False and ex0=True
            self.mun = [0] * 2
            self.n = [0] * 2
            self.sn = [0] * 2
            self.sumw = [0] * 2
            self._minimum = [inf] * 2
            self._maximum = [-inf] * 2
            if self._level:
                self._ttally_monitored = self.env._now
            self._weight = False

        else:
            if self.xtypecode:
                self._x = array.array(self.xtypecode)
            else:
                self._x = []
            self._t = array.array("d")
            self._weight = False
            if self._level:
                self._weight = True  # signal for statistics that weights are present (although not stored in _weight)
                if self._monitor:
                    self._x.append(self._tally)
                else:
                    self._x.append(self.off)
                self._t.append(self.env._now)
            else:
                self._weight = False  # weights are only stored if there is a non 1 weight
        self.cached_xweight.clear()  # invalidate cache

        self.monitor(monitor)

    def monitor(self, value: bool = None) -> bool:
        """
        enables/disables monitor

        Parameters
        ----------
        value : bool
            if True, monitoring will be on.

            if False, monitoring is disabled

            if omitted, no change

        Returns
        -------
        True, if monitoring enabled. False, if not : bool
        """
        if self._stats_only:
            if value is not None:
                if self._monitor:
                    if self._level:
                        self._tally_add_now()

                self._ttally_monitored = self.env._now

                self._monitor = value
        else:
            if value is not None:
                if value and self.isgenerated:
                    raise TypeError("sliced, merged or frozen monitors cannot be turned on")
                self._monitor = value
                if self._level:
                    if self._monitor:
                        self.tally(self._tally)
                    else:
                        self._tally_off()  # can't use tally() here because self._tally should be untouched
        return self._monitor

    def start_time(self) -> float:
        """
        Returns
        -------
        Start time of the monitor : float
             either the time of creation or latest reset
        """
        return self.start - self.env._offset

    def tally(self, value: Any, weight: float = 1) -> None:
        """
        Parameters
        ----------
        value : any, preferably int, float or translatable into int or float
            value to be tallied

        weight: float
            weight to be tallied

            default : 1

        """
        if self.isgenerated:
            raise TypeError("sliced, merged or frozen monitors cannot be reset")

        if self._stats_only:
            if self._level:
                if weight != 1:
                    raise ValueError("level monitor supports only weight=1, not: " + str(weight))
                if self._monitor:
                    weight = self.env._now - self._ttally_monitored
                    value_num = self._tally
                    self._ttally_monitored = self.env._now
                self._tally = value
                self._ttally = self.env._now

            else:
                value_num = value

            if self._monitor and weight != 0:
                if not isinstance(value_num, numbers.Number):
                    try:
                        if int(value_num) == float(value_num):
                            value_num = int(value_num)
                        else:
                            value_num = float(value_num)
                    except (ValueError, TypeError):
                        value_num = 0

                for ex0 in [False, True] if value_num else [False]:
                    self.n[ex0] += 1
                    # algorithm based on https://fanf2.user.srcf.net/hermes/doc/antiforgery/stats.pdf
                    self.sumw[ex0] += weight
                    mun1 = self.mun[ex0]
                    self.mun[ex0] = mun1 + (weight / self.sumw[ex0]) * (value_num - mun1)
                    self.sn[ex0] = self.sn[ex0] + weight * (value_num - mun1) * (value_num - self.mun[ex0])
                    self._minimum[ex0] = min(self._minimum[ex0], value_num)
                    self._maximum[ex0] = max(self._maximum[ex0], value_num)
                if weight != 1:
                    self._weight = True

        else:
            self.cached_xweight.clear()  # invalidate cache
            if self._level:
                if weight != 1:
                    if self._level:
                        raise ValueError("level monitor supports only weight=1, not: " + str(weight))
                if value == self.off:
                    raise ValueError("not allowed to tally " + str(self.off) + " (off)")
                self._tally = value
                self._ttally = self.env._now

                if self._monitor:
                    t = self.env._now
                    if self._t[-1] == t:
                        self._x[-1] = value
                    else:
                        self._x.append(value)
                        self._t.append(t)
            else:
                if self._monitor:
                    if weight == 1:
                        if self._weight:
                            self._weight.append(weight)
                    else:
                        if not self._weight:
                            self._weight = array.array("d", (1,) * len(self._x))
                        self._weight.append(weight)
                    self._x.append(value)
                    self._t.append(self.env._now)

    def _tally_add_now(self):
        # used by stats_only level monitors
        save_ttally = self._ttally
        self.tally(self._tally)
        self._ttally = save_ttally

    def _tally_off(self):
        if self.isgenerated:
            raise TypeError("sliced, merged or frozen monitors cannot be reset")
        t = self.env._now
        if self._t[-1] == t:
            self._x[-1] = self.off
        else:
            self._x.append(self.off)
            self._t.append(t)

    def to_years(self, name: str = None) -> "Monitor":
        """
        makes a monitor with all x-values converted to years

        Parameters
        ----------
        name : str
            name of the converted monitor

            default: name of this monitor

        Returns
        -------
        converted monitor : Monitor

        Note
        ----
        Only non level monitors with type float can be converted.

        It is required that a time_unit is defined for the environment.
        """
        self._block_stats_only()
        self.env._check_time_unit_na()
        return self.to_time_unit("years", name=name)

    def to_weeks(self, name: str = None) -> "Monitor":
        """
        makes a monitor with all x-values converted to weeks

        Parameters
        ----------
        name : str
            name of the converted monitor

            default: name of this monitor

        Returns
        -------
        converted monitor : Monitor

        Note
        ----
        Only non level monitors with type float can be converted.

        It is required that a time_unit is defined for the environment.
        """
        self._block_stats_only()
        self.env._check_time_unit_na()
        return self.to_time_unit("weeks", name=name)

    def to_days(self, name: str = None) -> "Monitor":
        """
        makes a monitor with all x-values converted to days

        Parameters
        ----------
        name : str
            name of the converted monitor

            default: name of this monitor

        Returns
        -------
        converted monitor : Monitor

        Note
        ----
        Only non level monitors with type float can be converted.

        It is required that a time_unit is defined for the environment.
        """
        self._block_stats_only()
        self.env._check_time_unit_na()
        return self.to_time_unit("days", name=name)

    def to_hours(self, name: str = None) -> "Monitor":
        """
        makes a monitor with all x-values converted to hours

        Parameters
        ----------
        name : str
            name of the converted monitor

            default: name of this monitor

        Returns
        -------
        converted monitor : Monitor

        Note
        ----
        Only non level monitors with type float can be converted.

        It is required that a time_unit is defined for the environment.
        """
        self._block_stats_only()
        self.env._check_time_unit_na()
        return self.to_time_unit("hours", name=name)

    def to_minutes(self, name: str = None) -> "Monitor":
        """
        makes a monitor with all x-values converted to minutes

        Parameters
        ----------
        name : str
            name of the converted monitor

            default: name of this monitor

        Returns
        -------
        converted monitor : Monitor

        Note
        ----
        Only non level monitors with type float can be converted.

        It is required that a time_unit is defined for the environment.
        """
        self._block_stats_only()
        self.env._check_time_unit_na()
        return self.to_time_unit("minutes", name=name)

    def to_seconds(self, name: str = None) -> "Monitor":
        """
        makes a monitor with all x-values converted to seconds

        Parameters
        ----------
        name : str
            name of the converted monitor

            default: name of this monitor

        Returns
        -------
        converted monitor : Monitor

        Note
        ----
        Only non level monitors with type float can be converted.

        It is required that a time_unit is defined for the environment.
        """
        self._block_stats_only()
        self.env._check_time_unit_na()
        return self.to_time_unit("seconds", name=name)

    def to_milliseconds(self, name: str = None) -> "Monitor":
        """
        makes a monitor with all x-values converted to milliseconds

        Parameters
        ----------
        name : str
            name of the converted monitor

            default: name of this monitor

        Returns
        -------
        converted monitor : Monitor

        Note
        ----
        Only non level monitors with type float can be converted.

        It is required that a time_unit is defined for the environment.
        """
        self._block_stats_only()
        self.env._check_time_unit_na()
        return self.to_time_unit("milliseconds", name=name)

    def to_microseconds(self, name: str = None) -> "Monitor":
        """
        makes a monitor with all x-values converted to microseconds

        Parameters
        ----------
        name : str
            name of the converted monitor

            default: name of this monitor

        Returns
        -------
        converted monitor : Monitor

        Note
        ----
        Only non level monitors with type float can be converted.

        It is required that a time_unit is defined for the environment.
        """
        self._block_stats_only()
        self.env._check_time_unit_na()
        return self.to_time_unit("microseconds", name=name)

    def to_time_unit(self, time_unit: str, name: str = None) -> "Monitor":
        """
        makes a monitor with all x-values converted to the specified time unit

        Parameters
        ----------
        time_unit : str
            Supported time_units:

            "years", "weeks", "days", "hours", "minutes", "seconds", "milliseconds", "microseconds"

        name : str
            name of the converted monitor

            default: name of this monitor

        Returns
        -------
        converted monitor : Monitor

        Note
        ----
        Only non level monitors with type float can be converted.

        It is required that a time_unit is defined for the environment.
        """
        self._block_stats_only()
        self.env._check_time_unit_na()
        return self.multiply(_time_unit_lookup(time_unit) / self.env._time_unit, name=name)

    def multiply(self, scale: float = 1, name: str = None) -> "Monitor":
        """
        makes a monitor with all x-values multiplied with scale

        Parameters
        ----------
        scale : float
           scale to be applied

        name : str
            name of the multiplied monitor

            default: name of this monitor

        Returns
        -------
        multiplied monitor : Monitor

        Note
        ----
        Only non level monitors with type float can be multiplied

        """
        self._block_stats_only()
        if self._level:
            raise ValueError("level monitors can't be multiplied")

        if self.xtype == "float":
            if name is None:
                name = self.name()
            new = _SystemMonitor(name=name, monitor=False, type="float", level=False, env=self.env)
            new.isgenerated = True
            new._x = [x * scale for x in self._x]
            new._t = [t for t in self._t]
            return new

        else:
            raise ValueError("type", self.xtype, " monitors can't be multiplied (only float)")

    def name(self, value: str = None) -> str:
        """
        Parameters
        ----------
        value : str
            new name of the monitor
            if omitted, no change

        Returns
        -------
        Name of the monitor : str

        Note
        ----
        base_name and sequence_number are not affected if the name is changed
        """
        if value is not None:
            self._name = value
        return self._name

    def rename(self, value: str = None) -> "Monitor":
        """
        Parameters
        ----------
        value : str
            new name of the monitor
            if omitted, no change

        Returns
        -------
        self : monitor

        Note
        ----
        in contrast to name(), this method returns itself, so can used to chain, e.g.

        (m0 + m1 + m2+ m3).rename('m0-m3').print_histograms()

        m0[1000 : 2000].rename('m between t=1000 and t=2000').print_histograms()

        """
        self.name(value)
        return self

    def base_name(self) -> str:
        """
        Returns
        -------
        base name of the monitor (the name used at initialization): str
        """
        return getattr(self, "_base_name", self._name)

    def sequence_number(self) -> int:
        """
        Returns
        -------
        sequence_number of the monitor : int
            (the sequence number at initialization)

            normally this will be the integer value of a serialized name.

            Non serialized names (without a dot or a comma at the end)
            will return 1)
        """
        return getattr(self, "_sequence_number", 1)

    def mean(self, ex0: bool = False) -> float:
        """
        mean of tallied values

        Parameters
        ----------
        ex0 : bool
            if False (default), include zeroes. if True, exclude zeroes

        Returns
        -------
        mean : float

        Note
        ----
        If weights are applied , the weighted mean is returned
        """
        if self._stats_only:
            ex0 = bool(ex0)
            if self._level:
                self._tally_add_now()
            if self.sumw[ex0]:
                return self.mun[ex0]
            else:
                return nan
        else:
            x, weight = self._xweight(ex0=ex0)
            sumweight = sum(weight)
            if sumweight:
                return sum(vx * vweight for vx, vweight in zip(x, weight)) / sumweight
            else:
                return nan

    def std(self, ex0: bool = False) -> float:
        """
        standard deviation of tallied values

        Parameters
        ----------
        ex0 : bool
            if False (default), include zeroes. if True, exclude zeroes

        Returns
        -------
        standard deviation : float

        Note
        ----
        If weights are applied, the weighted standard deviation is returned
        """
        if self._stats_only:
            ex0 = bool(ex0)
            if self._level:
                self._tally_add_now()
            if self.sumw[ex0]:
                return math.sqrt(self.sn[ex0] / self.sumw[ex0])
            else:
                return nan
        else:
            x, weight = self._xweight(ex0=ex0)
            sumweight = sum(weight)
            if sumweight:
                wmean = self.mean(ex0=ex0)
                wvar = sum((vweight * (vx - wmean) ** 2) for vx, vweight in zip(x, weight)) / sumweight
                return math.sqrt(wvar)
            else:
                return nan

    def minimum(self, ex0: bool = False) -> float:
        """
        minimum of tallied values

        Parameters
        ----------
        ex0 : bool
            if False (default), include zeroes. if True, exclude zeroes

        Returns
        -------
        minimum : float
        """
        if self._stats_only:
            ex0 = bool(ex0)
            if self.n[ex0]:
                return self._minimum[ex0]
            else:
                return nan
        else:
            x = self._xweight(ex0=ex0)[0]
            if x:
                return min(x)
            else:
                return nan

    def maximum(self, ex0: bool = False) -> float:
        """
        maximum of tallied values

        Parameters
        ----------
        ex0 : bool
            if False (default), include zeroes. if True, exclude zeroes

        Returns
        -------
        maximum : float
        """

        if self._stats_only:
            if self.n[ex0]:
                return self._maximum[ex0]
            else:
                return nan
        else:
            x = self._xweight(ex0=ex0)[0]
            if x:
                return max(x)
            else:
                return nan

    def median(self, ex0: bool = False, interpolation: str = "linear") -> float:
        """
        median of tallied values

        Parameters
        ----------
        ex0 : bool
            if False (default), include zeroes. if True, exclude zeroes

        interpolation : str
            Default: 'linear'



            For non weighted monitors:

            This optional parameter specifies the interpolation method to use when the 50% percentile lies between two data points i < j:

            'linear': i + (j - i) * fraction, where fraction is the fractional part of the index surrounded by i and j. (default for monitors that are not weighted not level

            'lower': i.

            'higher': j. (default for weighted and level monitors)

            'nearest': i or j, whichever is nearest.

            'midpoint': (i + j) / 2.



            For weighted and level monitors:

            This optional parameter specifies the interpolation method to use when the 50% percentile corresponds exactly to two data points i and j

            'linear': (i + j) /2

            'lower': i.

            'higher': j

            'midpoint': (i + j) / 2.


        Returns
        -------
        median (50% percentile): float
        """
        return self.percentile(50, ex0=ex0, interpolation=interpolation)

    def percentile(self, q: float, ex0: bool = False, interpolation: str = "linear") -> float:
        """
        q-th percentile of tallied values

        Parameters
        ----------
        q : float
            percentage of the distribution

            values <0 are treated a 0

            values >100 are treated as 100

        ex0 : bool
            if False (default), include zeroes. if True, exclude zeroes

        interpolation : str
            Default: 'linear'



            For non weighted monitors:

            This optional parameter specifies the interpolation method to use when the desired percentile lies between two data points i < j:

            'linear': i + (j - i) * fraction, where fraction is the fractional part of the index surrounded by i and j. (default for monitors that are not weighted not level

            'lower': i.

            'higher': j. (default for weighted and level monitors)

            'nearest': i or j, whichever is nearest.

            'midpoint': (i + j) / 2.



            For weighted and level monitors:

            This optional parameter specifies the interpolation method to use when the percentile corresponds exactly to two data points i and j

            'linear': (i + j) /2

            'lower': i.

            'higher': j

            'midpoint': (i + j) / 2.


        Returns
        -------
        q-th percentile : float
             0 returns the minimum, 50 the median and 100 the maximum
        """
        self._block_stats_only()

        if interpolation not in (("linear", "lower", "higher", "midpoint") if self._weight else ("linear", "lower", "higher", "midpoint", "nearest")):
            raise ValueError("incorrect interpolation method " + str(interpolation))

        q = max(0, min(q, 100))
        if q == 0:
            return self.minimum(ex0=ex0)
        if q == 100:
            return self.maximum(ex0=ex0)
        q /= 100
        x, weight = self._xweight(ex0=ex0)

        if len(x) == 1:
            return x[0]

        sum_weight = sum(weight)
        if not sum_weight:
            return nan

        x_sorted, weight_sorted = zip(*sorted(zip(x, weight), key=lambda v: v[0]))
        n = len(x_sorted)

        if self._weight:
            weight_cum = []
            cum = 0
            for k in range(n):
                cum += weight_sorted[k]
                weight_cum.append(cum / sum_weight)
            for k in range(n):
                if weight_cum[k] >= q:
                    break
            if weight_cum[k] != q:
                return x_sorted[k]

            if interpolation in ("linear", "midpoint"):
                return (x_sorted[k] + x_sorted[k + 1]) / 2
            if interpolation in ("lower"):
                return x_sorted[k]
            if interpolation == "higher":
                return x_sorted[k + 1]

        else:
            weight_cum = []
            for k in range(n):
                weight_cum.append(k / (n - 1))
            for k in range(n):
                if weight_cum[k + 1] > q:
                    break

            if interpolation == "linear":
                return interpolate(q, weight_cum[k], weight_cum[k + 1], x_sorted[k], x_sorted[k + 1])
            if interpolation == "lower":
                return x_sorted[k]
            if interpolation == "higher":
                return x_sorted[k + 1]
            if interpolation == "midpoint":
                return (x_sorted[k] + x_sorted[k + 1]) / 2
            if interpolation == "nearest":
                if q - weight_cum[k] <= weight_cum[k + 1] - q:
                    return x_sorted[k]
                else:
                    return x_sorted[k + 1]

    def bin_number_of_entries(self, lowerbound: float, upperbound: float, ex0: bool = False) -> int:
        """
        count of the number of tallied values in range (lowerbound,upperbound]

        Parameters
        ----------
        lowerbound : float
            non inclusive lowerbound

        upperbound : float
            inclusive upperbound

        ex0 : bool
            if False (default), include zeroes. if True, exclude zeroes

        Returns
        -------
        number of values >lowerbound and <=upperbound : int

        Note
        ----
        Not available for level monitors
        """
        self._block_stats_only()
        if self._level:
            raise TypeError("bin_number_of_entries not available for level monitors")
        x = self._xweight(ex0=ex0)[0]
        return sum(1 for vx in x if (vx > lowerbound) and (vx <= upperbound))

    def bin_weight(self, lowerbound: float, upperbound: float) -> float:
        """
        total weight of tallied values in range (lowerbound,upperbound]

        Parameters
        ----------
        lowerbound : float
            non inclusive lowerbound

        upperbound : float
            inclusive upperbound

        Returns
        -------
        total weight of values >lowerbound and <=upperbound : float

        Note
        ----
        Not available for level monitors
        """
        self._block_stats_only()
        if self._level:
            raise TypeError("bin_weight not available for level monitors")
        return self.sys_bin_weight(lowerbound, upperbound)

    def bin_duration(self, lowerbound: float, upperbound: float) -> float:
        """
        total duration of tallied values in range (lowerbound,upperbound]

        Parameters
        ----------
        lowerbound : float
            non inclusive lowerbound

        upperbound : float
            inclusive upperbound

        Returns
        -------
        total duration of values >lowerbound and <=upperbound : float

        Note
        ----
        Not available for level monitors
        """
        self._block_stats_only()
        if not self._level:
            raise TypeError("bin_duration not available for non level monitors")
        return self.sys_bin_weight(lowerbound, upperbound)

    def sys_bin_weight(self, lowerbound, upperbound):
        x, weight = self._xweight()
        return sum((vweight for vx, vweight in zip(x, weight) if (vx > lowerbound) and (vx <= upperbound)))

    def value_number_of_entries(self, value: Any) -> int:
        """
        count of the number of tallied values equal to value or in value

        Parameters
        ----------
        value : any
            if list, tuple or set, check whether the tallied value is in value

            otherwise, check whether the tallied value equals the given value

        Returns
        -------
        number of tallied values in value or equal to value : int

        Note
        ----
        Not available for level monitors
        """
        self._block_stats_only()
        if self._level:
            raise TypeError("value_number_of_entries not available for level monitors")
        if isinstance(value, str):
            value = [value]
        try:
            iter(value)  # iterable?
            values = value
        except TypeError:
            values = [value]

        x = self._xweight(force_numeric=False)[0]
        return sum(1 for vx in x if (vx in values))

    def value_weight(self, value: Any) -> float:
        """
        total weight of tallied values equal to value or in value

        Parameters
        ----------
        value : any
            if list, tuple or set, check whether the tallied value is in value

            otherwise, check whether the tallied value equals the given value

        Returns
        -------
        total of weights of tallied values in value or equal to value : float

        Note
        ----
        Not available for level monitors
        """
        self._block_stats_only()
        if self._level:
            raise TypeError("value_weight not supported for level monitors")
        return self.sys_value_weight(value)

    def value_duration(self, value: Any) -> float:
        """
        total duration of tallied values equal to value or in value

        Parameters
        ----------
        value : any
            if list, tuple or set, check whether the tallied value is in value

            otherwise, check whether the tallied value equals the given value

        Returns
        -------
        total of duration of tallied values in value or equal to value : float

        Note
        ----
        Not available for non level monitors
        """
        self._block_stats_only()
        if not self._level:
            raise TypeError("value_weight not available for non level monitors")
        return self.sys_value_weight(value)

    def sys_value_weight(self, value):
        x, weight = self._xweight(force_numeric=False)

        if isinstance(value, str):
            value = [value]
        try:
            iter(value)  # iterable?
            values = value
        except TypeError:
            values = [value]

        return sum(vweight for (vx, vweight) in zip(x, weight) if vx in values)

    def number_of_entries(self, ex0: bool = False) -> int:
        """
        count of the number of entries

        Parameters
        ----------
        ex0 : bool
            if False (default), include zeroes. if True, exclude zeroes

        Returns
        -------
        number of entries : int

        Note
        ----
        Not available for level monitors
        """
        if self._level:
            raise TypeError("number_of_entries not available for level monitors")
        if self._stats_only:
            ex0 = bool(ex0)
            return self.n[ex0]
        else:
            x = self._xweight(ex0=ex0)[0]
            return len(x)

    def number_of_entries_zero(self) -> int:
        """
        count of the number of zero entries

        Returns
        -------
        number of zero entries : int

        Note
        ----
        Not available for level monitors
        """
        if self._level:
            raise TypeError("number_of_entries_zero not available for level monitors")
        return self.number_of_entries() - self.number_of_entries(ex0=True)

    def weight(self, ex0: bool = False) -> float:
        """
        sum of weights

        Parameters
        ----------
        ex0 : bool
            if False (default), include zeroes. if True, exclude zeroes

        Returns
        -------
        sum of weights : float

        Note
        ----
        Not available for level monitors
        """
        if self._level:
            raise TypeError("weight not available for level monitors")
        return self.sys_weight(ex0)

    def duration(self, ex0: bool = False) -> float:
        """
        total duration

        Parameters
        ----------
        ex0 : bool
            if False (default), include zeroes. if True, exclude zeroes

        Returns
        -------
        total duration : float

        Note
        ----
        Not available for non level monitors
        """
        if not self._level:
            raise TypeError("duration not available for non level monitors")
        return self.sys_weight(ex0)

    def sys_weight(self, ex0: bool = False):
        if self._stats_only:
            ex0 = bool(ex0)
            return self.sumw[ex0]
        else:
            _, weight = self._xweight(ex0=ex0)
            return sum(weight)

    def weight_zero(self) -> float:
        """
        sum of weights of zero entries

        Returns
        -------
        sum of weights of zero entries : float

        Note
        ----
        Not available for level monitors
        """
        if self._level:
            raise TypeError("weight_zero not available for level monitors")
        return self.sys_weight_zero()

    def duration_zero(self) -> float:
        """
        total duratiom of zero entries

        Returns
        -------
        total duration of zero entries : float

        Note
        ----
        Not available for non level monitors
        """
        if not self._level:
            raise TypeError("duration_zero not available for non level monitors")
        return self.sys_weight_zero()

    def sys_weight_zero(self):
        return self.sys_weight() - self.sys_weight(ex0=True)

    def print_statistics(self, show_header: bool = True, show_legend: bool = True, do_indent: bool = False, as_str: bool = False, file: TextIO = None) -> str:
        """
        print monitor statistics

        Parameters
        ----------
        show_header: bool
            primarily for internal use

        show_legend: bool
            primarily for internal use

        do_indent: bool
            primarily for internal use

        as_str: bool
            if False (default), print the statistics
            if True, return a string containing the statistics

        file: file
            if None (default), all output is directed to stdout

            otherwise, the output is directed to the file

        Returns
        -------
        statistics (if as_str is True) : str
        """
        result = []
        if do_indent:
            ll = 45
        else:
            ll = 0
        indent = pad("", ll)

        if show_header:
            result.append(indent + f"Statistics of {self.name()} at {fn(self.env._now - self.env._offset, 13, 3)}")

        if show_legend:
            result.append(indent + "                        all    excl.zero         zero")
            result.append(pad("-" * (ll - 1) + " ", ll) + "-------------- ------------ ------------ ------------")

        if self._weight:
            result.append(
                f"{pad(self.name(), ll)}{pad(self.weight_legend, 14)}{fn(self.sys_weight(), 13, 3)}{fn(self.sys_weight(ex0=True), 13, 3)}{fn(self.sys_weight_zero(), 13, 3)}"
            )
        else:
            result.append(
                f"{pad(self.name(), ll)}{pad('entries', 14)}{fn(self.number_of_entries(), 13, 3)}{fn(self.number_of_entries(ex0=True), 13, 3)}{fn(self.number_of_entries_zero(), 13, 3)}"
            )

        result.append(f"{indent}mean          {fn(self.mean(), 13, 3)}{fn(self.mean(ex0=True), 13, 3)}")

        result.append(f"{indent}std.deviation {fn(self.std(), 13, 3)}{fn(self.std(ex0=True), 13, 3)}")
        result.append("")
        result.append(f"{indent}minimum       {fn(self.minimum(), 13, 3)}{fn(self.minimum(ex0=True), 13, 3)}")
        if not self._stats_only:
            result.append(f"{indent}median        {fn(self.percentile(50), 13, 3)}{fn(self.percentile(50, ex0=True), 13, 3)}")
            result.append(f"{indent}90% percentile{fn(self.percentile(90), 13, 3)}{fn(self.percentile(90, ex0=True), 13, 3)}")
            result.append(f"{indent}95% percentile{fn(self.percentile(95), 13, 3)}{fn(self.percentile(95, ex0=True), 13, 3)}")
        result.append(f"{indent}maximum       {fn(self.maximum(), 13, 3)}{fn(self.maximum(ex0=True), 13, 3)}")
        return return_or_print(result, as_str, file)

    def histogram_autoscale(self, ex0: bool = False) -> Tuple[float, float, int]:
        """
        used by histogram_print to autoscale

        may be overridden.

        Parameters
        ----------
        ex0 : bool
            if False (default), include zeroes. if True, exclude zeroes

        Returns
        -------
        bin_width, lowerbound, number_of_bins : tuple
        """
        self._block_stats_only()

        if self.sys_weight(ex0=ex0) == 0:
            return 1, 0, 0

        xmax = self.maximum(ex0=ex0)
        xmin = self.minimum(ex0=ex0)

        done = False
        for i in range(10):
            exp = 10**i
            for bin_width in (exp, exp * 2, exp * 5):
                lowerbound = math.floor(xmin / bin_width) * bin_width
                number_of_bins = int(math.ceil((xmax - lowerbound) / bin_width))
                if number_of_bins <= 30:
                    done = True
                    break
            if done:
                break
        return bin_width, lowerbound, number_of_bins

    def print_histograms(
        self,
        number_of_bins: int = None,
        lowerbound: float = None,
        bin_width: float = None,
        values: bool = False,
        ex0: bool = False,
        as_str: bool = False,
        file: TextIO = None,
        graph_scale: float = None,
    ) -> str:
        """
        print monitor statistics and histogram

        Parameters
        ----------
        number_of_bins : int
            number of bins

            default: 30

            if <0, also the header of the histogram will be surpressed

        lowerbound: float
            first bin

            default: 0

        bin_width : float
            width of the bins

            default: 1

        values : bool
            if False (default), bins will be used

            if True, the individual values will be shown (sorted on the value).
            in that case, no cumulative values will be given


        ex0 : bool
            if False (default), include zeroes. if True, exclude zeroes

        as_str: bool
            if False (default), print the histogram
            if True, return a string containing the histogram

        file: file
            if None(default), all output is directed to stdout

            otherwise, the output is directed to the file

        graph_scale : float
            Scale in the graphical representation of the % and cum% (default=80)

        Returns
        -------
        histogram (if as_str is True) : str

        Note
        ----
        If number_of_bins, lowerbound and bin_width are omitted, the histogram will be autoscaled,
        with a maximum of 30 classes.

        Exactly same functionality as Monitor.print_histogram()
        """
        return self.print_histogram(number_of_bins, lowerbound, bin_width, values, ex0, as_str=as_str, file=file, graph_scale=graph_scale)

    def print_histogram(
        self,
        number_of_bins: int = None,
        lowerbound: float = None,
        bin_width: float = None,
        values: Union[bool, Iterable] = False,
        ex0: bool = False,
        as_str: bool = False,
        file: TextIO = None,
        graph_scale: float = None,
        sort_on_weight: bool = False,
        sort_on_duration: bool = False,
        sort_on_value: bool = False,
    ) -> str:
        """
        print monitor statistics and histogram

        Parameters
        ----------
        number_of_bins : int
            number of bins

            default: 30

            if <0, also the header of the histogram will be surpressed

        lowerbound: float
            first bin

            default: 0

        bin_width : float
            width of the bins

            default: 1

        values : bool
            if False (default), bins will be used

            if True, the individual values will be shown (in alphabetical order).
            in that case, no cumulative values will be given

            if an iterable, the items will be used

        ex0 : bool
            if False (default), include zeroes. if True, exclude zeroes

        as_str: bool
            if False (default), print the histogram
            if True, return a string containing the histogram

        file: file
            if None(default), all output is directed to stdout

            otherwise, the output is directed to the file

        graph_scale : float
            Scale in the graphical representation of the % and cum% (default=80)

        sort_on_weight : bool
            if True, sort the values on weight first (largest first), then on the values itself

            if False, sort the values on the values itself

            False is the default for non level monitors. Not permitted for level monitors.

        sort_on_duration : bool
            if True, sort the values on duration first (largest first), then on the values itself

            if False, sort the values on the values itself

            False is the default for level monitors. Not permitted for non level monitors.

        sort_on_value : bool
            if True, sort on the values.

            if False (default), no sorting will take place, unless values is an iterable, in which case
            sorting will be done on the values anyway.

        Returns
        -------
        histogram (if as_str is True) : str

        Note
        ----
        If number_of_bins, lowerbound and bin_width are omitted, the histogram will be autoscaled,
        with a maximum of 30 classes.
        """

        if self._level and sort_on_weight:
            raise ValueError("level monitors can't be sorted on weight. Use sort_on_duration instead")
        if not self._level and sort_on_duration:
            raise ValueError("non level monitors can't be sorted on duration. Use sort_on_weight instead")
        if sort_on_value and sort_on_weight:
            raise ValueError("sort_on_value can't be combined with sorted_on_value")
        if sort_on_value and sort_on_weight:
            raise ValueError("sort_on_weight can't be combined with sorted_on_value")

        result = []
        result.append("Histogram of " + self.name() + ("[ex0]" if ex0 else ""))

        if graph_scale is None:
            graph_scale = 80

        if self._stats_only:
            weight_total = self.sumw[bool(ex0)]
        else:
            x, weight = self._xweight(ex0=ex0, force_numeric=not values)
            weight_total = sum(weight)

        values_is_iterable = False
        if not isinstance(values, str):
            try:
                values = list(values)
                values_is_iterable = True
            except TypeError:
                pass
        if values or values_is_iterable:
            nentries = len(x)
            if self._weight:
                result.append(f"{pad(self.weight_legend, 13)}{fn(weight_total, 13, 3)}")
            if not self._level:
                result.append(f"{pad('entries', 13)}{fn(nentries, 13, 3)}")
            result.append("")
            if self._level:
                result.append(f"value                {rpad(self.weight_legend, 13)}     %")
            else:
                if self._weight:
                    result.append(f"value                {rpad(self.weight_legend, 13)}     % entries     %")
                else:
                    result.append("value               entries     %")

            if values_is_iterable:
                unique_values = []
                for v in values:
                    if v in unique_values:
                        raise ValueError(f"value {v} used more than once")
                    unique_values.append(v)

                if sort_on_weight or sort_on_duration or sort_on_value:
                    values_label = [v for v in self.values(ex0=ex0, sort_on_weight=sort_on_weight, sort_on_duration=sort_on_duration) if v in values]
                    values_not_in_monitor = [v for v in values if v not in values_label]
                    values_label.extend(sorted(values_not_in_monitor))
                else:
                    values_label = values
            else:
                values_label = self.values(ex0=ex0, sort_on_weight=sort_on_weight, sort_on_duration=sort_on_duration)

            values_condition = [[v] for v in values_label]
            rest_values = self.values(ex0=ex0)
            for v in values_label:
                if v in rest_values:
                    rest_values.remove(v)

            if rest_values:  # not possible via set subtraction as values may be not hashable
                values_condition.append(rest_values)
                values_label.append("<rest>")

            for value_condition, value_label in zip(values_condition, values_label):
                if self._level:
                    count = self.value_duration(value_condition)
                else:
                    if self._weight:
                        count = self.value_weight(value_condition)
                        count_entries = self.value_number_of_entries(value_condition)
                    else:
                        count = self.value_number_of_entries(value_condition)

                perc = count / (weight_total if weight_total else 1)

                n = int(perc * graph_scale)
                s = "*" * n

                if self._level:
                    result.append(pad(str(value_label), 20) + fn(count, 14, 3) + fn(perc * 100, 6, 1) + " " + s)
                else:
                    if self._weight:
                        result.append(
                            pad(str(value_label), 20)
                            + fn(count, 14, 3)
                            + fn(perc * 100, 6, 1)
                            + rpad(str(count_entries), 8)
                            + fn(count_entries * 100 / nentries, 6, 1)
                        )
                    else:
                        result.append(pad(str(value_label), 20) + rpad(str(count), 7) + fn(perc * 100, 6, 1) + " " + s)
        else:
            auto_scale = True
            if bin_width is None:
                bin_width = 1
            else:
                auto_scale = False
            if lowerbound is None:
                lowerbound = 0
            else:
                auto_scale = False
            if number_of_bins is None:
                number_of_bins = 30
            else:
                auto_scale = False

            if auto_scale:
                bin_width, lowerbound, number_of_bins = self.histogram_autoscale()
            result.append(self.print_statistics(show_header=False, show_legend=True, do_indent=False, as_str=True))
            if not self._stats_only and number_of_bins >= 0:
                result.append("")
                if self._weight:
                    result.append("           <= " + rpad(self.weight_legend, 13) + "     %  cum%")
                else:
                    result.append("           <=       entries     %  cum%")

                cumperc: float = 0
                for i in range(-1, number_of_bins + 1):
                    if i == -1:
                        lb = -inf
                    else:
                        lb = lowerbound + i * bin_width
                    if i == number_of_bins:
                        ub = inf
                    else:
                        ub = lowerbound + (i + 1) * bin_width
                    if self._weight:
                        count = self.sys_bin_weight(lb, ub)
                    else:
                        count = self.bin_number_of_entries(lb, ub)

                    perc = count / (weight_total if weight_total else 1)
                    if weight_total == inf:
                        s = ""
                    else:
                        cumperc += perc
                        n = int(perc * graph_scale)
                        ncum = int(cumperc * graph_scale) + 1
                        s = ("*" * n) + (" " * (graph_scale - n))
                        s = s[: ncum - 1] + "|" + s[ncum + 1 :]

                    result.append(f"{fn(ub, 13, 3)} {fn(count, 13, 3)}{fn(perc * 100, 6, 1)}{fn(cumperc * 100, 6, 1)} {s}")
        result.append("")
        return return_or_print(result, as_str=as_str, file=file)

    def values(self, ex0: bool = False, force_numeric: bool = False, sort_on_weight: bool = False, sort_on_duration: bool = False) -> List:
        """
        values

        Parameters
        ----------
        ex0 : bool
            if False (default), include zeroes. if True, exclude zeroes

        force_numeric : bool
            if True, convert non numeric tallied values numeric if possible, otherwise assume 0

            if False (default), do not interpret x-values, return as list if type is list

        sort_on_weight : bool
            if True, sort the values on weight first (largest first), then on the values itself

            if False, sort the values on the values itself

            False is the default for non level monitors. Not permitted for level monitors.

        sort_on_duration : bool
            if True, sort the values on duration first (largest first), then on the values itself

            if False, sort the values on the values itself

            False is the default for level monitors. Not permitted for non level monitors.

        Returns
        -------
        all tallied values : list
        """
        self._block_stats_only()
        x, _ = self._xweight(ex0, force_numeric)

        if self._level:
            if sort_on_weight:
                raise ValueError("level monitors can't be sorted on weight. Use sort_on_duration instead")
        else:
            if sort_on_duration:
                raise ValueError("non level monitors can't be sorted on duration. Use sort_on_weight instead")

        def key(x):
            if sort_on_weight:
                weight = -self.value_weight(x)
            elif sort_on_duration:
                weight = -self.value_duration(x)
            else:
                weight = 1

            try:
                return (weight, float(x), "")
            except (ValueError, TypeError):
                return (weight, math.inf, str(x).lower())

        x_unique = []  # not possible to use set() as items do not have to be hashable
        for item in x:
            if item not in x_unique:
                x_unique.append(item)

        return list(sorted(x_unique, key=key))

    def animate(self, *args, **kwargs):
        """
        animates the monitor in a panel

        Parameters
        ----------
        linecolor : colorspec
            color of the line or points (default foreground color)

        linewidth : int
            width of the line or points (default 1 for level, 3 for non level monitors)

        fillcolor : colorspec
            color of the panel (default transparent)

        bordercolor : colorspec
            color of the border (default foreground color)

        borderlinewidth : int
            width of the line around the panel (default 1)

        nowcolor : colorspec
            color of the line indicating now (default red)

        titlecolor : colorspec
            color of the title (default foreground color)

        titlefont : font
            font of the title (default null string)

        titlefontsize : int
            size of the font of the title (default 15)

        title : str
            title to be shown above panel

            default: name of the monitor

        x : int
            x-coordinate of panel, relative to xy_anchor, default 0

        y : int
            y-coordinate of panel, relative to xy_anchor. default 0

        offsetx : float
            offsets the x-coordinate of the panel (default 0)

        offsety : float
            offsets the y-coordinate of the panel (default 0)

        angle : float
            rotation angle in degrees, default 0

        xy_anchor : str
            specifies where x and y are relative to

            possible values are (default: sw):

            ``nw    n    ne``

            ``w     c     e``

            ``sw    s    se``

        vertical_offset : float
            the vertical position of x within the panel is
             vertical_offset + x * vertical_scale (default 0)

        vertical_scale : float
            the vertical position of x within the panel is
            vertical_offset + x * vertical_scale (default 5)

        horizontal_scale : float
            the relative horizontal position of time t within the panel is on
            t * horizontal_scale, possibly shifted (default 1)


        width : int
            width of the panel (default 200)

        height : int
            height of the panel (default 75)

        vertical_map : function
            when a y-value has to be plotted it will be translated by this function

            default: float

            when the function results in a TypeError or ValueError, the value 0 is assumed

            when y-values are non numeric, it is advised to provide an approriate map function, like:

            vertical_map = "unknown red green blue yellow".split().index

        labels : iterable
            labels to be shown on the vertical axis (default: empty tuple)

            the placement of the labels is controlled by the vertical_map method

        label_color : colorspec
            color of labels (default: foreground color)

        label_font : font
            font of the labels (default null string)

        label_fontsize : int
            size of the font of the labels (default 15)

        label_anchor : str
            specifies where the label coordinates (as returned by map_value) are relative to

            possible values are (default: e):

            ``nw    n    ne``

            ``w     c     e``

            ``sw    s    se``

        label_offsetx : float
            offsets the x-coordinate of the label (default 0)

        label_offsety : float
            offsets the y-coordinate of the label (default 0)

        label_linewidth : int
            width of the label line (default 1)

        label_linecolor : colorspec
            color of the label lines (default foreground color)

        layer : int
            layer (default 0)

        as_points : bool
            allows to override the as_points setting of tallies, which is
            by default False for level monitors and True for non level monitors

        parent : Component
            component where this animation object belongs to (default None)

            if given, the animation object will be removed
            automatically when the parent component is no longer accessible

        screen_coordinates : bool
            use screen_coordinates

            normally, the scale parameters are use for positioning and scaling
            objects.

            if True, screen_coordinates will be used instead.

        over3d : bool
            if True, this object will be rendered to the OpenGL window

            if False (default), the normal 2D plane will be used.

        Returns
        -------
        reference to AnimateMonitor object : AnimateMonitor

        Note
        ----
        All measures are in screen coordinates


        Note
        ----
        It is recommended to use sim.AnimateMonitor instead


        All measures are in screen coordinates

        """
        return AnimateMonitor(monitor=self, *args, **kwargs)

    def x(self, ex0: bool = False, force_numeric: bool = True) -> Union[List, array.array]:
        """
        array/list of tallied values

        Parameters
        ----------
        ex0 : bool
            if False (default), include zeroes. if True, exclude zeroes

        force_numeric : bool
            if True (default), convert non numeric tallied values numeric if possible, otherwise assume 0

            if False, do not interpret x-values, return as list if type is any (list)

        Returns
        -------
        all tallied values : array/list

        Note
        ----
        Not available for level monitors. Use xduration(), xt() or tx() instead.
        """
        self._block_stats_only()

        if self._level:
            raise TypeError("x not available for level monitors")
        return self._xweight(ex0=ex0, force_numeric=force_numeric)[0]

    def xweight(self, ex0: bool = False, force_numeric: bool = True) -> Union[List, array.array]:
        """
        array/list of tallied values

        Parameters
        ----------
        ex0 : bool
            if False (default), include zeroes. if True, exclude zeroes

        force_numeric : bool
            if True (default), convert non numeric tallied values numeric if possible, otherwise assume 0

            if False, do not interpret x-values, return as list if type is list

        Returns
        -------
        all tallied values : array/list

        Note
        ----
        not available for level monitors
        """
        self._block_stats_only()

        if self._level:
            raise TypeError("xweight not available for level monitors")
        return self._xweight(ex0, force_numeric)

    def xduration(self, ex0: bool = False, force_numeric: bool = True) -> Tuple:
        """
        array/list of tallied values

        Parameters
        ----------
        ex0 : bool
            if False (default), include zeroes. if True, exclude zeroes

        force_numeric : bool
            if True (default), convert non numeric tallied values numeric if possible, otherwise assume 0

            if False, do not interpret x-values, return as list if type is list

        Returns
        -------
        tuple of tallied value and duration : array/list

        Note
        ----
        not available for non level monitors
        """
        self._block_stats_only()

        if not self._level:
            raise TypeError("xduration not available for non level monitors")
        return self._xweight(ex0, force_numeric)

    def xt(self, ex0: bool = False, exoff=False, force_numeric: bool = True, add_now: bool = True) -> Tuple:
        """
        tuple of array/list with x-values and array with timestamp

        Parameters
        ----------
        ex0 : bool
            if False (default), include zeroes. if True, exclude zeroes

        exoff : bool
            if False (default), include self.off. if True, exclude self.off's

            non level monitors will return all values, regardless of exoff

        force_numeric : bool
            if True (default), convert non numeric tallied values numeric if possible, otherwise assume 0

            if False, do not interpret x-values, return as list if type is list

        add_now : bool
            if True (default), the last tallied x-value and the current time is added to the result

            if False, the result ends with the last tallied value and the time that was tallied

            non level monitors will never add now

            if now is <= last tallied value, nothing will be added, even if add_now is True

        Returns
        -------
        array/list with x-values and array with timestamps : tuple

        Note
        ----
        The value self.off is stored when monitoring is turned off

        The timestamps are not corrected for any reset_now() adjustment.
        """
        self._block_stats_only()

        if not self._level:
            exoff = False
            add_now = False

        if self.xtypecode or (not force_numeric):
            x = self._x
            typecode = self.xtypecode
            off = self.off
        else:
            x = do_force_numeric(self._x)
            typecode = ""
            off = -inf  # float

        if typecode:
            xx = array.array(typecode)
        else:
            xx = []
        t = array.array("d")
        if add_now:
            addx = [x[-1]]
            t_extra = self.env._t if self.env._animate else self.env._now
            addt = [t_extra]
        else:
            addx = []
            addt = []

        for vx, vt in zip(itertools.chain(x, addx), itertools.chain(self._t, addt)):
            if not ex0 or (vx != 0):
                if not exoff or (vx != off):
                    xx.append(vx)
                    t.append(vt)

        return xx, t

    def tx(self, ex0: bool = False, exoff: bool = False, force_numeric: bool = False, add_now: bool = True) -> Tuple:
        """
        tuple of array with timestamps and array/list with x-values

        Parameters
        ----------
        ex0 : bool
            if False (default), include zeroes. if True, exclude zeroes

        exoff : bool
            if False (default), include self.off. if True, exclude self.off's

            non level monitors will return all values, regardless of exoff

        force_numeric : bool
            if True (default), convert non numeric tallied values numeric if possible, otherwise assume 0

            if False, do not interpret x-values, return as list if type is list

        add_now : bool
            if True (default), the last tallied x-value and the current time is added to the result

            if False, the result ends with the last tallied value and the time that was tallied

            non level monitors will never add now

        Returns
        -------
        array with timestamps and array/list with x-values : tuple

        Note
        ----
        The value self.off is stored when monitoring is turned off

        The timestamps are not corrected for any reset_now() adjustment.
        """
        self._block_stats_only()

        return tuple(reversed(self.xt(ex0=ex0, exoff=exoff, force_numeric=force_numeric, add_now=add_now)))

    def _xweight(self, ex0=False, force_numeric=True):
        t_extra = self.env._t if self.env._animate else self.env._now

        if (ex0, force_numeric) in self.cached_xweight:
            if self.cached_xweight[(ex0, force_numeric)][0] == t_extra:
                return self.cached_xweight[(ex0, force_numeric)][1]

        if self.xtypecode or (not force_numeric):
            x = self._x
            typecode = self.xtypecode
        else:
            x = do_force_numeric(self._x)
            typecode = ""

        if self._level:
            weightall = array.array("d")
            lastt = None
            for t in self._t:
                if lastt is not None:
                    weightall.append(t - lastt)
                lastt = t
            weightall.append(t_extra - lastt)

            weight = array.array("d")
            if typecode:
                xx = array.array(typecode)
            else:
                xx = []

            for vx, vweight in zip(x, weightall):
                if vx != self.off:
                    if vx != 0 or not ex0:
                        xx.append(vx)
                        weight.append(vweight)
            xweight = (xx, weight)
        else:
            if ex0:
                x0 = [vx for vx in x if vx != 0]
                if typecode:
                    x0 = array.array(typecode, x)

            if self._weight:
                if ex0:
                    xweight = (x0, array.array("d", [vweight for vx, vweight in zip(x, self._weight) if vx != 0]))
                else:
                    xweight = (x, self._weight)
            else:
                if ex0:
                    xweight = (x0, array.array("d", (1,) * len(x0)))
                else:
                    xweight = (x, array.array("d", (1,) * len(x)))

        self.cached_xweight[(ex0, force_numeric)] = (t_extra, xweight)
        return xweight

    def as_dataframe(
        self, include_t: bool = True, use_datetime0=False, ex0: bool = False, exoff=False, force_numeric: bool = True, add_now: bool = True
    ) -> "dataframe":
        """
        makes a pandas dataframe with the x-values and optionally the t-values of the monitors

        The x column names will be the name of the monitor, suffixed with ".x".

        Parameters
        ----------
        include_t : bool
            if True (default), include the t values in the dataframe

            if False, do not include t values in the dataframe

        use_datetime0 : bool
            if False (default), use t-values as such

            if True, use datetime.datetime as t-values (only allowed datetime0 is set for the environment)

        Returns
        -------
        dataframe containing x (and t) values : pandas dataframe

        Notes
        -----
        Requires pandas to be installed

        For level monitors, Monitor.as_resampled_dataframe is likely more useful
        """
        try:
            import pandas as pd
        except ImportError:
            raise ImportError("Monitor.as_dataframe requires pandas")
        xs, ts = self.xt(ex0=ex0, exoff=exoff, force_numeric=force_numeric, add_now=add_now)
        if include_t:
            if use_datetime0:
                df = pd.DataFrame({"t": [self.env.t_to_datetime(t) for t in ts]})
            else:
                df = pd.DataFrame({"t": ts})
        else:
            df = pd.DataFrame()
        df[f"{self.name()}.x"] = xs

        return df

    def as_resampled_dataframe(
        self,
        extra_monitors: Iterable = [],
        delta_t: Union[float, datetime.timedelta] = 1,
        min_t: Union[float, datetime.datetime] = None,
        max_t: Union[float, datetime.datetime] = None,
        use_datetime0=False,
    ) -> "dataframe":
        """
        makes a pandas dataframe with t, and x_values for the monitor(s)

        the t values will be uniformly distributes between min_t and max_t with a time step of delta_t

        this is essentially the result of a resampling process. It is guaranteed that the values
        at the given times are correct.

        The x column names will be the name of the monitor, suffixed with ".x".

        Parameters
        ----------
        extra_monitors : iterable of level monitors
            monitors to be included in the dataframe


        delta_t : float or datetime.timedelta
            time step (default: 1)

            specification as datetime.timedelta only allowed if use_datetime0=True

        min_t : float or datetime.datetime
            start of the resampled time (default: start time of the monitor)

            specification as datetime.datetime only allowed if use_datetime0=True

        max_t : float or datetime.datetime
            end of the resampled time (default: env.now())

            specification as datetime.datetime only allowed if use_datetime0=True

        use_datetime0 : bool
            if False (default), use t-values as such

            if True, use datetime.datetime as t-values (only allowed datetime0 is set for the environment)

        Returns
        -------
        dataframe containing t and x values : pandas dataframe

        Notes
        -----
        Requires pandas to be installed
        """
        try:
            import pandas as pd
        except ImportError:
            raise ImportError("Monitor.as_dataframe requires pandas")
        if use_datetime0 and not self.env._datetime0:
            raise ValueError("use_date_time0=True only allowed of env.datetime0 is set")

        if delta_t is None:
            delta_t = 1
        else:
            if isinstance(delta_t, datetime.timedelta):
                if not use_datetime0:
                    raise TypeError("delta_t can't be a datetime.timedelta if use_datetime0=False")
                delta_t = self.env.timedelta_to_duration(delta_t)

        if min_t is None:
            min_t = self._t[0]
        else:
            if isinstance(min_t, datetime.datetime):
                if not use_datetime0:
                    raise TypeError("min_t can't be a datetime.datetime if use_datetime0=False")

                min_t = self.env.datetime_to_t(min_t)
            min_t += self.env._offset

        if max_t is None:
            max_t = self.env._now
        else:
            if isinstance(max_t, datetime.datetime):
                if not use_datetime0:
                    raise TypeError("max_t can't be a datetime.datetime if use_datetime0=False")
                max_t = self.env.datetime_to_t(max_t)
            max_t += self.env._offset
        if use_datetime0:
            df = pd.DataFrame({"t": [self.env.t_to_datetime(t) for t in self.env.arange(min_t, max_t, delta_t)]})
        else:
            df = pd.DataFrame({"t": self.env.arange(min_t, max_t, delta_t)})

        for mon in [self] + extra_monitors:
            if not mon._level:
                raise ValueError("not all monitors are level")
            if mon._t[0] != self._t[0]:
                raise ValueError("not all monitors have the same start time")
            if mon.env != self.env:
                raise ValueError("not all monitors have the environment")
            x_last = mon.off
            xt_iter = iter(zip(mon._x, mon._t))
            x_next, t_next = next(xt_iter)
            new_x = []
            for t in self.env.arange(min_t, max_t, delta_t):
                while t >= t_next:
                    x_last = x_next
                    try:
                        x_next, t_next = next(xt_iter)
                    except StopIteration:
                        t_next = inf
                new_x.append(pd.NA if x_last == self.off else x_last)
            df[f"{mon.name()}.x"] = new_x
        return df


class _CapacityMonitor(Monitor):
    @property
    def value(self):
        return self._tally

    @value.setter
    def value(self, value):
        self.parent.set_capacity(value)


class _ModeMonitor(Monitor):
    def __init__(self, parent, *args, **kwargs):
        self.parent = parent
        super().__init__(*args, **kwargs)

    @property
    def value(self) -> Any:
        return self._tally

    @value.setter
    def value(self, value: Any) -> None:
        self.parent.set_mode(value)


class _StatusMonitor(Monitor):
    @property
    def _value(self) -> Any:
        # this is just defined to be able to make the setter
        return self._tally

    @_value.setter
    def _value(self, value: Any) -> None:
        self.tally(value)

    @property
    def value(self) -> Any:
        return self._tally

    @value.setter
    def value(self, value: Any) -> None:
        raise ValueError("not possible to use status.value")


class _StateMonitor(Monitor):
    def __init__(self, parent, *args, **kwargs):
        self.parent = parent
        super().__init__(*args, **kwargs)

    @property
    def _value(self) -> Any:
        return self.parent.get()

    @_value.setter
    def _value(self, value: Any) -> None:
        self.parent.set(value)

    @property
    def value(self) -> Any:
        return self.parent.get()

    @value.setter
    def value(self, value: Any) -> None:
        self.parent.set(value)


class _SystemMonitor(Monitor):
    @property
    def value(self) -> Any:
        return self._tally


class DynamicClass:
    def __init__(self):
        self._dynamics = set()

    def register_dynamic_attributes(self, attributes):
        """
        Registers one or more attributes as being dynamic

        Parameters
        ----------
        attributes : str
            a specification of attributes to be registered as dynamic

            e.g. "x y"
        """
        if isinstance(attributes, str):
            attributes = attributes.split()
        for attribute in attributes:
            if hasattr(self, attribute):
                self._dynamics.add((attribute))
            else:
                raise ValueError(f"attribute {attribute} does not exist")

    def deregister_dynamic_attributes(self, attributes):
        """
        Deregisters one or more attributes as being dynamic

        Parameters
        ----------
        attributes : str
            a specification of attributes to be registered as dynamic

            e.g. "x y"
        """

        if isinstance(attributes, str):
            attributes = attributes.split()
        for attribute in attributes:
            if hasattr(self, attribute):
                self._dynamics.remove((attribute))
            else:
                raise ValueError(f"attribute {attribute} does not exist")

    def __getattribute__(self, attr):
        if attr == "_dynamics":
            return super().__getattribute__(attr)

        c = super().__getattribute__(attr)
        if attr not in self._dynamics:
            return c
        if callable(c):
            if inspect.isfunction(c):
                nargs = c.__code__.co_argcount
                if c.__defaults__ is not None:
                    c.__defaults__ = tuple(self if x == object else x for x in c.__defaults__)  # indicate that object refers to animation object itself
                    nargs -= len(c.__defaults__)

                if nargs == 0:
                    return lambda t: c()
                if nargs == 1:
                    return c
                return functools.partial(c, self.arg)
            if inspect.ismethod(c):
                return c
        return lambda t: c

    def getattribute_spec(self, attr):
        """
        special version of getattribute.
        When it's dynamic it will return the value in case of a constant or a parameterless function
        Used only in AnimateCombined
        """

        if attr == "_dynamics":
            return super().__getattribute__(attr)

        c = super().__getattribute__(attr)
        if attr not in self._dynamics:
            return c
        if callable(c):
            if inspect.isfunction(c):
                nargs = c.__code__.co_argcount
                if c.__defaults__ is not None:
                    c.__defaults__ = tuple(self if x == object else x for x in c.__defaults__)  # indicate that object refers to animation object itself
                    nargs -= len(c.__defaults__)
                if nargs == 0:
                    return c()
                if nargs == 1:
                    return c
                return functools.partial(c, self.arg)
            if inspect.ismethod(c):
                return c
        return c

    def __call__(self, **kwargs):
        for k, v in kwargs.items():
            if hasattr(self, k):
                setattr(self, k, v)
            else:
                raise AttributeError(f"attribute {k} does not exist")

    def add_attr(self, **kwargs):
        for k, v in kwargs.items():
            if hasattr(self, k):
                raise AttributeError("attribute " + k + " already set")
            setattr(self, k, v)
        return self


class AnimateMonitor(DynamicClass):
    """
    animates a monitor in a panel

    Parameters
    ----------
    monitor : Monitor
        monitor to be animated

    linecolor : colorspec
        color of the line or points (default foreground color)

    linewidth : int
        width of the line or points (default 1 for level, 3 for non level monitors)

    fillcolor : colorspec
        color of the panel (default transparent)

    bordercolor : colorspec
        color of the border (default foreground color)

    borderlinewidth : int
        width of the line around the panel (default 1)

    nowcolor : colorspec
        color of the line indicating now (default red)

    titlecolor : colorspec
        color of the title (default foreground color)

    titlefont : font
        font of the title (default null string)

    titlefontsize : int
        size of the font of the title (default 15)

    title : str
        title to be shown above panel

        default: name of the monitor

    x : int
        x-coordinate of panel, relative to xy_anchor, default 0

    y : int
        y-coordinate of panel, relative to xy_anchor. default 0

    offsetx : float
        offsets the x-coordinate of the panel (default 0)

    offsety : float
        offsets the y-coordinate of the panel (default 0)

    angle : float
        rotation angle in degrees, default 0

    xy_anchor : str
        specifies where x and y are relative to

        possible values are (default: sw):

        ``nw    n    ne``

        ``w     c     e``

        ``sw    s    se``

    vertical_offset : float
        the vertical position of x within the panel is
         vertical_offset + x * vertical_scale (default 0)

    vertical_scale : float
        the vertical position of x within the panel is
        vertical_offset + x * vertical_scale (default 5)

    horizontal_scale : float
        the relative horizontal position of time t within the panel is on
        t * horizontal_scale, possibly shifted (default 1)


    width : int
        width of the panel (default 200)

    height : int
        height of the panel (default 75)

    vertical_map : function
        when a y-value has to be plotted it will be translated by this function

        default: float

        when the function results in a TypeError or ValueError, the value 0 is assumed

        when y-values are non numeric, it is advised to provide an approriate map function, like:

        vertical_map = "unknown red green blue yellow".split().index

    labels : iterable or dict
        if an iterable, these are the values of the labels to be shown

        if a dict, the keys are the values of the labels, the keys are the texts to be shown

        labels will be shown on the vertical axis (default: empty tuple)

        the placement of the labels is controlled by the vertical_map method

    label_color : colorspec
        color of labels (default: foreground color)

    label_font : font
        font of the labels (default null string)

    label_fontsize : int
        size of the font of the labels (default 15)

    label_anchor : str
        specifies where the label coordinates (as returned by map_value) are relative to

        possible values are (default: e):

        ``nw    n    ne``

        ``w     c     e``

        ``sw    s    se``

    label_offsetx : float
        offsets the x-coordinate of the label (default 0)

    label_offsety : float
        offsets the y-coordinate of the label (default 0)

    label_linewidth : int
        width of the label line (default 1)

    label_linecolor : colorspec
        color of the label lines (default foreground color)

    layer : int
        layer (default 0)

    as_points : bool
        allows to override the line/point setting, which is by default False for level
        monitors and True for non level monitors

    parent : Component
        component where this animation object belongs to (default None)

        if given, the animation object will be removed
        automatically when the parent component is no longer accessible

    over3d : bool
        if True, this object will be rendered to the OpenGL window

        if False (default), the normal 2D plane will be used.

    visible : bool
        visible

        if False, animation monitor is not shown, shown otherwise
        (default True)

    screen_coordinates : bool
        use screen_coordinates

        if False,  the scale parameters are use for positioning and scaling
        objects.

        if True (default), screen_coordinates will be used.

    Note
    ----
    All measures are in screen coordinates

    """

    def __init__(
        self,
        monitor: "Monitor",
        linecolor: Union[ColorType, Callable] = "fg",
        linewidth: Union[float, Callable] = None,
        fillcolor: Union[Callable, ColorType] = "",
        bordercolor: Union[ColorType, Callable] = "fg",
        borderlinewidth: Union[float, Callable] = 1,
        titlecolor: Union[ColorType, Callable] = "fg",
        nowcolor: Union[ColorType, Callable] = "red",
        titlefont: Union[str, Callable] = "",
        titlefontsize: Union[float, Callable] = 15,
        title: Union[str, Callable] = None,
        x: Union[float, Callable] = 0,
        y: Union[float, Callable] = 0,
        offsetx: Union[float, Callable] = 0,
        offsety: Union[float, Callable] = 0,
        angle: Union[float, Callable] = 0,
        vertical_offset: Union[float, Callable] = 0,
        parent: "Component" = None,
        vertical_scale: Union[float, Callable] = 5,
        horizontal_scale: Union[float, Callable] = 1,
        width: Union[float, Callable] = 200,
        height: Union[float, Callable] = 75,
        xy_anchor: Union[str, Callable] = "sw",
        vertical_map: Callable = float,
        labels: Union[Iterable, Dict] = (),
        label_color: Union[ColorType, Callable] = "fg",
        label_font: Union[str, Callable] = "",
        label_fontsize: Union[float, Callable] = 15,
        label_anchor: Union[str, Callable] = "e",
        label_offsetx: Union[float, Callable] = 0,
        label_offsety: Union[float, Callable] = 0,
        label_linewidth: Union[float, Callable] = 1,
        label_linecolor: ColorType = "fg",
        as_points: bool = None,
        over3d: bool = None,
        layer: Union[float, Callable] = 0,
        visible: Union[bool, Callable] = True,
        keep: Union[bool, Callable] = True,
        screen_coordinates: bool = True,
        arg: Any = None,
    ):
        super().__init__()
        _checkismonitor(monitor)
        monitor._block_stats_only()

        if title is None:
            title = monitor.name()

        if linewidth is None:
            linewidth = 1 if monitor._level else 3

        if over3d is None:
            over3d = default_over3d()

        offsetx += monitor.env.xy_anchor_to_x(xy_anchor, screen_coordinates=True, over3d=over3d)
        offsety += monitor.env.xy_anchor_to_y(xy_anchor, screen_coordinates=True, over3d=over3d)

        self.linecolor = linecolor
        self.linewidth = linewidth
        self.fillcolor = fillcolor
        self.bordercolor = bordercolor
        self.borderlinewidth = borderlinewidth
        self.titlecolor = titlecolor
        self.nowcolor = nowcolor
        self.titlefont = titlefont
        self.titlefontsize = titlefontsize
        self.title = title
        self.x = x
        self.y = y
        self.offsetx = offsetx
        self.offsety = offsety
        self.angle = angle
        self.vertical_offset = vertical_offset
        self.parent = parent
        self.vertical_scale = vertical_scale
        self.horizontal_scale = horizontal_scale
        self.width = width
        self.height = height
        self.xy_anchor = xy_anchor
        self.vertical_map = vertical_map
        self.labels = labels
        self.label_color = label_color
        self.label_font = label_font
        self.label_fontsize = label_fontsize
        self.label_anchor = label_anchor
        self.label_offsetx = label_offsetx
        self.label_offsety = label_offsety
        self.label_linewidth = label_linewidth
        self.label_linecolor = label_linecolor
        self.layer = layer
        self.visible = visible
        self.keep = keep
        self.arg = self if arg is None else arg
        self.as_points = not monitor._level if as_points is None else as_points
        self._monitor = monitor
        self.as_level = monitor._level
        self.over3d = over3d
        self.screen_coordinates = screen_coordinates
        self.register_dynamic_attributes(
            "linecolor linewidth fillcolor bordercolor borderlinewidth titlecolor nowcolor titlefont titlefontsize title "
            "x y offsetx offsety angle vertical_offset parent vertical_scale horizontal_scale width height "
            "xy_anchor labels label_color label_font label_fontsize label_anchor label_offsetx label_offsety "
            "label_linewidth label_linecolor layer visible keep"
        )

        if parent is not None:
            if not isinstance(parent, Component):
                raise ValueError(repr(parent) + " is not a component")
            parent._animation_children.add(self)
        self.env = monitor.env
        self.ao_frame_fill = AnimateRectangle(
            spec=lambda: (0, 0, self.width_t, self.height_t),
            x=lambda: self.x_t,
            y=lambda: self.y_t,
            offsetx=lambda: self.offsetx_t,
            offsety=lambda: self.offsety_t,
            angle=lambda: self.angle_t,
            fillcolor=lambda t: self.fillcolor(t),
            linewidth=lambda t: self.borderlinewidth(t),
            linecolor="",
            screen_coordinates=self.screen_coordinates,
            layer=lambda: self.layer_t + 0.2,  # to make it appear behind label lines and plot line/points
            over3d=self.over3d,
            visible=lambda: self.visible_t,
        )
        self.ao_frame_line = AnimateRectangle(
            spec=lambda: (0, 0, self.width_t, self.height_t),
            x=lambda: self.x_t,
            y=lambda: self.y_t,
            offsetx=lambda: self.offsetx_t,
            offsety=lambda: self.offsety_t,
            angle=lambda: self.angle_t,
            fillcolor="",
            linewidth=lambda t: self.borderlinewidth(t),
            linecolor=lambda t: self.bordercolor(t),
            screen_coordinates=self.screen_coordinates,
            layer=lambda: self.layer_t,
            over3d=self.over3d,
            visible=lambda: self.visible_t,
        )
        self.ao_text = AnimateText(
            text=lambda t: self.title(t),
            textcolor=lambda t: self.titlecolor(t),
            x=lambda: self.x_t,
            y=lambda: self.y_t,
            offsetx=lambda: self.offsetx_t,
            offsety=lambda t: self.offsety_t + self.height_t + self.titlefontsize(t) * 0.15,
            angle=lambda: self.angle_t,
            text_anchor="sw",
            fontsize=lambda t: self.titlefontsize(t),
            font=lambda t: self.titlefont(t),
            layer=lambda t: self.layer_t,
            over3d=self.over3d,
            visible=lambda: self.visible_t,
            screen_coordinates=self.screen_coordinates,
        )

        self.ao_line = AnimateLine(
            spec=lambda t: self.line(t),
            x=lambda: self.x_t,
            y=lambda: self.y_t,
            offsetx=lambda: self.offsetx_t,
            offsety=lambda: self.offsety_t,
            angle=lambda: self.angle_t,
            linewidth=lambda t: self.linewidth(t),
            linecolor=lambda t: self.linecolor(t),
            as_points=self.as_points,
            layer=lambda: self.layer_t,
            over3d=self.over3d,
            visible=lambda: self.visible_t,
            screen_coordinates=self.screen_coordinates,
        )

        self.ao_now_line = AnimateLine(
            spec=lambda t: self.now_line(t),
            x=lambda: self.x_t,
            y=lambda: self.y_t,
            offsetx=lambda: self.offsetx_t,
            offsety=lambda: self.offsety_t,
            angle=lambda: self.angle_t,
            linecolor=lambda t: self.nowcolor(t),
            layer=lambda: self.layer_t,
            over3d=self.over3d,
            visible=lambda: self.visible_t,
            screen_coordinates=self.screen_coordinates,
        )

        self.ao_label_texts = []
        self.ao_label_lines = []

        self.show()

    def t_to_x(self, t):
        t -= self.t_start
        if self.displacement_t < 0:
            t += self.displacement_t
            if t < 0:
                t = 0
                self.done = True
        x = t * self.horizontal_scale_t
        return max(0, min(self.width_t, x))

    def value_to_y(self, value):
        if value == self._monitor.off:
            value = 0
        else:
            try:
                value = self.vertical_map(value)

            except (ValueError, TypeError):
                value = 0
        return max(0, min(self.height_t, value * self.vertical_scale_t + self.vertical_offset_t))

    def line(self, t):
        result = []
        if len(self._monitor._x) != 0:
            value = self._monitor._x[-1]
        else:
            value = 0
        lastt = t + self._monitor.env._offset
        if self.as_level:
            result.append(self.t_to_x(lastt))
            result.append(self.value_to_y(value))
        self.done = False
        for value, t in zip(reversed(self._monitor._x), reversed(self._monitor._t)):
            if self.as_level:
                result.append(self.t_to_x(lastt))
                result.append(self.value_to_y(value))
            result.append(self.t_to_x(t))
            result.append(self.value_to_y(value))
            if self.done:
                if not self.as_level:
                    result.pop()  # remove the last outlier x
                    result.pop()  # remove the last outlier y
                break
            lastt = t
        return result

    def now_line(self, t):
        t -= self._monitor.start - self._monitor.env._offset
        t = min(t, self.width_div_horizontal_scale_t)
        x = t * self.horizontal_scale_t
        return x, 0, x, self.height_t

    def update(self, t):
        if not self.keep(t):
            self.remove()
            return

        self.width_t = self.width(t)
        self.height_t = self.height(t)
        self.x_t = self.x(t)
        self.y_t = self.y(t)
        self.offsetx_t = self.offsetx(t)
        self.offsety_t = self.offsety(t)
        self.angle_t = self.angle(t)
        self.layer_t = self.layer(t)
        self.visible_t = self.visible(t)
        self.vertical_scale_t = self.vertical_scale(t)
        self.vertical_offset_t = self.vertical_offset(t)
        self.horizontal_scale_t = self.horizontal_scale(t)
        self.linewidth_t = self.linewidth(t)
        self.t_start = self._monitor.start
        self.width_div_horizontal_scale_t = self.width_t / self.horizontal_scale_t
        self.displacement_t = self.width_div_horizontal_scale_t - (t - self.t_start)

        labels = []
        label_ys = []

        _labels = self.labels(t)

        for value in _labels:
            if isinstance(_labels, dict):
                text = _labels[value]
            else:
                text = value

            try:
                label_y = self.vertical_map(value) * self.vertical_scale_t + self.vertical_offset_t
                if 0 <= label_y <= self.height_t:
                    labels.append(text)
                    label_ys.append(label_y)
            except (ValueError, TypeError):
                pass

        for label, label_y, ao_label_text, ao_label_line in itertools.zip_longest(labels, label_ys, self.ao_label_texts[:], self.ao_label_lines[:]):
            if label is None:
                ao_label_text = self.ao_label_texts.pop()
                ao_label_line = self.ao_label_lines.pop()
                ao_label_text.remove()
                ao_label_line.remove()
            else:
                if ao_label_text is None:
                    ao_label_text = AnimateText(screen_coordinates=self.screen_coordinates)
                    ao_label_line = AnimateLine(screen_coordinates=self.screen_coordinates)
                    self.ao_label_texts.append(ao_label_text)
                    self.ao_label_lines.append(ao_label_line)
                ao_label_text.text = str(label)
                ao_label_text.textcolor = self.label_color(t)
                ao_label_text.x = self.x_t
                ao_label_text.y = self.y_t
                ao_label_text.offsetx = self.offsetx_t + self.label_offsetx(t)
                ao_label_text.offsety = self.offsety_t + self.label_offsety(t) + label_y
                ao_label_text.angle = self.angle_t
                ao_label_text.text_anchor = self.label_anchor(t)
                ao_label_text.fontsize = self.label_fontsize(t)
                ao_label_text.font = self.label_font(t)
                ao_label_text.layer = self.layer_t
                ao_label_text.over3d = self.over3d
                ao_label_text.visible = self.visible_t
                ao_label_text.screen_coordinates = self.screen_coordinates

                ao_label_line.spec = (0, 0, self.width_t, 0)
                ao_label_line.x = self.x_t
                ao_label_line.y = self.y_t
                ao_label_line.offsetx = self.offsetx_t
                ao_label_line.offsety = self.offsety_t + label_y
                ao_label_line.angle = self.angle_t
                ao_label_line.linewidth = self.label_linewidth(t)
                ao_label_line.linecolor = self.label_linecolor(t)
                ao_label_line.layer = self.layer_t + 0.2  # to make it appear behind the plot line/points
                ao_label_line.over3d = self.over3d
                ao_label_line.visible = self.visible_t
                ao_label_line.screen_coordinates = self.screen_coordinates

    def monitor(self) -> "Monitor":
        """
        Returns
        -------
        monitor this animation object refers to : Monitor
        """
        return self._monitor

    def show(self) -> None:
        """
        show (unremove)

        It is possible to use this method if already shown
        """
        self.ao_frame_line.show()
        self.ao_frame_fill.show()
        self.ao_text.show()
        self.ao_line.show()
        self.ao_now_line.show()
        self.env.sys_objects.add(self)

    def remove(self) -> None:
        """
        removes the animate object and thus closes this animation
        """
        self.ao_frame_line.remove()
        self.ao_frame_fill.remove()
        self.ao_text.remove()
        self.ao_line.remove()
        self.ao_now_line.remove()
        for ao in self.ao_label_texts:
            ao.remove()
        self.ao_label_texts = []
        for ao in self.ao_label_lines:
            ao.remove()
        self.ao_label_lines = []

        self.env.sys_objects.discard(self)

    def is_removed(self) -> bool:
        return self in self.env.sys_objects


if Pythonista:

    class AnimationScene(scene.Scene):
        def __init__(self, env, *args, **kwargs):
            scene.Scene.__init__(self, *args, **kwargs)

        def setup(self):
            if g.animation_env.retina:
                self.bg = None

        def touch_ended(self, touch):
            env = g.animation_env
            if env is not None:
                for uio in env.ui_objects:
                    ux = uio.x + env.xy_anchor_to_x(uio.xy_anchor, screen_coordinates=True, retina_scale=True)
                    uy = uio.y + env.xy_anchor_to_y(uio.xy_anchor, screen_coordinates=True, retina_scale=True)
                    if uio.type == "button":
                        if touch.location in scene.Rect(ux - 2, uy - 2, uio.width + 2, uio.height + 2):
                            uio.action()
                            break  # new buttons might have been installed
                    if uio.type == "slider":
                        if touch.location in scene.Rect(ux - 2, uy - 2, uio.width + 4, uio.height + 4):
                            xsel = touch.location[0] - ux
                            uio._v = uio.vmin + round(-0.5 + xsel / uio.xdelta) * uio.resolution
                            uio._v = max(min(uio._v, uio.vmax), uio.vmin)
                            if uio.action is not None:
                                uio.action(str(uio._v))
                                break  # new items might have been installed

        def draw(self):
            env = g.animation_env
            g.in_draw = True
            if (env is not None) and env._animate and env.running:
                scene.background(env.pythonistacolor("bg"))

                if env._synced or env._video:  # video forces synced
                    if env._video:
                        env._t = env.video_t
                    else:
                        if env._paused:
                            env._t = env.animation_start_time
                        else:
                            env._t = env.animation_start_time + ((time.time() - env.animation_start_clocktime) * env._speed)
                    while (env.peek() < env._t) and env.running and env._animate:
                        env.step()

                else:
                    if (env._step_pressed or (not env._paused)) and env._animate:
                        env.step()
                        env._t = env._now
                        if not env._current_component._suppress_pause_at_step:
                            env._step_pressed = False
                if not env._paused:
                    env.frametimes.append(time.time())
                touchvalues = self.touches.values()
                if env.retina > 1:
                    with io.BytesIO() as fp:
                        env._capture_image("RGB", include_topleft=True).save(fp, "BMP")
                        img = ui.Image.from_data(fp.getvalue(), env.retina)
                    if self.bg is None:
                        self.bg = scene.SpriteNode(scene.Texture(img))
                        self.add_child(self.bg)
                        self.bg.position = self.size / 2
                        self.bg.z_position = 10000
                    else:
                        self.bg.texture = scene.Texture(img)
                else:
                    env.animation_pre_tick(env.t())
                    env.animation_pre_tick_sys(env.t())
                    capture_image = env._capture_image("RGBA", include_topleft=True)
                    env.animation_post_tick(env.t)
                    try:
                        ims = scene.load_pil_image(capture_image)
                    except SystemError:
                        im_file = "temp.png"  # hack for Pythonista 3.4
                        capture_image.save(im_file, "PNG")
                        ims = scene.load_image_file(im_file)
                    scene.image(ims, 0, 0, *capture_image.size)
                    scene.unload_image(ims)
                if env._video and (not env._paused):
                    env._save_frame()
                    env.video_t += env._speed / env._real_fps

                for uio in env.ui_objects:
                    if not uio.installed:
                        uio.install()
                    ux = uio.x + env.xy_anchor_to_x(uio.xy_anchor, screen_coordinates=True, retina_scale=True)
                    uy = uio.y + env.xy_anchor_to_y(uio.xy_anchor, screen_coordinates=True, retina_scale=True)

                    if uio.type == "entry":
                        raise NotImplementedError("AnimateEntry not supported on Pythonista")
                    if uio.type == "button":
                        linewidth = uio.linewidth
                        scene.push_matrix()
                        scene.fill(env.pythonistacolor(uio.fillcolor))
                        scene.stroke(env.pythonistacolor(uio.linecolor))
                        scene.stroke_weight(linewidth)
                        scene.rect(ux - 4, uy + 2, uio.width + 8, uio.height - 4)
                        scene.tint(env.pythonistacolor(uio.color))
                        scene.translate(ux + uio.width / 2, uy + uio.height / 2)
                        scene.text(uio.text(), uio.font, uio.fontsize, alignment=5)
                        scene.tint(1, 1, 1, 1)
                        # required for proper loading of images
                        scene.pop_matrix()
                    elif uio.type == "slider":
                        scene.push_matrix()
                        scene.tint(env.pythonistacolor(uio.foreground_color))
                        v = uio.vmin
                        x = ux + uio.xdelta / 2
                        y = uy
                        mindist = inf
                        v = uio.vmin
                        while v <= uio.vmax:
                            if abs(v - uio._v) < mindist:
                                mindist = abs(v - uio._v)
                                vsel = v
                            v += uio.resolution
                        thisv = uio._v
                        for touch in touchvalues:
                            if touch.location in scene.Rect(ux, uy, uio.width, uio.height):
                                xsel = touch.location[0] - ux
                                vsel = round(-0.5 + xsel / uio.xdelta) * uio.resolution
                                thisv = vsel
                        scene.stroke(env.pythonistacolor(uio.foreground_color))
                        v = uio.vmin
                        xfirst = -1
                        while v <= uio.vmax:
                            if xfirst == -1:
                                xfirst = x
                            if v == vsel:
                                scene.stroke_weight(3)
                            else:
                                scene.stroke_weight(1)
                            scene.line(x, y, x, y + uio.height)
                            v += uio.resolution
                            x += uio.xdelta

                        scene.push_matrix()
                        scene.stroke(env.pythonistacolor(uio.foreground_color))
                        scene.translate(xfirst, uy + uio.height + 2)
                        if uio._label:
                            scene.text(uio._label, uio.font, uio.fontsize, alignment=9)
                        scene.pop_matrix()
                        scene.translate(ux + uio.width, uy + uio.height + 2)
                        scene.text(str(thisv) + " ", uio.font, uio.fontsize, alignment=7)
                        scene.tint(1, 1, 1, 1)
                        # required for proper loading of images later
                        scene.pop_matrix()
            else:
                width, height = ui.get_screen_size()
                scene.pop_matrix()
                scene.tint(1, 1, 1, 1)
                scene.translate(width / 2, height / 2)
                scene.text("salabim animation paused/stopped")
                scene.pop_matrix()
                scene.tint(1, 1, 1, 1)
            g.in_draw = False


class Qmember:
    def __init__(self):
        pass

    def insert_in_front_of(self, m2, c, q, priority):
        available_quantity = q.capacity._tally - q._length - 1
        if available_quantity < 0:
            raise QueueFullError(q.name() + " has reached capacity " + str(q.capacity._tally))
        q.available_quantity.tally(available_quantity)

        m1 = m2.predecessor
        m1.successor = self
        m2.predecessor = self
        self.predecessor = m1
        self.successor = m2
        self.priority = priority
        self.component = c
        self.queue = q
        self.enter_time = c.env._now
        q._length += 1
        if not (isinstance(q, Store) or q._isinternal):  # this is because internal and as store never need touch handling (new in 23.0.1)
            for iter in q._iter_touched:
                q._iter_touched[iter] = True
        c._qmembers[q] = self
        if q.env._trace:
            if not q._isinternal:
                q.env.print_trace("", "", c.name(), "enter " + q.name())
        q.length.tally(q._length)
        q.number_of_arrivals += 1
        if isinstance(q, Store):
            store = q
            for requester in store._from_store_requesters:
                if requester._from_store_filter(c):
                    c.leave(store)
                    for store0 in requester._from_stores:
                        requester.leave(store0._from_store_requesters)
                    requester._from_stores = []
                    requester._from_store_item = c
                    requester._from_store_store = store
                    requester._remove()
                    requester.status._value = scheduled
                    requester._reschedule(
                        requester.env._now, 0, False, f"from_store ({store.name()}) honor with {c.name()}", False, s0=requester.env.last_s0, return_value=c
                    )
                    break


class Queue:
    """
    Queue object

    Parameters
    ----------
    fill : iterable, usually Queue, list or tuple
        fill the queue with the components in fill

        if omitted, the queue will be empty at initialization

    name : str
        name of the queue

        if the name ends with a period (.),
        auto serializing will be applied

        if the name end with a comma,
        auto serializing starting at 1 will be applied

        if omitted, the name will be derived from the class
        it is defined in (lowercased)

    capacity : float
        maximum number of components the queue can contain.

        if exceeded, a QueueFullError will be raised

        default: inf

    monitor : bool
        if True (default) , both length and length_of_stay are monitored

        if False, monitoring is disabled.

    env : Environment
        environment where the queue is defined

        if omitted, default_env will be used
    """

    def __init__(self, name: str = None, monitor: Any = True, fill: Iterable = None, capacity: float = inf, env: "Environment" = None, *args, **kwargs) -> None:
        self.env = _set_env(env)

        _set_name(name, self.env._nameserializeQueue, self)
        self._head = Qmember()
        self._tail = Qmember()
        self._head.successor = self._tail
        self._head.predecessor = None
        self._tail.successor = None
        self._tail.predecessor = self._head
        self._head.component = None
        self._tail.component = None
        self._head.priority = 0
        self._tail.priority = 0
        self._length = 0
        self._iter_sequence = 0
        self._iter_touched = {}
        self._isinternal = False
        self.arrival_rate(reset=True)
        self.departure_rate(reset=True)
        self.length = _SystemMonitor("Length of " + self.name(), level=True, initial_tally=0, monitor=monitor, type="uint32", env=self.env)
        self.length_of_stay = Monitor("Length of stay in " + self.name(), monitor=monitor, type="float", env=self.env)
        self.capacity = _CapacityMonitor("Capacity of " + self.name(), level=True, initial_tally=capacity, monitor=monitor, type="float", env=env)
        self.capacity.parent = self
        self.available_quantity = _SystemMonitor(
            "Available quantity of " + self.name(), level=True, initial_tally=capacity, monitor=monitor, type="float", env=env
        )

        if fill is not None:
            with self.env.suppress_trace():
                for c in fill:
                    c.enter(self)
        if self.env._trace:
            self.env.print_trace("", "", self.name() + " create")
        self.setup(*args, **kwargs)

    def setup(self) -> None:
        """
        called immediately after initialization of a queue.

        by default this is a dummy method, but it can be overridden.

        only keyword arguments are passed
        """
        pass

    def animate(self, *args, **kwargs) -> "AnimateQueue":
        """
        Animates the components in the queue.

        Parameters
        ----------
        x : float
            x-position of the first component in the queue

            default: 50

        y : float
            y-position of the first component in the queue

            default: 50

        direction : str
            if "w", waiting line runs westwards (i.e. from right to left)

            if "n", waiting line runs northeards (i.e. from bottom to top)

            if "e", waiting line runs eastwards (i.e. from left to right) (default)

            if "s", waiting line runs southwards (i.e. from top to bottom)
            if "t", waiting line runs follows given trajectory

        trajectory : Trajectory
            trajectory to be followed if direction == "t"

        reverse : bool
            if False (default), display in normal order. If True, reversed.

        max_length : int
            maximum number of components to be displayed

        xy_anchor : str
            specifies where x and y are relative to

            possible values are (default: sw):

            ``nw    n    ne``

            ``w     c     e``

            ``sw    s    se``

        id : any
            the animation works by calling the animation_objects method of each component, optionally
            with id. By default, this is self, but can be overriden, particularly with the queue

        arg : any
            this is used when a parameter is a function with two parameters, as the first argument or
            if a parameter is a method as the instance

            default: self (instance itself)

        Returns
        -------
        reference to AnimationQueue object : AnimationQueue

        Note
        ----
        It is recommended to use sim.AnimateQueue instead


        All measures are in screen coordinates


        All parameters, apart from queue and arg can be specified as:

        - a scalar, like 10

        - a function with zero arguments, like lambda: title

        - a function with one argument, being the time t, like lambda t: t + 10

        - a function with two parameters, being arg (as given) and the time, like lambda comp, t: comp.state

        - a method instance arg for time t, like self.state, actually leading to arg.state(t) to be called

        """
        return AnimateQueue(self, *args, **kwargs)

    def animate3d(self, *args, **kwargs) -> "Animate3dQueue":
        """
        Animates the components in the queue in 3D.

        Parameters
        ----------
        x : float
            x-position of the first component in the queue

            default: 0

        y : float
            y-position of the first component in the queue

            default: 0

        z : float
            z-position of the first component in the queue

            default: 0

        direction : str
            if "x+", waiting line runs in positive x direction (default)

            if "x-", waiting line runs in negative x direction

            if "y+", waiting line runs in positive y direction

            if "y-", waiting line runs in negative y direction

            if "z+", waiting line runs in positive z direction

            if "z-", waiting line runs in negative z direction


        reverse : bool
            if False (default), display in normal order. If True, reversed.

        max_length : int
            maximum number of components to be displayed

        layer : int
            layer (default 0)

        id : any
            the animation works by calling the animation_objects method of each component, optionally
            with id. By default, this is self, but can be overriden, particularly with the queue

        arg : any
            this is used when a parameter is a function with two parameters, as the first argument or
            if a parameter is a method as the instance

            default: self (instance itself)

        Returns
        -------
        reference to Animation3dQueue object : Animation3dQueue

        Note
        ----
        It is recommended to use sim.AnimatedQueue instead


        All parameters, apart from queue and arg can be specified as:

        - a scalar, like 10

        - a function with zero arguments, like lambda: title

        - a function with one argument, being the time t, like lambda t: t + 10

        - a function with two parameters, being arg (as given) and the time, like lambda comp, t: comp.state

        - a method instance arg for time t, like self.state, actually leading to arg.state(t) to be called

        """
        return Animate3dQueue(self, *args, **kwargs)

    def all_monitors(self) -> Tuple:
        """
        returns all monitors belonging to the queue

        Returns
        -------
        all monitors : tuple of monitors
        """
        return (self.length, self.length_of_stay)

    def reset_monitors(self, monitor: bool = None, stats_only: bool = None) -> None:
        """
        resets queue monitor length_of_stay and length

        Parameters
        ----------
        monitor : bool
            if True, monitoring will be on.

            if False, monitoring is disabled

            if omitted, no change of monitoring state

        stats_only : bool
            if True, only statistics will be collected (using less memory, but also less functionality)

            if False, full functionality

            if omittted, no change of stats_only

        Note
        ----
        it is possible to reset individual monitoring with length_of_stay.reset() and length.reset()
        """
        self.length.reset(monitor=monitor, stats_only=stats_only)
        self.length_of_stay.reset(monitor=monitor, stats_only=stats_only)

    def arrival_rate(self, reset: bool = False) -> float:
        """
        returns the arrival rate

        When the queue is created, the registration is reset.

        Parameters
        ----------
        reset : bool
            if True, number_of_arrivals is set to 0 since last reset and the time of the last reset to now

            default: False ==> no reset

        Returns
        -------
        arrival rate :  float
            number of arrivals since last reset / duration since last reset

            nan if duration is zero
        """
        if reset:
            self.number_of_arrivals = 0
            self.number_of_arrivals_t0 = self.env._now
        duration = self.env._now - self.number_of_arrivals_t0
        if duration == 0:
            return nan
        else:
            return self.number_of_arrivals / duration

    def departure_rate(self, reset: bool = False) -> float:
        """
        returns the departure rate

        When the queue is created, the registration is reset.

        Parameters
        ----------
        reset : bool
            if True, number_of_departures is set to 0 since last reset and the time of the last reset to now

            default: False ==> no reset

        Returns
        -------
        departure rate :  float
            number of departures since last reset / duration since last reset

            nan if duration is zero
        """
        if reset:
            self.number_of_departures = 0
            self.number_of_departures_t0 = self.env._now
        duration = self.env._now - self.number_of_departures_t0
        if duration == 0:
            return nan
        else:
            return self.number_of_departures / duration

    def monitor(self, value: bool) -> None:
        """
        enables/disables monitoring of length_of_stay and length

        Parameters
        ----------
        value : bool
            if True, monitoring will be on.

            if False, monitoring is disabled


        Note
        ----
        it is possible to individually control monitoring with length_of_stay.monitor() and length.monitor()
        """

        self.length.monitor(value=value)
        self.length_of_stay.monitor(value=value)

    def register(self, registry: List) -> "Queue":
        """
        registers the queue in the registry

        Parameters
        ----------
        registry : list
            list of (to be) registered objects

        Returns
        -------
        queue (self) : Queue

        Note
        ----
        Use Queue.deregister if queue does not longer need to be registered.
        """
        if not isinstance(registry, list):
            raise TypeError("registry not list")
        if self in registry:
            raise ValueError(self.name() + " already in registry")
        registry.append(self)
        return self

    def deregister(self, registry: List) -> "Queue":
        """
        deregisters the queue in the registry

        Parameters
        ----------
        registry : list
            list of registered queues

        Returns
        -------
        queue (self) : Queue
        """
        if not isinstance(registry, list):
            raise TypeError("registry not list")
        if self not in registry:
            raise ValueError(self.name() + " not in registry")
        registry.remove(self)
        return self

    def __repr__(self):
        return object_to_str(self) + " (" + self.name() + ")"

    def print_info(self, as_str: bool = False, file: TextIO = None) -> str:
        """
        prints information about the queue

        Parameters
        ----------
        as_str: bool
            if False (default), print the info
            if True, return a string containing the info

        file: file
            if None(default), all output is directed to stdout

            otherwise, the output is directed to the file

        Returns
        -------
        info (if as_str is True) : str
        """
        result = []
        result.append(object_to_str(self) + " " + hex(id(self)))
        result.append("  name=" + self.name())
        if self._length:
            result.append("  component(s):")
            mx = self._head.successor
            while mx != self._tail:
                result.append(
                    "    "
                    + pad(mx.component.name(), 20)
                    + " enter_time"
                    + self.env.time_to_str(mx.enter_time - self.env._offset)
                    + " priority="
                    + str(mx.priority)
                )
                mx = mx.successor
        else:
            result.append("  no components")
        return return_or_print(result, as_str, file)

    def print_statistics(self, as_str: bool = False, file: TextIO = None) -> Any:
        """
        prints a summary of statistics of a queue

        Parameters
        ----------
        as_str: bool
            if False (default), print the statistics
            if True, return a string containing the statistics

        file: file
            if None(default), all output is directed to stdout

            otherwise, the output is directed to the file

        Returns
        -------
        statistics (if as_str is True) : str
        """
        result = []
        result.append(f"Statistics of {self.name()} at {fn(self.env._now - self.env._offset, 13, 3)}")
        result.append(self.length.print_statistics(show_header=False, show_legend=True, do_indent=True, as_str=True))

        result.append("")
        result.append(self.length_of_stay.print_statistics(show_header=False, show_legend=False, do_indent=True, as_str=True))
        return return_or_print(result, as_str, file)

    def print_histograms(self, exclude: Iterable = [], as_str: bool = False, file: bool = None, graph_scale: float = None) -> Any:
        """
        prints the histograms of the length and length_of_stay monitor of the queue

        Parameters
        ----------
        exclude : tuple or list
            specifies which monitors to exclude

            default: ()


        as_str: bool
            if False (default), print the histograms
            if True, return a string containing the histograms

        file: file
            if None(default), all output is directed to stdout

            otherwise, the output is directed to the file

        graph_scale : float
            Scale in the graphical representation of the % and cum% (default=80)

        Returns
        -------
        histograms (if as_str is True) : str
        """
        result = []
        for m in (self.length, self.length_of_stay):
            if m not in exclude:
                result.append(m.print_histogram(as_str=True, graph_scale=graph_scale))
        return return_or_print(result, as_str, file)

    def set_capacity(self, cap: float) -> None:
        """
        Parameters
        ----------
        cap : float or int
            capacity of the queue

        """
        self.capacity.tally(cap)
        self.available_quantity.tally(cap - self._length)

    def name(self, value: str = None) -> str:
        """
        Parameters
        ----------
        value : str
            new name of the queue
            if omitted, no change

        Returns
        -------
        Name of the queue : str

        Note
        ----
        base_name and sequence_number are not affected if the name is changed

        All derived named are updated as well.
        """
        if value is not None:
            self._name = value
            self.length.name("Length of " + self.name())
            self.length_of_stay.name("Length of stay of " + self.name())
        return self._name

    def rename(self, value: str = None) -> "Queue":
        """
        Parameters
        ----------
        value : str
            new name of the queue
            if omitted, no change

        Returns
        -------
        self : queue

        Note
        ----
        in contrast to name(), this method returns itself, so can used to chain, e.g.

        (q0 + q1 + q2 + q3).rename('q0 - q3').print_statistics()

        (q1 - q0).rename('difference of q1 and q0)').print_histograms()
        """
        self.name(value)
        return self

    def base_name(self) -> str:
        """
        Returns
        -------
        base name of the queue (the name used at initialization): str
        """
        return getattr(self, "_base_name", self._name)

    def sequence_number(self) -> int:
        """
        Returns
        -------
        sequence_number of the queue : int
            (the sequence number at initialization)

            normally this will be the integer value of a serialized name.

            Non serialized names (without a dot or a comma at the end)
            will return 1)
        """
        return getattr(self, "_sequence_number", 1)

    def add(self, component: "Component") -> "Queue":
        """
        adds a component to the tail of a queue

        Parameters
        ----------
        component : Component
            component to be added to the tail of the queue

            may not be member of the queue yet

        Note
        ----
        the priority will be set to
        the priority of the tail of the queue, if any
        or 0 if queue is empty

        This method is equivalent to append()
        """
        component.enter(self)
        return self

    def append(self, component: "Component") -> "Queue":
        """
        appends a component to the tail of a queue

        Parameters
        ----------
        component : Component
            component to be appened to the tail of the queue

            may not be member of the queue yet

        Note
        ----
        the priority will be set to
        the priority of the tail of the queue, if any
        or 0 if queue is empty

        This method is equivalent to add()
        """
        component.enter(self)
        return self

    def add_at_head(self, component: "Component") -> "Queue":
        """
        adds a component to the head of a queue

        Parameters
        ----------

        component : Component
            component to be added to the head of the queue

            may not be member of the queue yet

        Note
        ----
        the priority will be set to
        the priority of the head of the queue, if any
        or 0 if queue is empty
        """
        component.enter_at_head(self)
        return self

    def add_in_front_of(self, component: "Component", poscomponent: "Component") -> "Queue":
        """
        adds a component to a queue, just in front of a component

        Parameters
        ----------
        component : Component
            component to be added to the queue

            may not be member of the queue yet

        poscomponent : Component
            component in front of which component will be inserted

            must be member of the queue

        Note
        ----
        the priority of component will be set to the priority of poscomponent
        """
        component.enter_in_front_of(self, poscomponent)
        return self

    def insert(self, index: int, component: "Component") -> "Queue":
        """
        Insert component before index-th element of the queue

        Parameters
        ----------
        index : int
            component to be added just before index'th element

            should be >=0 and <=len(self)

        component : Component
            component to be added to the queue

        Note
        ----
        the priority of component will be set to the priority of the index'th component,
        or 0 if the queue is empty
        """
        if index < 0:
            raise IndexError("index < 0")
        if index > self._length:
            raise IndexError("index > lengh of queue")
        component._checknotinqueue(self)
        mx = self._head.successor
        count = 0
        while mx != self._tail:
            if count == index:
                break
            count = count + 1
            mx = mx.successor
        priority = mx.priority
        Qmember().insert_in_front_of(mx, component, self, priority)
        return self

    def add_behind(self, component: "Component", poscomponent: "Component") -> "Queue":
        """
        adds a component to a queue, just behind a component

        Parameters
        ----------
        component : Component
            component to be added to the queue

            may not be member of the queue yet

        poscomponent : Component
            component behind which component will be inserted

            must be member of the queue

        Note
        ----
        the priority of component will be set to the priority of poscomponent

        """
        component.enter_behind(self, poscomponent)
        return self

    def add_sorted(self, component: "Component", priority: Any) -> "Queue":
        """
        adds a component to a queue, according to the priority

        Parameters
        ----------
        component : Component
            component to be added to the queue

            may not be member of the queue yet

        priority: type that can be compared with other priorities in the queue
            priority in the queue

        Note
        ----
        The component is placed just before the first component with a priority > given priority
        """
        component.enter_sorted(self, priority)
        return self

    def remove(self, component: "Component" = None) -> "Queue":
        """
        removes component from the queue

        Parameters
        ----------
        component : Component
            component to be removed

            if omitted, all components will be removed.

        Note
        ----
        component must be member of the queue
        """
        if component is None:
            self.clear()
        else:
            component.leave(self)
        return self

    def head(self) -> "Component":
        """
        Returns
        -------
        the head component of the queue, if any. None otherwise : Component

        Note
        ----
        q[0] is a more Pythonic way to access the head of the queue
        """
        return self._head.successor.component

    def tail(self) -> "Component":
        """
        Returns
        -------
        the tail component of the queue, if any. None otherwise : Component

        Note
        -----
        q[-1] is a more Pythonic way to access the tail of the queue
        """
        return self._tail.predecessor.component

    def pop(self, index: int = None) -> "Component":
        """
        removes a component by its position (or head)

        Parameters
        ----------
        index : int
            index-th element to remove, if any

            if omitted, return the head of the queue, if any

        Returns
        -------
        The i-th component or head : Component
            None if not existing
        """
        if index is None:
            c = self._head.successor.component
        else:
            c = self[index]
        if c is not None:
            c.leave(self)
        return c

    def successor(self, component: "Component") -> "Component":
        """
        successor in queue

        Parameters
        ----------
        component : Component
            component whose successor to return

            must be member of the queue

        Returns
        -------
        successor of component, if any : Component
            None otherwise
        """
        return component.successor(self)

    def predecessor(self, component: "Component") -> "Component":
        """
        predecessor in queue

        Parameters
        ----------
        component : Component
            component whose predecessor to return

            must be member of the queue

        Returns
        -------
        predecessor of component, if any : Component

            None otherwise.
        """
        return component.predecessor(self)

    def __contains__(self, component: "Component") -> bool:
        return component._member(self) is not None

    def __getitem__(self, key):
        if isinstance(key, slice):
            # Get the start, stop, and step from the slice
            startval, endval, incval = key.indices(self._length)
            if incval > 0:
                result = []
                targetval = startval
                mx = self._head.successor
                count = 0
                while mx != self._tail:
                    if targetval >= endval:
                        break
                    if targetval == count:
                        result.append(mx.component)
                        targetval += incval
                    count += 1
                    mx = mx.successor
            else:
                result = []
                targetval = startval
                mx = self._tail.predecessor
                count = self._length - 1
                while mx != self._head:
                    if targetval <= endval:
                        break
                    if targetval == count:
                        result.append(mx.component)
                        targetval += incval  # incval is negative here!
                    count -= 1
                    mx = mx.predecessor

            return list(result)

        elif isinstance(key, int):
            if key < 0:  # Handle negative indices
                key += self._length
            if key < 0 or key >= self._length:
                raise IndexError("queue index out of range")
            mx = self._head.successor
            count = 0
            while mx != self._tail:
                if count == key:
                    return mx.component
                count = count + 1
                mx = mx.successor

            return None  # just for safety

        else:
            raise TypeError("Invalid argument type: " + object_to_str(key))

    def __delitem__(self, key):
        if isinstance(key, slice):
            for c in self[key]:
                self.remove(c)
        elif isinstance(key, int):
            self.remove(self[key])
        else:
            raise TypeError("Invalid argument type:" + object_to_str(key))

    def __len__(self):
        return self._length

    def __reversed__(self):
        self._iter_sequence += 1
        iter_sequence = self._iter_sequence
        self._iter_touched[iter_sequence] = False
        iter_list = []
        mx = self._tail.predecessor
        while mx != self._head:
            iter_list.append(mx)
            mx = mx.predecessor
        iter_index = 0
        while len(iter_list) > iter_index or self._iter_touched[iter_sequence]:
            if self._iter_touched[iter_sequence]:
                # place all taken qmembers on the list
                iter_list = iter_list[:iter_index]
                mx = self._tail.predecessor
                while mx != self._head:
                    if mx not in iter_list:
                        iter_list.append(mx)
                    mx = mx.predecessor
                self._iter_touched[iter_sequence] = False
            else:
                c = iter_list[iter_index].component
                if c is not None:  # skip deleted components
                    yield c
                iter_index += 1

        del self._iter_touched[iter_sequence]

    def __add__(self, other):
        if not isinstance(other, Queue):
            return NotImplemented
        return self.union(other)

    def __radd__(self, other):
        if other == 0:  # to be able to use sum
            return self
        if not isinstance(other, Queue):
            return NotImplemented
        return self.union(other)

    def __or__(self, other):
        if not isinstance(other, Queue):
            return NotImplemented
        return self.union(other)

    def __sub__(self, other):
        if not isinstance(other, Queue):
            return NotImplemented
        return self.difference(other)

    def __and__(self, other):
        if not isinstance(other, Queue):
            return NotImplemented
        return self.intersection(other)

    def __xor__(self, other):
        if not isinstance(other, Queue):
            return NotImplemented
        return self.symmetric_difference(other)

    def _operator(self, other, op):
        if hasattr(other, "__iter__"):
            return op(set(self), set(other))
        return NotImplemented

    def __hash__(self):
        return id(self)

    def __eq__(self, other):
        return self._operator(other, operator.__eq__)

    def __ne__(self, other):
        return self._operator(other, operator.__ne__)

    def __lt__(self, other):
        return self._operator(other, operator.__lt__)

    def __le__(self, other):
        return self._operator(other, operator.__le__)

    def __gt__(self, other):
        return self._operator(other, operator.__gt__)

    def __ge__(self, other):
        return self._operator(other, operator.__ge__)

    def count(self, component: "Component") -> int:
        """
        component count

        Parameters
        ---------
        component : Component
            component to count

        Returns
        -------
        number of occurences of component in the queue

        Note
        ----
        The result can only be 0 or 1
        """
        return component.count(self)

    def index(self, component: "Component") -> int:
        """
        get the index of a component in the queue

        Parameters
        ----------
        component : Component
            component to be queried

            does not need to be in the queue

        Returns
        -------
        index of component in the queue : int
            0 denotes the head,

            returns -1 if component is not in the queue
        """
        return component.index(self)

    def component_with_name(self, txt: str) -> "Component":
        """
        returns a component in the queue according to its name

        Parameters
        ----------
        txt : str
            name of component to be retrieved

        Returns
        -------
        the first component in the queue with name txt : Component

            returns None if not found
        """
        mx = self._head.successor
        while mx != self._tail:
            if mx.component.name() == txt:
                return mx.component
            mx = mx.successor
        return None

    def __iter__(self):
        self._iter_sequence += 1
        iter_sequence = self._iter_sequence
        self._iter_touched[iter_sequence] = False

        iter_list = []
        mx = self._head.successor
        while mx != self._tail:
            iter_list.append(mx)
            mx = mx.successor
        iter_index = 0
        while len(iter_list) > iter_index or self._iter_touched[iter_sequence]:
            if self._iter_touched[iter_sequence]:
                # place all taken qmembers on the list
                iter_list = iter_list[:iter_index]
                mx = self._head.successor
                while mx != self._tail:
                    if mx not in iter_list:
                        iter_list.append(mx)
                    mx = mx.successor
                self._iter_touched[iter_sequence] = False
            else:
                c = iter_list[iter_index].component
                if c is not None:  # skip deleted components
                    yield c
                iter_index += 1

        del self._iter_touched[iter_sequence]

    def extend(self, source: Iterable, clear_source: bool = False) -> None:
        """
        extends the queue with components of source that are not already in self (at the end of self)

        Parameters
        ----------
        source : iterable (usually queue, list or tuple)

        clear_source : bool
            if False (default), the elements will remain in source

            if True, source will be cleared, so effectively moving all elements in source to self. If source is
            not a queue, but a list or tuple, the clear_source flag may not be set.

        Returns
        -------
        None

        Note
        ----
        The components in source added to the queue will get the priority of the tail of self.
        """
        count = 0
        with self.env.suppress_trace():
            self.env._trace = False
            for c in source:
                if c not in self:
                    c.enter(self)
                    count += 1
        if self.env._trace:
            self.env.print_trace(
                "",
                "",
                self.name()
                + " extend from "
                + (source.name() if isinstance(source, Queue) else "instance of " + str(type(source)))
                + " ("
                + str(count)
                + " components)",
            )
        if clear_source:
            if isinstance(source, Queue):
                source.clear()
            else:
                raise TypeError("clear_source cannot be applied to instances of type" + str(type(source)))

    def as_set(self):
        return {c for c in self}

    def as_list(self):
        return [c for c in self]

    def union(self, q: "Queue", name: str = None, monitor: bool = False) -> "Queue":
        """
        Parameters
        ----------
        q : Queue
            queue to be unioned with self

        name : str
            name of the  new queue

            if omitted, self.name() + q.name()

        monitor : bool
            if True, monitor the queue

            if False (default), do not monitor the queue

        Returns
        -------
        queue containing all elements of self and q : Queue

        Note
        ----
        the priority will be set to 0 for all components in the
        resulting  queue

        the order of the resulting queue is as follows:

        first all components of self, in that order,
        followed by all components in q that are not in self,
        in that order.

        Alternatively, the more pythonic | operator is also supported, e.g. q1 | q2
        """
        with self.env.suppress_trace():
            if name is None:
                name = self.name() + " | " + q.name()
            q1 = type(self)(name=name, monitor=monitor, env=self.env)
            self_set = self.as_set()

            mx = self._head.successor
            while mx != self._tail:
                Qmember().insert_in_front_of(q1._tail, mx.component, q1, 0)
                mx = mx.successor

            mx = q._head.successor
            while mx != q._tail:
                if mx.component not in self_set:
                    Qmember().insert_in_front_of(q1._tail, mx.component, q1, 0)
                mx = mx.successor

        return q1

    def intersection(self, q: "Queue", name: str = None, monitor: bool = False) -> "Queue":
        """
        returns the intersect of two queues

        Parameters
        ----------
        q : Queue
            queue to be intersected with self

        name : str
            name of the  new queue

            if omitted, self.name() + q.name()

        monitor : bool
            if True, monitor the queue

            if False (default), do not monitor the queue

        Returns
        -------
        queue with all elements that are in self and q : Queue

        Note
        ----
        the priority will be set to 0 for all components in the
        resulting  queue

        the order of the resulting queue is as follows:

        in the same order as in self.

        Alternatively, the more pythonic & operator is also supported, e.g. q1 & q2
        """
        with self.env.suppress_trace():
            if name is None:
                name = self.name() + " & " + q.name()
            q1 = type(self)(name=name, monitor=monitor, env=self.env)
            q_set = q.as_set()
            mx = self._head.successor
            while mx != self._tail:
                if mx.component in q_set:
                    Qmember().insert_in_front_of(q1._tail, mx.component, q1, 0)
                mx = mx.successor
        return q1

    def difference(self, q: "Queue", name: str = None, monitor: bool = False) -> "Queue":
        """
        returns the difference of two queues

        Parameters
        ----------
        q : Queue
            queue to be 'subtracted' from self

        name : str
            name of the  new queue

            if omitted, self.name() - q.name()

        monitor : bool
            if True, monitor the queue

            if False (default), do not monitor the queue

        Returns
        -------
        queue containing all elements of self that are not in q

        Note
        ----
        the priority will be copied from the original queue.
        Also, the order will be maintained.

        Alternatively, the more pythonic - operator is also supported, e.g. q1 - q2
        """
        if name is None:
            name = self.name() + " - " + q.name()
        with self.env.suppress_trace():
            q1 = type(self)(name=name, monitor=monitor, env=self.env)
            q_set = q.as_set()
            mx = self._head.successor
            while mx != self._tail:
                if mx.component not in q_set:
                    Qmember().insert_in_front_of(q1._tail, mx.component, q1, mx.priority)
                mx = mx.successor
        return q1

    def symmetric_difference(self, q: "Queue", name: str = None, monitor: bool = False) -> "Queue":
        """
        returns the symmetric difference of two queues

        Parameters
        ----------
        q : Queue
            queue to be 'subtracted' from self

        name : str
            name of the  new queue

            if omitted, self.name() - q.name()

        monitor : bool
            if True, monitor the queue

            if False (default), do not monitor the queue

        Returns
        -------
        queue containing all elements that are either in self or q, but not in both

        Note
        ----
        the priority of all elements will be set to 0 for all components in the new queue.
        Order: First, elelements in self (in that order), then elements in q (in that order)
        Alternatively, the more pythonic ^ operator is also supported, e.g. q1 ^ q2
        """
        if name is None:
            name = self.name() + " ^ " + q.name()
        with self.env.suppress_trace():
            q1 = type(self)(name=name, monitor=monitor, env=self.env)

            intersection_set = self.as_set() & q.as_set()
            mx = self._head.successor
            while mx != self._tail:
                if mx.component not in intersection_set:
                    Qmember().insert_in_front_of(q1._tail, mx.component, q1, 0)
                mx = mx.successor
            mx = q._head.successor
            while mx != q._tail:
                if mx.component not in intersection_set:
                    Qmember().insert_in_front_of(q1._tail, mx.component, q1, 0)
                mx = mx.successor

        return q1

    def copy(self, name: str = None, copy_capacity: bool = False, monitor: bool = False) -> "Queue":
        """
        returns a copy of a queue

        Parameters
        ----------
        name : str
            name of the new queue

            if omitted, "copy of " + self.name()

        monitor : bool
            if True, monitor the queue

            if False (default), do not monitor the queue

        copy_capacity : bool
            if True, the capacity will be copied

            if False (default), the resulting queue will always be unrestricted

        Returns
        -------
        queue with all elements of self : Queue

        Note
        ----
        The priority will be copied from original queue.
        Also, the order will be maintained.
        """
        with self.env.suppress_trace():
            if name is None:
                name = "copy of " + self.name()
            q1 = type(self)(name=name, monitor=monitor, env=self.env)
            if copy_capacity:
                q1.capacity._tally = self.capacity._tally
            mx = self._head.successor
            while mx != self._tail:
                Qmember().insert_in_front_of(q1._tail, mx.component, q1, mx.priority)
                mx = mx.successor
        return q1

    def move(self, name: str = None, monitor: bool = False, copy_capacity=False):
        """
        makes a copy of a queue and empties the original

        Parameters
        ----------
        name : str
            name of the new queue

        monitor : bool
            if True, monitor the queue

            if False (default), do not monitor the yqueue

        copy_capacity : bool
            if True, the capacity will be copied

            if False (default), the new queue will always be unrestricted

        Returns
        -------
        queue containing all elements of self: Queue
        the capacity of the original queue will not be changed

        Note
        ----
        Priorities will be kept

        self will be emptied
        """
        q1 = self.copy(name, monitor=monitor, copy_capacity=copy_capacity)
        self.clear()
        return q1

    def clear(self):
        """
        empties a queue

        removes all components from a queue
        """
        with self.env.suppress_trace():
            mx = self._head.successor
            while mx != self._tail:
                c = mx.component
                mx = mx.successor
                c.leave(self)
        if self.env._trace:
            self.env.print_trace("", "", self.name() + " clear")


class Store(Queue):
    def __init__(self, name: str = None, monitor: Any = True, fill: Iterable = None, capacity: float = inf, env: "Environment" = None, *args, **kwargs) -> None:
        super().__init__(name=name, monitor=monitor, fill=None, capacity=capacity, env=env, *args, **kwargs)

        with self.env.suppress_trace():
            self._to_store_requesters = Queue(f"{name}.to_store_requesters", env=env)
            self._to_store_requesters._isinternal = True
            self._from_store_requesters = Queue(f"{name}.from_store_requesters", env=env)
            self._from_store_requesters._isinternal = True

            if fill is not None:  # this cannot be done by Queue.__init__ as the requesters are not defined at that time
                with self.env.suppress_trace():
                    for c in fill:
                        c.enter(self)

    def set_capacity(self, cap: float) -> None:
        """
        Parameters
        ----------
        cap : float or int
            capacity of the store


        Note
        ----
        Might cause (to_store requests to be honoured)
        """
        old_cap = self.capacity()
        super().set_capacity(cap=cap)
        if cap >= old_cap:
            self._rescan_to()

    def from_store_requesters(self) -> "Queue":
        """
        get the queue holding all from_store requesting components

        Returns
        -------
        queue holding all from_store requesting components : Queue
        """
        return self._from_store_requesters

    def to_store_requesters(self) -> "Queue":
        """
        get the queue holding all from_store requesting components

        Returns
        -------
        queue holding all from_store requesting components : Queue
        """
        return self._to_store_requesters

    def rescan(self):
        """
        Rescan for any components to be allowed from.
        """
        for c in self._from_store_requesters:
            for item in list(self):
                if c._from_store_filter(item):
                    for store in c._from_stores:
                        c.leave(store._from_store_requesters)
                    with self.env.suppress_trace():
                        item = self.pop(0)
                    c._from_stores = []
                    c._from_store_item = item
                    c._from_store_store = self
                    c._remove()
                    c.status._value = scheduled
                    c._reschedule(c.env._now, 0, False, f"from_store ({self.name()}) honor with {item.name()}", False, s0=c.env.last_s0, return_value=item)

    def _rescan_to(self):
        """
        Rescan for any components to be allowed to.
        """
        for c in self._to_store_requesters:
            if self.available_quantity() > 0:
                for store in c._to_stores:
                    c.leave(store._to_store_requesters)
                with self.env.suppress_trace():
                    c._to_store_item.enter_sorted(self, c._to_store_priority)
                c._to_stores = []
                c._remove()
                c.status._value = scheduled
                c._reschedule(c.env._now, 0, False, f"to_store ({self.name()}) honor ", False, s0=c.env.last_s0)
                c._to_store_item = None
                c._to_store_store = self
            else:
                break


class Animate3dBase(DynamicClass):
    """
    Base class for a 3D animation object

    When a class inherits from this base class, it will be added to the animation objects list to be shown

    Parameters
    ----------
    visible : bool
        visible

        if False, animation object is not shown, shown otherwise
        (default True)

    layer : int
         layer value

         lower layer values are displayed later in the frame (default 0)

    arg : any
        this is used when a parameter is a function with two parameters, as the first argument or
        if a parameter is a method as the instance

        default: self (instance itself)

    parent : Component
        component where this animation object belongs to (default None)

        if given, the animation object will be removed
        automatically when the parent component is no longer accessible

    env : Environment
        environment where the component is defined

        if omitted, default_env will be used
    """

    def __init__(
        self, visible: bool = True, keep: bool = True, arg: Any = None, layer: float = 0, parent: "Component" = None, env: "Environment" = None, **kwargs
    ) -> None:
        super().__init__()
        self.env = _set_env(env)

        self.visible = visible
        self.keep = keep
        self.arg = self if arg is None else arg
        self.layer = layer
        if parent is not None:
            if not isinstance(parent, Component):
                raise ValueError(repr(parent) + " is not a component")
            parent._animation_children.add(self)

        self.sequence = self.env.serialize()
        self.env.an_objects3d.add(self)
        self.register_dynamic_attributes("visible keep layer")
        self.setup(**kwargs)

    def setup(self) -> None:
        """
        called immediately after initialization of a the Animate3dBase object.

        by default this is a dummy method, but it can be overridden.

        only keyword arguments will be passed

        Example
        -------
            class AnimateVehicle(sim.Animate3dBase):
                def setup(self, length):
                    self.length = length
                    self.register_dynamic_attributes("length")

                ...
        """
        pass

    def show(self) -> None:
        """
        show (unremove)

        It is possible to use this method if already shown
        """
        self.env.an_objects3d.add(self)

    def remove(self) -> None:
        """
        removes the 3d animation oject
        """
        self.env.an_objects3d.discard(self)

    def is_removed(self) -> bool:
        return self in self.env.an_objects3d


class _Movement:
    # used by trajectories
    def __init__(self, l, vmax: float = None, v0: float = None, v1: float = None, acc: float = None, dec: float = None) -> None:
        if vmax is None:
            vmax = 1
        if v0 is None:
            v0 = vmax
        if v1 is None:
            v1 = vmax
        if acc is None:
            acc = math.inf
        if dec is None:
            dec = math.inf

        acc2inv = 1 / (2 * acc)
        dec2inv = 1 / (2 * dec)
        s_v0_vmax = (vmax**2 - v0**2) * acc2inv
        s_vmax_v1 = (vmax**2 - v1**2) * dec2inv

        if s_v0_vmax + s_vmax_v1 > l:
            vmax = math.sqrt((l + (v0**2 * acc2inv) + (v1**2 * dec2inv)) / (acc2inv + dec2inv))

        self.l_v0_vmax = (vmax**2 - v0**2) * acc2inv
        self.l_vmax_v1 = (vmax**2 - v1**2) * dec2inv

        self.l_vmax = l - self.l_v0_vmax - self.l_vmax_v1
        if self.l_v0_vmax < 0 or self.l_vmax_v1 < 0:
            raise ValueError("not feasible")
        self.t_v0_vmax = (vmax - v0) / acc
        self.t_vmax = self.l_vmax / vmax
        self.t_vmax_v1 = (vmax - v1) / dec
        self.t = self.t_v0_vmax + self.t_vmax + self.t_vmax_v1
        self.v0 = v0
        self.vmax = vmax
        self.acc = acc
        self.dec = dec

    def l_at_t(self, t):
        if t < 0:
            return 0
        if t > self.t:
            t = self.t
        if self.acc == math.inf and self.dec == math.inf:
            return self.vmax * t
        if t < self.t_v0_vmax:
            return (self.v0 * t) + self.acc * t**2 / 2
        t -= self.t_v0_vmax
        if t < self.t_vmax:
            return self.l_v0_vmax + t * self.vmax
        t -= self.t_vmax
        if self.dec == math.inf:
            return self.l_v0_vmax + self.l_vmax + (self.vmax * t)
        return self.l_v0_vmax + self.l_vmax + (self.vmax * t) - self.dec * t**2 / 2


class _Trajectory:
    # used by trajectories
    def in_trajectory(self, t):
        return self._t0 <= t <= self._t1

    def t0(self):
        return self._t0

    def t1(self):
        return self._t1

    def duration(self):
        return self._duration

    def rendered_polygon(self, time_step=1):
        result = []
        for t in arange(self.t0(), self.t1(), time_step):
            result.extend([self.x(t), self.y(t)])
        result.extend([self.x(self.t1()), self.y(self.t1())])
        return result

    def __add__(self, other):
        if other == 0:
            return self
        if not isinstance(other, _Trajectory):
            return NotImplemented
        return TrajectoryMerged([self, other])

    __radd__ = __add__
    _duration: float
    _length: float


class TrajectoryMerged(_Trajectory):
    """
    merge trajectories

    Parameters
    ----------
    trajectories : iterable (list, tuple, ...)
        list trajectories to be merged

    Returns
    -------
    merged trajectory : Trajectory

    Notes
    -----
    It is arguably easier just to add or sum trajectories, like


        trajectory = trajectory1 + trajectory2 + trajectory3 or

        trajectory = sum((trajectory, trajectory2, trajectory3))
    """

    def __init__(self, trajectories) -> None:
        self._trajectories = trajectories
        self._duration = sum(trajectory._duration for trajectory in self._trajectories)
        self._t0 = trajectories[0]._t0
        self._t1 = self._t0 + self._duration
        self._length = sum(trajectory._length for trajectory in self._trajectories)
        cum_length = 0
        _t0 = self._t0
        self.cum_lengths = []
        self._t0s = []
        for trajectory in self._trajectories:
            self.cum_lengths.append(cum_length)
            self._t0s.append(_t0)
            cum_length += trajectory._length
            _t0 += trajectory._duration
        self.cum_lengths.append(cum_length)
        self._length = cum_length

    @functools.lru_cache(maxsize=1)
    def index(self, t):
        if t <= self._t0s[0]:
            return 0
        if t >= self._t0s[-1]:
            return len(self._t0s) - 1
        i = searchsorted(self._t0s, t, "left") - 1
        return i

    def __repr__(self):
        return f"TrajectoryMerged(t0={self._t0}, trajectories={self._trajectories}, t0s={self._t0s})"

    def x(self, t: float, _t0: float = None) -> float:
        """
        value of x

        Parameters
        ----------
        t : float
            time at which to evaluate x

        Returns
        -------
        evaluated x : float
        """
        i = self.index(t)
        trajectory = self._trajectories[i]
        t0 = self._t0s[i]
        return trajectory.x(t=t, _t0=t0)

    def y(self, t: float, _t0: float = None) -> float:
        """
        value of y

        Parameters
        ----------
        t : float
            time at which to evaluate y

        Returns
        -------
        evaluated y : float
        """
        i = self.index(t)
        trajectory = self._trajectories[i]
        t0 = self._t0s[i]
        return trajectory.y(t=t, _t0=t0)

    def angle(self, t: float, _t0: float = None) -> float:
        """
        value of angle (in degrees)

        Parameters
        ----------
        t : float
            time at which to evaluate angle

        Returns
        -------
        evaluated angle (in degrees) : float
        """
        i = self.index(t)
        trajectory = self._trajectories[i]
        t0 = self._t0s[i]
        return trajectory.angle(t=t, _t0=t0)

    def in_trajectory(self, t: float) -> bool:
        """
        is t in trajectory?

        Parameters
        ----------
        t : float
            time at which to evaluate

        Returns
        -------
        is t in trajectory? : bool
        """
        return super().in_trajectory(t)

    def t0(self) -> float:
        """
        start time of trajectory

        Returns
        -------
        start time of trajectory : float
        """
        return super().t0()

    def t1(self) -> float:
        """
        end time of trajectory

        Returns
        -------
        end time of trajectory : float
        """
        return super().t1()

    def duration(self) -> float:
        """
        duration of trajectory

        Returns
        -------
        duration of trajectory (t1 - t0): float
        """
        return super().duration()

    def length(self, t: float = None, _t0: float = None) -> float:
        """
        length of traversed trajectory at time t or total length

        Parameters
        ----------
        t : float
            time at which to evaluate length. If omitted, total length will be returned

        Returns
        -------
        length : float
            length of traversed trajectory at time t or

            total length if t omitted
        """
        if t is None:
            i = len(self._trajectories) - 1
        else:
            i = self.index(t)
        trajectory = self._trajectories[i]
        t0 = self._t0s[i]
        return trajectory.length(t=t, _t0=t0) + self.cum_lengths[i]

    def rendered_polygon(self, time_step: float = 1) -> List[Tuple[float, float]]:
        """
        rendered polygon

        Parameters
        ----------
        time_step : float
            defines at which point in time the trajectory has to be rendered

            default : 1

        Returns
        -------
        polygon : list of x, y
            rendered from t0 to t1 with time_step

            can be used directly in sim.AnimatePoints() or AnimatePolygon()
        """
        return super().rendered_polygon(time_step)


class TrajectoryStandstill(_Trajectory):
    """
    Standstill trajectory, to be used in Animatexxx through x, y and angle methods

    Parameters
    ----------
    xy : tuple or list of 2 floats
        initial (and final) position. should be like x, y

    orientation : float or callable
        orientation (angle) in degrees

        a one parameter callable is also accepted (and will be called with 0)

        default: 0

    t0 : float
        time the trajectory should start

        default: env.now()

        if not the first in a merged trajectory or AnimateQueue, ignored

    env : Environment
        environment where the trajectory is defined

        if omitted, default_env will be used
    """

    def __init__(self, xy: Iterable, duration: float, orientation: Union[Callable, float] = 0, t0: float = None, env: "Environment" = None):
        env = g.default_env if env is None else env
        self._t0 = 0 if env is None else (env.now() if t0 is None else t0)

        self._x, self._y = xy
        self._duration = duration
        self._length = 0
        self._t1 = self._t0 + duration
        if callable(orientation):
            self._angle = orientation(0)
        else:
            self._angle = orientation

    def x(self, t: float, _t0: float = None) -> float:
        """
        value of x

        Parameters
        ----------
        t : float
            time at which to evaluate x

        Returns
        -------
        evaluated x : float
        """
        return self._x

    def y(self, t: float, _t0: float = None) -> float:
        """
        value of y

        Parameters
        ----------
        t : float
            time at which to evaluate y

        Returns
        -------
        evaluated y : float
        """
        return self._y

    def angle(self, t: float, _t0: float = None) -> float:
        """
        value of angle (in degrees)

        Parameters
        ----------
        t : float
            time at which to evaluate angle

        Returns
        -------
        evaluated angle (in degrees) : float
        """
        return self._angle

    def in_trajectory(self, t: float) -> bool:
        """
        is t in trajectory?

        Parameters
        ----------
        t : float
            time at which to evaluate

        Returns
        -------
        is t in trajectory? : bool
        """
        return super().in_trajectory(t)

    def t0(self) -> float:
        """
        start time of trajectory

        Returns
        -------
        start time of trajectory : float
        """
        return super().t0()

    def t1(self) -> float:
        """
        end time of trajectory

        Returns
        -------
        end time of trajectory : float
        """
        return super().t1()

    def duration(self) -> float:
        """
        duration of trajectory

        Returns
        -------
        duration of trajectory (t1 - t0): float
        """
        return super().duration()

    def length(self, t: float = None, _t0: float = None) -> float:
        """
        length of traversed trajectory at time t or total length

        Parameters
        ----------
        t : float
            time at which to evaluate length.

        Returns
        -------
        length : float
            always 0

        """
        return 0

    def rendered_polygon(self, time_step: float = 1) -> List[Tuple[float, float]]:
        """
        rendered polygon

        Parameters
        ----------
        time_step : float
            defines at which point in time the trajectory has to be rendered

            default : 1

        Returns
        -------
        polygon : list of x, y
            rendered from t0 to t1 with time_step

            can be used directly in sim.AnimatePoints() or AnimatePolygon()
        """
        return super().rendered_polygon(time_step)


class TrajectoryPolygon(_Trajectory):
    """
    Polygon trajectory, to be used in Animatexxx through x, y and angle methods

    Parameters
    ----------
    polygon : iterable of floats
        should be like x0, y0, x1, y1, ...

    t0 : float
        time the trajectory should start

        default: env.now()

        if not the first in a merged trajectory or AnimateQueue, ignored

    vmax : float
        maximum speed, i.e. position units per time unit

        default: 1

    v0 : float
        velocity at start

        default: vmax

    v1 : float
        velocity at end

        default: vmax

    acc : float
        acceleration rate (position units / time units ** 2)

        default: inf (i.e. no acceleration)

    dec : float
        deceleration rate (position units / time units ** 2)

        default: inf (i.e. no deceleration)

    orientation : float
        default: gives angle in the direction of the movement when calling angle(t)

        if a one parameter callable, the angle in the direction of the movement will be callled

        if a float, this orientation will always be returned as angle(t)

    spline : None or string
        if None (default) or '', polygon is used as such

        if 'bezier' (or any string starting with 'b' or 'B', Bézier splining is used

        if 'catmull_rom' (or any string starting with 'c' or 'C', Catmull-Rom splining is used

    res : int
        resolution of spline (ignored when no splining is applied)

    env : Environment
        environment where the trajectory is defined

        if omitted, default_env will be used

    Notes
    -----
    bezier and catmull_rom splines require numpy to be installed.
    """

    def __init__(
        self,
        polygon: Iterable,
        t0: float = None,
        vmax: float = None,
        v0: float = None,
        v1: float = None,
        acc: float = None,
        dec: float = None,
        orientation: Union[Callable, float] = None,
        spline: str = None,
        res: float = 50,
        env: "Environment" = None,
    ) -> None:
        def catmull_rom_polygon(polygon, res):
            def evaluate(x, v0, v1, v2, v3):
                c1 = 1.0 * v1
                c2 = -0.5 * v0 + 0.5 * v2
                c3 = 1.0 * v0 + -2.5 * v1 + 2.0 * v2 - 0.5 * v3
                c4 = -0.5 * v0 + 1.5 * v1 + -1.5 * v2 + 0.5 * v3
                return ((c4 * x + c3) * x + c2) * x + c1

            if not has_numpy():
                raise ImportError("catmull_rom trajectory requires numpy")

            p_x = []
            p_y = []
            for x, y in zip(polygon[::2], polygon[1::2]):
                p_x.append(x)
                p_y.append(y)

            _x = numpy.empty(res * (len(p_x) - 1) + 1)
            _y = numpy.empty(res * (len(p_x) - 1) + 1)

            _x[-1] = p_x[-1]
            _y[-1] = p_y[-1]

            for i in range(len(p_x) - 1):
                _x[i * res : (i + 1) * res] = numpy.linspace(p_x[i], p_x[i + 1], res, endpoint=False)
                numpy.linspace(polygon[i * 2], polygon[i * 2 + 2], res, endpoint=False)
                _y[i * res : (i + 1) * res] = numpy.array(
                    [
                        evaluate(
                            x,
                            p_y[0] - (p_y[1] - p_y[0]) if i == 0 else p_y[i - 1],
                            p_y[i],
                            p_y[i + 1],
                            p_y[i + 1] + (p_y[i + 1] - p_y[i]) if i == len(p_x) - 2 else p_y[i + 2],
                        )
                        for x in numpy.linspace(0.0, 1.0, res, endpoint=False)
                    ]
                )
            polygon = []
            for xy in zip(_x, _y):
                polygon.extend(xy)
            return polygon

        def bezier_polygon(polygon, res):
            # based on https://github.com/torresjrjr/Bezier.py

            def bezier_curve(t_values, points):
                def two_points(t, P1, P2):
                    return (1 - t) * P1 + t * P2

                def do_points(t, points):
                    newpoints = []
                    for i1 in range(0, len(points) - 1):
                        newpoints += [two_points(t, points[i1], points[i1 + 1])]
                    return newpoints

                def do_point(t, points):
                    newpoints = points
                    while len(newpoints) > 1:
                        newpoints = do_points(t, newpoints)
                    return newpoints[0]

                curve = numpy.array([[0.0] * len(points[0])])
                for t in t_values:
                    curve = numpy.append(curve, [do_point(t, points)], axis=0)
                curve = numpy.delete(curve, 0, 0)
                return curve

            if not has_numpy():
                raise ImportError("bezier trajectory requires numpy")
            points = []
            for x, y in zip(polygon[::2], polygon[1::2]):
                points.append([x, y])
            points = numpy.array(points)
            t_points = numpy.linspace(0, 1, res)

            polygon = []
            curve = bezier_curve(t_points, points)
            for xy in curve:
                polygon.extend(xy)
            return polygon

        if spline is not None:
            if isinstance(spline, str) and spline.lower().startswith("c"):
                polygon = catmull_rom_polygon(polygon, res=res)
            elif isinstance(spline, str) and spline.lower().startswith("b"):
                polygon = bezier_polygon(polygon, res=res)
            elif isinstance(spline, str) and not spline.strip() == "":
                raise ValueError(f"spline {spline} not recognized")

        env = g.default_env if env is None else env
        self._t0 = 0 if env is None else (env.now() if t0 is None else t0)

        cum_length = 0.0
        self.cum_length = []
        self._x = []
        self._y = []
        self._angle = []
        for x, y, next_x, next_y in zip(polygon[::2], polygon[1::2], polygon[2::2], polygon[3::2]):
            dx = next_x - x
            dy = next_y - y
            if orientation is None:
                self._angle.append(math.degrees(math.atan2(dy, dx)))
            else:
                if callable(orientation):
                    self._angle.append(orientation(math.degrees(math.atan2(dy, dx))))
                else:
                    self._angle.append(orientation)
            self._x.append(x)
            self._y.append(y)
            self.cum_length.append(cum_length)
            segment_length = math.sqrt(dx * dx + dy * dy)
            cum_length += segment_length
        self._x.append(next_x)
        self._y.append(next_y)
        self._angle.append(self._angle[-1])
        self.cum_length.append(cum_length)

        self._length = self.cum_length[-1]
        self.movement = _Movement(l=self._length, v0=v0, v1=v1, vmax=vmax, acc=acc, dec=dec)
        self._duration = self.movement.t
        self._t1 = self._t0 + self._duration

    def __repr__(self):
        return f"TrajectoryPolygon(t0={self._t0})"

    @functools.lru_cache(maxsize=1)
    def indexes(self, t, _t0=None):
        if t is None:
            t = self._duration
        else:
            t = t - (self._t0 if _t0 is None else _t0)

        length = self.movement.l_at_t(t)

        if length <= self.cum_length[0]:
            return length, 0, 0
        if length >= self.cum_length[-1]:
            return 0, len(self.cum_length) - 1, len(self.cum_length) - 1

        i = searchsorted(self.cum_length, length) - 1
        return length, i, i + 1

    def x(self, t: float, _t0: float = None) -> float:
        """
        value of x

        Parameters
        ----------
        t : float
            time at which to evaluate x

        Returns
        -------
        evaluated x : float
        """
        length, i, j = self.indexes(t, _t0=_t0)
        return interp(length, [self.cum_length[i], self.cum_length[j]], [self._x[i], self._x[j]])

    def y(self, t: float, _t0: float = None) -> float:
        """
        value of y

        Parameters
        ----------
        t : float
            time at which to evaluate y

        Returns
        -------
        evaluated y : float
        """
        length, i, j = self.indexes(t, _t0=_t0)
        return interp(length, [self.cum_length[i], self.cum_length[j]], [self._y[i], self._y[j]])

    def angle(self, t: float, _t0: float = None) -> float:
        """
        value of angle (in degrees)

        Parameters
        ----------
        t : float
            time at which to evaluate angle

        Returns
        -------
        evaluated angle (in degrees) : float
        """
        length, i, j = self.indexes(t, _t0=_t0)
        return self._angle[i]

    def in_trajectory(self, t: float) -> bool:
        """
        is t in trajectory?

        Parameters
        ----------
        t : float
            time at which to evaluate

        Returns
        -------
        is t in trajectory? : bool
        """
        return super().in_trajectory(t)

    def t0(self) -> float:
        """
        start time of trajectory

        Returns
        -------
        start time of trajectory : float
        """
        return super().t0()

    def t1(self) -> float:
        """
        end time of trajectory

        Returns
        -------
        end time of trajectory : float
        """
        return super().t1()

    def duration(self) -> float:
        """
        duration of trajectory

        Returns
        -------
        duration of trajectory (t1 - t0): float
        """
        return super().duration()

    def length(self, t: float = None, _t0: float = None) -> float:
        """
        length of traversed trajectory at time t or total length

        Parameters
        ----------
        t : float
            time at which to evaluate lenght. If omitted, total length will be returned

        Returns
        -------
        length : float
            length of traversed trajectory at time t or

            total length if t omitted
        """
        length, i, j = self.indexes(t, _t0=_t0)
        return self.cum_length[i] + length

    def rendered_polygon(self, time_step: float = 1) -> List[Tuple[float, float]]:
        """
        rendered polygon

        Parameters
        ----------
        time_step : float
            defines at which point in time the trajectory has to be rendered

            default : 1

        Returns
        -------
        polygon : list of x, y
            rendered from t0 to t1 with time_step

            can be used directly in sim.AnimatePoints() or AnimatePolygon()
        """
        return super().rendered_polygon(time_step)


class TrajectoryCircle(_Trajectory):
    """
    Circle (arc) trajectory, to be used in Animatexxx through x, y and angle methods

    Parameters
    ----------
    radius : float
        radius of the circle or arc

    x_center : float
        x-coordinate of the circle

    y_center : float
        y-coordinate of the circle

    angle0 : float
        start angle in degrees

        default: 0

    angle1 : float
        end angle in degrees

        default: 360

    t0 : float
        time the trajectory should start

        default: env.now()

        if not the first in a merged trajectory or AnimateQueue, ignored

    vmax : float
        maximum speed, i.e. position units per time unit

        default: 1

    v0 : float
        velocity at start

        default: vmax

    v1 : float
        velocity at end

        default: vmax

    acc : float
        acceleration rate (position units / time units ** 2)

        default: inf (i.e. no acceleration)

    dec : float
        deceleration rate (position units / time units ** 2)

        default: inf (i.e. no deceleration)

    orientation : float
        default: gives angle in the direction of the movement when calling angle(t)

        if a one parameter callable, the angle in the direction of the movement will be callled

        if a float, this orientation will always be returned as angle(t)

    env : Environment
        environment where the trajectory is defined

        if omitted, default_env will be used
    """

    def __init__(
        self,
        radius: float,
        x_center: float = 0,
        y_center: float = 0,
        angle0: float = 0,
        angle1: float = 360,
        t0: float = None,
        vmax: float = None,
        v0: float = None,
        v1: float = None,
        acc: float = None,
        dec: float = None,
        orientation: Union[Callable, float] = None,
        env: "Environment" = None,
    ):
        env = g.default_env if env is None else env
        self._t0 = 0 if env is None else (env.now() if t0 is None else t0)

        self.radius = radius
        self.angle0 = angle0
        self.angle1 = angle1
        self.x_center = x_center
        self.y_center = y_center
        self._length = abs(math.radians(self.angle1 - self.angle0)) * self.radius
        self.movement = _Movement(l=self._length, v0=v0, v1=v1, vmax=vmax, acc=acc, dec=dec)
        self._duration = self.movement.t
        self._t1 = self._t0 + self._duration
        self.orientation = orientation

    def __repr__(self):
        return f"TrajectoryCircle(t0={self._t0})"

    def x(self, t: float, _t0: float = None) -> float:
        """
        value of x

        Parameters
        ----------
        t : float
            time at which to evaluate x

        Returns
        -------
        evaluated x : float
        """
        length = self.length(t, _t0=_t0)
        return self.x_center + self.radius * math.cos(math.radians(interp(length, (0, self._length), (self.angle0, self.angle1))))

    def y(self, t: float, _t0: float = None) -> float:
        """
        value of y

        Parameters
        ----------
        t : float
            time at which to evaluate x

        Returns
        -------
        evaluated y : float
        """
        length = self.length(t, _t0=_t0)
        return self.y_center + self.radius * math.sin(math.radians(interp(length, (0, self._length), (self.angle0, self.angle1))))

    def angle(self, t: float, _t0: float = None) -> float:
        """
        value of angle

        Parameters
        ----------
        t : float
            time at which to evaluate x

        Returns
        -------
        evaluated amgle : float
        """
        length = self.length(t, _t0=_t0)
        if self.angle0 < self.angle1:
            result = interp(length, (0, self._length), (self.angle0, self.angle1)) + 90
        else:
            result = interp(length, (0, self._length), (self.angle0, self.angle1)) - 90

        if self.orientation is None:
            return result

        if callable(self.orientation):
            return self.orientation(result)
        return self.orientation

    def in_trajectory(self, t: float) -> bool:
        """
        is t in trajectory?

        Parameters
        ----------
        t : float
            time at which to evaluate

        Returns
        -------
        is t in trajectory? : bool
        """
        return super().in_trajectory(t)

    def t0(self) -> float:
        """
        start time of trajectory

        Returns
        -------
        start time of trajectory : float
        """
        return super().t0()

    def t1(self) -> float:
        """
        end time of trajectory

        Returns
        -------
        end time of trajectory : float
        """
        return super().t1()

    def duration(self) -> float:
        """
        duration of trajectory

        Returns
        -------
        duration of trajectory (t1 - t0): float
        """
        return super().duration()

    @functools.lru_cache(maxsize=1)
    def length(self, t: Any = None, _t0: float = None) -> float:
        """
        length of traversed trajectory at time t or total length

        Parameters
        ----------
        t : float
            time at which to evaluate length. If omitted, total length will be returned

        Returns
        -------
        length : float
            length of traversed trajectory at time t or

            total length if t omitted
        """
        t0 = self._t0 if _t0 is None else _t0
        t1 = t0 + self._duration
        if t < t0:
            t = t0
        elif t > t1:
            t = t1
        return self.movement.l_at_t(t - t0)

    def rendered_polygon(self, time_step: float = 1) -> List[Tuple[float, float]]:
        """
        rendered polygon

        Parameters
        ----------
        time_step : float
            defines at which point in time the trajectory has to be rendered

            default : 1

        Returns
        -------
        polygon : list of x, y
            rendered from t0 to t1 with time_step

            can be used directly in sim.AnimatePoints() or AnimatePolygon()
        """
        return super().rendered_polygon(time_step)


class Component:
    """
    Component object

    A salabim component is used as component (primarily for queueing)
    or as a component with a process

    Usually, a component will be defined as a subclass of Component.

    Parameters
    ----------
    name : str
        name of the component.

        if the name ends with a period (.),
        auto serializing will be applied

        if the name end with a comma,
        auto serializing starting at 1 will be applied

        if omitted, the name will be derived from the class
        it is defined in (lowercased)

    at : float or distribution
        schedule time

        if omitted, now is used

        if distribution, the distribution is sampled

    delay : float or distributiom
        schedule with a delay

        if omitted, no delay

        if distribution, the distribution is sampled

    priority : float
        priority

        default: 0

        if a component has the same time on the event list, this component is sorted accoring to
        the priority.

    urgent : bool
        urgency indicator

        if False (default), the component will be scheduled
        behind all other components scheduled
        for the same time and priority

        if True, the component will be scheduled
        in front of all components scheduled
        for the same time and priority

    process : str
        name of process to be started.

        if None (default), it will try to start self.process()

        if null string, no process will be started even if self.process() exists,
        i.e. become a data component.


    suppress_trace : bool
        suppress_trace indicator

        if True, this component will be excluded from the trace

        If False (default), the component will be traced

        Can be queried or set later with the suppress_trace method.

    suppress_pause_at_step : bool
        suppress_pause_at_step indicator

        if True, if this component becomes current, do not pause when stepping

        If False (default), the component will be paused when stepping

        Can be queried or set later with the suppress_pause_at_step method.

    skip_standby : bool
        skip_standby indicator

        if True, after this component became current, do not activate standby components

        If False (default), after the component became current  activate standby components

        Can be queried or set later with the skip_standby method.

    mode : str preferred
        mode

        will be used in trace and can be used in animations

        if omitted, the mode will be "".

        also mode_time will be set to now.

    cap_now : bool
        indicator whether times (at, delay) in the past are allowed. If, so now() will be used.
        default: sys.default_cap_now(), usualy False

    env : Environment
        environment where the component is defined

        if omitted, default_env will be used
    """

    def __init__(
        self,
        name: str = None,
        at: Union[float, Callable] = None,
        delay: Union[float, Callable] = None,
        priority: float = None,
        urgent: bool = None,
        process: str = None,
        suppress_trace: bool = False,
        suppress_pause_at_step: bool = False,
        skip_standby: bool = False,
        mode: str = "",
        cap_now: bool = None,
        env: "Environment" = None,
        **kwargs,
    ):
        self.env = _set_env(env)

        _set_name(name, self.env._nameserializeComponent, self)
        self._qmembers = {}
        self._process = None
        self.status = _StatusMonitor(name=self.name() + ".status", level=True, initial_tally=data, env=self.env)

        self._requests = collections.OrderedDict()
        self._claims = collections.OrderedDict()
        self._waits = []
        self._from_stores = []
        self._to_stores = []
        self._on_event_list = False
        self._scheduled_time = inf
        self._failed = False
        self._skip_standby = skip_standby
        self._creation_time = self.env._now
        self._suppress_trace = suppress_trace
        self._suppress_pause_at_step = suppress_pause_at_step
        self.mode = _ModeMonitor(parent=self, name=self.name() + ".mode", level=True, initial_tally=mode, env=self.env)

        self._mode_time = self.env._now
        self._aos = {}
        self._animation_children = set()

        if process is None:
            if hasattr(self, "process"):
                p = self.process
                process_name = "process"
            else:
                p = None
        else:
            if process == "":
                p = None
            else:
                try:
                    p = getattr(self, process)
                    process_name = process
                except AttributeError:
                    raise AttributeError("self." + process + " does not exist")

        if p is None:
            if at is not None:
                raise TypeError("at is not allowed for a data component")
            if delay is not None:
                raise TypeError("delay is not allowed for a data component")
            if urgent is not None:
                raise TypeError("urgent is not allowed for a data component")
            if priority is not None:
                raise TypeError("priority is not allowed for a data component")
            if self.env._trace:
                if self._name == "main":
                    self.env.print_trace("", "", self.name() + " create", self._modetxt())
                else:
                    self.env.print_trace("", "", self.name() + " create data component", self._modetxt())
        else:
            self.env.print_trace("", "", self.name() + " create", self._modetxt())

            kwargs_p = {}
            if kwargs:
                parameters = inspect.signature(p).parameters

                for kwarg in list(kwargs):
                    if kwarg in parameters:
                        kwargs_p[kwarg] = kwargs[kwarg]
                        del kwargs[kwarg]  # here kwargs consumes the used arguments

            if inspect.isgeneratorfunction(p):
                if self.env._yieldless:
                    raise ValueError(
                        """process may not be a generator (contain yield statements.)
Maybe this a non yieldless model. In that case:
- add sim.yieldless(False) or
- remove all yields or
- run salabim_unyield.py"""
                    )

                self.env._any_yield = True
                self._process = p(**kwargs_p)
                self._process_isgenerator = True
            else:
                if not self.env._yieldless and not self.env._any_yield:
                    raise ValueError(
                        """process must be a generator (contain yield statements.)
Maybe this a yieldless model. In that case:
- remove sim.yieldless(False)
If it is indeed a yield model, make this process method into a generator, e.g.
by adding at the end:
    return
    yield  # just to make this a generator"""
                    )
                self._process = p
                self._process_isgenerator = False
                self._process_kwargs = kwargs_p

            extra = "process=" + process_name

            urgent = bool(urgent)
            if priority is None:
                priority = 0

            if delay is None:
                delay = 0.0
            else:
                delay = self.env.spec_to_duration(delay)

            if at is None:
                scheduled_time = self.env._now + delay
            else:
                at = self.env.spec_to_time(at)
                scheduled_time = at + self.env._offset + delay
            self.status._value = scheduled
            if self.env._yieldless:
                self._glet = greenlet.greenlet(lambda: self._process(**kwargs_p), parent=self.env._glet)
            self._reschedule(scheduled_time, priority, urgent, "activate", cap_now, extra=extra)
        self.setup(**kwargs)

    overridden_lineno = None

    def animation_objects(self, id: Any) -> Tuple:
        """
        defines how to display a component in AnimateQueue

        Parameters
        ----------
        id : any
            id as given by AnimateQueue. Note that by default this the reference to the AnimateQueue object.

        Returns
        -------
        List or tuple containg

            size_x : how much to displace the next component in x-direction, if applicable

            size_y : how much to displace the next component in y-direction, if applicable

            animation objects : instances of Animate class

            default behaviour:

            square of size 40 (displacements 50), with the sequence number centered.

        Note
        ----
        If you override this method, be sure to use the same header, either with or without the id parameter.

        """
        size_x = 50
        size_y = 50
        ao0 = AnimateRectangle(text=str(self.sequence_number()), textcolor="bg", spec=(-20, -20, 20, 20), linewidth=0, fillcolor="fg")
        return (size_x, size_y, ao0)

    def animation3d_objects(self, id: Any) -> Tuple:
        """
        defines how to display a component in Animate3dQueue

        Parameters
        ----------
        id : any
            id as given by Animate3dQueue. Note that by default this the reference to the Animate3dQueue object.

        Returns
        -------
        List or tuple containg

            size_x : how much to displace the next component in x-direction, if applicable

            size_y : how much to displace the next component in y-direction, if applicable

            size_z : how much to displace the next component in z-direction, if applicable

            animation objects : instances of Animate3dBase class

            default behaviour:

            white 3dbox of size 8, placed on the z=0 plane (displacements 10).

        Note
        ----
        If you override this method, be sure to use the same header, either with or without the id parameter.


        Note
        ----
        The animation object should support the x_offset, y_offset and z_offset attributes, in order to be able
        to position the object correctly. All native salabim Animate3d classes are offset aware.
        """
        size_x = 10
        size_y = 10
        size_z = 10
        ao0 = Animate3dBox(x_len=8, y_len=8, z_len=8, x_ref=0, y_ref=0, z_ref=1, color="white", shaded=True)
        return (size_x, size_y, size_z, ao0)

    def _remove_from_aos(self, q):
        if q in self._aos:
            for ao in self._aos[q][2:]:
                ao.remove()
            del self._aos[q]

    def setup(self) -> None:
        """
        called immediately after initialization of a component.

        by default this is a dummy method, but it can be overridden.

        only keyword arguments will be passed

        Example
        -------
            class Car(sim.Component):
                def setup(self, color):
                    self.color = color

                def process(self):
                    ...

            redcar=Car(color="red")

            bluecar=Car(color="blue")
        """
        pass

    def __repr__(self):
        return object_to_str(self) + " (" + self.name() + ")"

    def reset_monitors(self, monitor: bool = None, stats_only: bool = None) -> None:
        """
        resets the monitor for the component's status and mode monitors

        Parameters
        ----------
        monitor : bool
            if True, monitoring will be on.

            if False, monitoring is disabled

            if omitted, no change of monitoring state

        stats_only : bool
            if True, only statistics will be collected (using less memory, but also less functionality)

            if False, full functionality

            if omittted, no change of stats_only
        """
        self.status.reset(monitor=monitor, stats_only=stats_only)
        self.mode.reset(monitor=monitor, stats_only=stats_only)


    def register(self, registry: List) -> "Component":
        """
        registers the component in the registry

        Parameters
        ----------
        registry : list
            list of (to be) registered objects

        Returns
        -------
        component (self) : Component

        Note
        ----
        Use Component.deregister if component does not longer need to be registered.
        """
        if not isinstance(registry, list):
            raise TypeError("registry not list")
        if self in registry:
            raise ValueError(self.name() + " already in registry")
        registry.append(self)
        return self

    def deregister(self, registry: List) -> "Component":
        """
        deregisters the component in the registry

        Parameters
        ----------
        registry : list
            list of registered components

        Returns
        -------
        component (self) : Component
        """
        if not isinstance(registry, list):
            raise TypeError("registry not list")
        if self not in registry:
            raise ValueError(self.name() + " not in registry")
        registry.remove(self)
        return self

    def print_info(self, as_str: "bool" = False, file: TextIO = None) -> str:
        """
        prints information about the component

        Parameters
        ----------
        as_str: bool
            if False (default), print the info
            if True, return a string containing the info

        file: file
            if None(default), all output is directed to stdout

            otherwise, the output is directed to the file

        Returns
        -------
        info (if as_str is True) : str
        """
        result = []
        result.append(object_to_str(self) + " " + hex(id(self)))
        result.append("  name=" + self.name())
        result.append("  class=" + str(type(self)).split(".")[-1].split("'")[0])
        result.append("  suppress_trace=" + str(self._suppress_trace))
        result.append("  suppress_pause_at_step=" + str(self._suppress_pause_at_step))
        result.append("  status=" + self.status())
        result.append("  mode=" + self.mode())
        result.append("  mode_time=" + self.env.time_to_str(self.mode_time()))
        result.append("  creation_time=" + self.env.time_to_str(self.creation_time()))
        result.append("  scheduled_time=" + self.env.time_to_str(self.scheduled_time()))
        if len(self._qmembers) > 0:
            result.append("  member of queue(s):")
            for q in sorted(self._qmembers, key=lambda obj: obj.name().lower()):
                result.append(
                    "    "
                    + pad(q.name(), 20)
                    + " enter_time="
                    + self.env.time_to_str(self._qmembers[q].enter_time - self.env._offset)
                    + " priority="
                    + str(self._qmembers[q].priority)
                )
        if len(self._requests) > 0:
            result.append("  requesting resource(s):")

            for r in sorted(list(self._requests), key=lambda obj: obj.name().lower()):
                result.append("    " + pad(r.name(), 20) + " quantity=" + str(self._requests[r]))
        if len(self._claims) > 0:
            result.append("  claiming resource(s):")

            for r in sorted(list(self._claims), key=lambda obj: obj.name().lower()):
                result.append("    " + pad(r.name(), 20) + " quantity=" + str(self._claims[r]))
        if len(self._waits) > 0:
            if self._wait_all:
                result.append("  waiting for all of state(s):")
            else:
                result.append("  waiting for any of state(s):")
            for s, value, _ in self._waits:
                result.append("    " + pad(s.name(), 20) + " value=" + str(value))
        return return_or_print(result, as_str, file)

    def _push(self, t, priority, urgent, return_value=None, switch=True):
        if t != inf:
            self.env._seq += 1
            if urgent:
                seq = -self.env._seq
            else:
                seq = self.env._seq
            self._on_event_list = True
            heapq.heappush(self.env._event_list, (t, priority, seq, self, return_value))
        if self.env._yieldless:
            if self is self.env._current_component:
                self.env._glet.switch()

    def _remove(self):
        if self._on_event_list:
            for i in range(len(self.env._event_list)):
                if self.env._event_list[i][3] == self:
                    self.env._event_list[i] = self.env._event_list[0]
                    self.env._event_list.pop(0)
                    heapq.heapify(self.env._event_list)
                    self._on_event_list = False
                    return
            raise Exception("remove error", self.name())
        if self.status.value == standby:
            if self in self.env._standbylist:
                self.env._standbylist.remove(self)
            if self in self.env._pendingstandbylist:
                self.env._pendingstandbylist.remove(self)

    def _check_fail(self):
        if self._requests:
            if self.env._trace:
                self.env.print_trace("", "", self.name(), "request failed")
            for r in list(self._requests):
                self.leave(r._requesters)
                if r._requesters._length == 0:
                    r._minq = inf
            self._requests = collections.OrderedDict()
            self._failed = True

        if self._waits:
            if self.env._trace:
                self.env.print_trace("", "", self.name(), "wait failed")
            for state, _, _ in self._waits:
                if self in state._waiters:  # there might be more values for this state
                    self.leave(state._waiters)
            self._waits = []
            self._failed = True

        if self._from_stores:
            if self.env._trace:
                self.env.print_trace("", "", self.name(), "from_store failed")
            for store in list(self._from_stores):
                self.leave(store._from_store_requesters)
            self._from_stores = []
            self._failed = True

        if self._to_stores:
            if self.env._trace:
                self.env.print_trace("", "", self.name(), "to_store failed")
            for store in list(self._to_stores):
                self.leave(store._to_store_requesters)
            self._to_stores = []
            self._failed = True

    def _reschedule(self, scheduled_time, priority, urgent, caller, cap_now, extra="", s0=None, return_value=None):
        if scheduled_time < self.env._now:
            if cap_now is None:
                cap_now = g._default_cap_now
            if cap_now:
                scheduled_time = self.env._now
            else:
                raise ValueError(f"scheduled time ({scheduled_time:0.3f}) before now ({self.env._now:0.3f})")
        self._scheduled_time = scheduled_time
        if self.env._trace:
            if extra == "*":
                scheduled_time_str = "ends on no events left  "
                extra = " "
            else:
                scheduled_time_str = "scheduled for " + self.env.time_to_str(scheduled_time - self.env._offset).strip()
            if (scheduled_time == self.env._now) or (scheduled_time == inf):
                delta = ""
            else:
                delta = f" +{self.env.duration_to_str(scheduled_time - self.env._now)}"
            lineno = self.lineno_txt(add_at=True)
            self.env.print_trace(
                "",
                "",
                self.name() + " " + caller + delta,
                merge_blanks(scheduled_time_str + _prioritytxt(priority) + _urgenttxt(urgent) + lineno, self._modetxt(), extra),
                s0=s0,
            )
        self._push(scheduled_time, priority, urgent, return_value)

    def activate(
        self,
        at: Union[float, Callable] = None,
        delay: Union[Callable, float] = 0,
        priority: float = 0,
        urgent: bool = False,
        process: str = None,
        keep_request: bool = False,
        keep_wait: bool = False,
        mode: str = None,
        cap_now: bool = None,
        **kwargs,
    ) -> None:
        """
        activate component

        Parameters
        ----------
        at : float or distribution
            schedule time

            if omitted, now is used

            inf is allowed

            if distribution, the distribution is sampled

        delay : float or distribution
            schedule with a delay

            if omitted, no delay

            if distribution, the distribution is sampled

        priority : float
            priority

            default: 0

            if a component has the same time on the event list, this component is sorted accoring to
            the priority.

        urgent : bool
            urgency indicator

            if False (default), the component will be scheduled
            behind all other components scheduled
            for the same time and priority

            if True, the component will be scheduled
            in front of all components scheduled
            for the same time and priority

        process : str
            name of process to be started.

            if None (default), process will not be changed

            if the component is a data component, the
            process method will be used as the default process.

        keep_request : bool
            this affects only components that are requesting.

            if True, the requests will be kept and thus the status will remain requesting

            if False (the default), the request(s) will be canceled and the status will become scheduled

        keep_wait : bool
            this affects only components that are waiting.

            if True, the waits will be kept and thus the status will remain waiting

            if False (the default), the wait(s) will be canceled and the status will become scheduled

        cap_now : bool
            indicator whether times (at, delay) in the past are allowed. If, so now() will be used.
            default: sys.default_cap_now(), usualy False

        mode : str preferred
            mode

            will be used in the trace and can be used in animations

            if nothing specified, the mode will be unchanged.

            also mode_time will be set to now, if mode is set.

        Note
        ----
        Only if yieldless is False: if to be applied to the current component, use ``yield self.activate()``.

        if both at and delay are specified, the component becomes current at the sum
        of the two values.
        """
        p = None
        if process is None:
            if self.status.value == data:
                if hasattr(self, "process"):
                    p = self.process
                    process_name = "process"
                else:
                    raise AttributeError("no process for data component")
        else:
            try:
                p = getattr(self, process)
                process_name = process
            except AttributeError:
                raise AttributeError("self." + process + " does not exist")

        if p is None:
            extra = ""
        else:
            if kwargs:
                parameters = inspect.signature(p).parameters

                for kwarg in kwargs:
                    if kwarg not in parameters:
                        raise TypeError("unexpected keyword argument '" + kwarg + "'")

            if inspect.isgeneratorfunction(p):
                if self.env._yieldless:
                    raise ValueError(
                        """process may not be a generator (contain yield statements.)
Maybe this a non yieldless model. In that case:
- add sim.yieldless(False) or
- remove all yields or
- run salabim_unyield.py"""
                    )
                self.env._any_yield = True
                self._process = p(**kwargs)
                self._process_isgenerator = True
            else:
                if not self.env._yieldless and not self.env._any_yield:
                    raise ValueError(
                        """process must be a generator (contain yield statements.)
Maybe this a yieldless model. In that case:
- remove sim.yieldless(False)
If it is indeed a yield model, make this process method into a generator, e.g.
by adding:
    return
    yield"""
                    )
                self._process = p
                self._process_isgenerator = False
                self._process_kwargs = kwargs

            extra = "process=" + process_name
            if self.env._yieldless:
                self._glet = greenlet.greenlet(self._process, parent=self.env._glet)

        if self.status.value != current:
            self._remove()
            if p is None:
                if not (keep_request or keep_wait):
                    self._check_fail()
            else:
                self._check_fail()

        self.set_mode(mode)

        delay = self.env.spec_to_duration(delay)

        if at is None:
            scheduled_time = self.env._now + delay
        else:
            at = self.env.spec_to_time(at)
            scheduled_time = at + self.env._offset + delay

        self.status._value = scheduled
        self._reschedule(scheduled_time, priority, urgent, "activate", cap_now, extra=extra)

    def hold(
        self,
        duration: Union[float, Callable] = None,
        till: Union[float, Callable] = None,
        priority: float = 0,
        urgent: bool = False,
        mode: str = None,
        interrupted: Union[bool, int] = False,
        cap_now: bool = None,
    ) -> None:
        """
        hold the component

        Parameters
        ----------
        duration : float or distribution
            specifies the duration

            if omitted, 0 is used

            inf is allowed

            if distribution, the distribution is sampled

        till : float or distribution
            specifies at what time the component will become current

            if omitted, now is used

            inf is allowed

            if distribution, the distribution is sampled

        priority : float
            priority

            default: 0

            if a component has the same time on the event list, this component is sorted accoring to
            the priority.

        urgent : bool
            urgency indicator

            if False (default), the component will be scheduled
            behind all other components scheduled
            for the same time and priority

            if True, the component will be scheduled
            in front of all components scheduled
            for the same time and priority

        mode : str preferred
            mode

            will be used in trace and can be used in animations

            if nothing specified, the mode will be unchanged.

            also mode_time will be set to now, if mode is set.

        interrupted : bool or int
            if False (default), not interrupted

            if True, the component will immediately go into interrupted state

            if an integer, this is the interrupt_level

        cap_now : bool
            indicator whether times (duration, till) in the past are allowed. If, so now() will be used.
            default: sys.default_cap_now(), usualy False

        Note
        ----
        Only if yieldless is False: if to be used for the current component, use ``yield self.hold(...)``.


        if both duration and till are specified, the component will become current at the sum of
        these two.
        """
        if self.status.value != passive:
            if self.status.value != current:
                self._checkisnotdata()
                self._remove()
                self._check_fail()

        self.set_mode(mode)

        if till is None:
            if duration is None:
                scheduled_time = self.env._now
            else:
                duration = self.env.spec_to_duration(duration)
                scheduled_time = self.env._now + duration
        else:
            if duration is None:
                till = self.env.spec_to_time(till)
                scheduled_time = till + self.env._offset
            else:
                raise ValueError("both duration and till specified")
        if scheduled_time < self.env._now:
            if cap_now is None:
                cap_now = g._default_cap_now
            if cap_now:
                scheduled_time = self.env._now
            else:
                raise ValueError(f"scheduled time ({scheduled_time:0.3f}) before now ({self.env._now:0.3f})")

        if interrupted:
            self._remaining_duration = scheduled_time - self.env._now
            self._interrupted_status = scheduled
            self._interrupt_level = int(interrupted)
            self.status._value = "interrupted"
            if self.env._trace:
                caller = "hold-interrupt"
                lineno = self.lineno_txt(add_at=True)

                scheduled_time_str = "scheduled for " + self.env.time_to_str(scheduled_time - self.env._offset).strip()
                if (scheduled_time == self.env._now) or (scheduled_time == inf):
                    delta = ""
                else:
                    delta = f" +{self.env.duration_to_str(scheduled_time - self.env._now)}"
                lineno = self.lineno_txt(add_at=True)
                self.env.print_trace(
                    "",
                    "",
                    self.name() + " " + caller + delta,
                    merge_blanks(scheduled_time_str + _prioritytxt(priority) + _urgenttxt(urgent) + lineno, self._modetxt(), ""),
                )

            if self.env._yieldless:
                if self is self.env._current_component:
                    self.env._glet.switch()
        else:
            self.status._value = scheduled
            self._reschedule(scheduled_time, priority, urgent, "hold", cap_now)

    def passivate(self, mode: str = None) -> None:
        """
        passivate the component

        Parameters
        ----------
        mode : str preferred
            mode

            will be used in trace and can be used in animations

            if nothing is specified, the mode will be unchanged.

            also mode_time will be set to now, if mode is set.

        Note
        ----
        Only if yieldless is False: if to be used for the current component (nearly always the case), use ``yield self.passivate()``.
        """
        if self.status.value == current:
            self._remaining_duration = 0.0
        else:
            self._checkisnotdata()
            self._remove()
            self._check_fail()
            self._remaining_duration = self._scheduled_time - self.env._now
        self._scheduled_time = inf

        self.set_mode(mode)
        if self.env._trace:
            lineno = self.lineno_txt(add_at=True)
            self.env.print_trace("", "", self.name() + " passivate", merge_blanks(lineno, self._modetxt()))
        self.status._value = passive

        if self.env._yieldless:
            if self is self.env._current_component:
                self.env._glet.switch()

    def interrupt(self, mode: str = None) -> None:
        """
        interrupt the component

        Parameters
        ----------
        mode : str preferred
            mode

            will be used in trace and can be used in animations

            if nothing is specified, the mode will be unchanged.

            also mode_time will be set to now, if mode is set.

        Note
        ----
        The component has to be scheduled or interrupted.

        Use resume() to resume
        """
        if self.status.value not in (scheduled, interrupted):
            raise ValueError(self.name() + " component not scheduled")
        self.set_mode(mode)
        if self.status.value == interrupted:
            self._interrupt_level += 1
            extra = "." + str(self._interrupt_level)
        else:
            self._checkisnotdata()
            self._remove()
            self._remaining_duration = self._scheduled_time - self.env._now
            self._interrupted_status = self.status.value
            self._interrupt_level = 1
            self.status._value = interrupted
            extra = ""
        lineno = self.lineno_txt(add_at=True)
        self.env.print_trace("", "", self.name() + " interrupt" + extra, merge_blanks(lineno, self._modetxt()))

    def resume(self, all: bool = False, mode: str = None, priority: float = 0, urgent: bool = False) -> None:
        """
        resumes an interrupted component

        Parameters
        ----------
        all : bool
            if True, the component returns to the original status, regardless of the number of interrupt levels

            if False (default), the interrupt level will be decremented and if the level reaches 0,
            the component will return to the original status.

        mode : str preferred
            mode

            will be used in trace and can be used in animations

            if nothing is specified, the mode will be unchanged.

            also mode_time will be set to now, if mode is set.

        priority : float
            priority

            default: 0

            if a component has the same time on the event list, this component is sorted accoring to
            the priority.


        urgent : bool
            urgency indicator

            if False (default), the component will be scheduled
            behind all other components scheduled
            for the same time and priority

            if True, the component will be scheduled
            in front of all components scheduled
            for the same time and priority

        Note
        ----
        Can be only applied to interrupted components.

        """
        if self.status.value == interrupted:
            self.set_mode(mode)
            self._interrupt_level -= 1
            if self._interrupt_level and (not all):
                self.env.print_trace("", "", self.name() + " resume (interrupted." + str(self._interrupt_level) + ")", merge_blanks(self._modetxt()))
            else:
                self.status._value = self._interrupted_status
                lineno = self.lineno_txt(add_at=True)
                self.env.print_trace("", "", self.name() + " resume (" + self.status() + ")", merge_blanks(lineno, self._modetxt()))
                if self.status.value == scheduled:
                    self._reschedule(self.env._now + self._remaining_duration, priority, urgent, "hold", False)
                else:
                    raise Exception(self.name() + " unexpected interrupted_status", self.status.value())
        else:
            raise ValueError(self.name() + " not interrupted")

    def cancel(self, mode: str = None) -> None:
        """
        cancel component (makes the component data)

        Parameters
        ----------
        mode : str preferred
            mode

            will be used in trace and can be used in animations

            if nothing specified, the mode will be unchanged.

            also mode_time will be set to now, if mode is set.

        Note
        ----
        Only if yieldless is False: if to be used for the current component, use ``yield self.cancel()``.
        """
        if self.status.value == data:
            if self.env._trace:
                self.env.print_trace("", "", "cancel (on data component) " + self.name() + " " + self._modetxt())
            return
        if self.status.value != current:
            self._checkisnotdata()
            self._remove()
            self._check_fail()
        for r in list(self._claims):
            self._release(r)
        self._process = None
        self._scheduled_time = inf
        self.set_mode(mode)
        if self.env._trace:
            self.env.print_trace("", "", "cancel " + self.name() + " " + self._modetxt())
        self.status._value = data
        if self.env._yieldless:
            if self is self.env._current_component:
                self.env._glet.switch()

    def standby(self, mode: str = None) -> None:
        """
        puts the component in standby mode

        Parameters
        ----------
        mode : str preferred
            mode

            will be used in trace and can be used in animations

            if nothing specified, the mode will be unchanged.

            also mode_time will be set to now, if mode is set.

        Note
        ----
        Not allowed for data components or main.

        Only if yieldless is False: if to be used for the current component
        (which will be nearly always the case),
        use ``yield self.standby()``.
        """
        if self.status.value != current:
            self._checkisnotdata()
            self._checkisnotmain()
            self._remove()
            self._check_fail()
        self._scheduled_time = self.env._now
        self.set_mode(mode)
        caller = "standby"
        self.env._standbylist.append(self)
        self.status._value = standby

        if self.env._trace:
            if self.env._buffered_trace:
                self.env._buffered_trace = False
            else:
                lineno = self.lineno_txt(add_at=True)
                self.env.print_trace("", "", caller, merge_blanks(lineno, self._modetxt()))

        if self.env._yieldless:
            if self is self.env._current_component:
                self.env._glet.switch()

    def from_store(
        self,
        store: Union["Store", Iterable],
        filter: Callable = lambda c: True,
        fail_priority: float = 0,
        urgent: bool = True,
        fail_at: float = None,
        fail_delay: float = None,
        mode: str = None,
        cap_now: bool = None,
        key: callable = None,
    ) -> "Component":
        """
        get item from store(s)

        Parameters
        ----------
        store : store or iterable stores
            store(s) to get item from

        filter : callable
            only components that return True when applied to them will be considered

            should be a function with one parameter(component) and returning a bool

            default: lambda c: True (i.e. always return True)

        fail_priority : float
            priority of the fail event

            default: 0

            if a component has the same time on the event list, this component is sorted according to
            the priority.

        urgent : bool
            urgency indicator

            if False (default), the component will be scheduled
            behind all other components scheduled
            for the same time and priority

            if True, the component will be scheduled
            in front of all components scheduled
            for the same time and priority

        fail_at : float or distribution
            time out

            if the request is not honored before fail_at,
            the request will be cancelled and the
            parameter failed will be set.

            if not specified, the request will not time out.

            if distribution, the distribution is sampled

        fail_delay : float or distribution
            time out

            if the request is not honored before now+fail_delay,
            the request will be cancelled and the
            parameter failed will be set.

            if not specified, the request will not time out.

            if distribution, the distribution is sampled

        mode : str preferred
            mode

            will be used in trace and can be used in animations

            if nothing specified, the mode will be unchanged.

            also mode_time will be set to now, if mode is set.

        cap_now : bool
            indicator whether times (fail_at, fail_delay) in the past are allowed. If, so now() will be used.
            default: sys.default_cap_now(), usualy False

        key : callable
            should be a function with one parameter (a component) and return a key value, to be used
            to compare components (most likely a number or string).

            The component with lowest key value (satisfying the filter condition) will be returned.

            If omitted, no sorting will be applied.

        Note
        ----
        Only allowed for current component

        Only if yieldless is False: Always use as
        use ``item = yield self.from_store(...)``.

        The parameter failed will be reset by a calling request, wait, from_store or to_store
        """

        if isinstance(store, Store):
            from_stores = [store]
        else:
            from_stores = list(store)
            if len(set(from_stores)) != len(from_stores):
                raise ValueError("one or more stores specified more than once")
            if len(from_stores) == 0:
                raise ValueError("no stores specified")

        if self.status.value != current:
            self._checkisnotdata()
            self._checkisnotmain()
            self._remove()
            self._check_fail()

        if fail_at is None:
            if fail_delay is None:
                scheduled_time = inf
            else:
                if fail_delay == inf:
                    scheduled_time = inf
                else:
                    fail_delay = self.env.spec_to_duration(fail_delay)
                    scheduled_time = self.env._now + fail_delay
        else:
            if fail_delay is None:
                fail_at = self.env.spec_to_time(fail_at)
                scheduled_time = fail_at + self.env._offset
            else:
                raise ValueError("both fail_at and fail_delay specified")

        self.set_mode(mode)

        self._failed = False

        if self.env._trace:
            self.env.print_trace("", "", self.name(), f"from_store ({', '.join(store._name for store in from_stores)})")

        found_c = None
        found_key = None
        done = False
        for store in from_stores:
            for c in store:
                if filter(c):
                    if key:
                        this_key = key(c)
                        if found_key is None or this_key < found_key:
                            found_c = c
                            found_key = this_key
                    else:
                        found_c = c
                        done = True
                        break
            if done:
                break
        if found_c:
            found_c.leave(store)
            self._from_store_item = found_c
            self._from_store_store = store
            self._remove()
            self.status._value = scheduled
            self._reschedule(
                self.env._now, 0, False, f"from_store ({store.name()}) honor with {found_c.name()}", False, s0=self.env.last_s0, return_value=found_c
            )
            return self._from_store_item

        self._from_stores = from_stores
        for store in from_stores:
            self.enter(store._from_store_requesters)
        self.status._value = requesting
        self._from_store_item = None
        self._from_store_filter = filter

        self._reschedule(scheduled_time, fail_priority, urgent, "request from_store", cap_now)
        if self.env._yieldless:
            return self._from_store_item

    def to_store(
        self,
        store: Union["Store", Iterable],
        item: "Component",
        priority: float = 0,
        fail_priority: float = 0,
        urgent: bool = True,
        fail_at: float = None,
        fail_delay: float = None,
        mode: str = None,
        cap_now: bool = None,
    ) -> "Component":
        """
        put item to store(s)

        Parameters
        ----------
        store : store or iterable stores
            store(s) to put item to

        item: Component
            component to put to store

        fail_priority : float
            priority of the fail event

            default: 0

            if a component has the same time on the event list, this component is sorted according to
            the priority.

        urgent : bool
            urgency indicator

            if False (default), the component will be scheduled
            behind all other components scheduled
            for the same time and priority

            if True, the component will be scheduled
            in front of all components scheduled
            for the same time and priority

        fail_at : float or distribution
            time out

            if the request is not honored before fail_at,
            the request will be cancelled and the
            parameter failed will be set.

            if not specified, the request will not time out.

            if distribution, the distribution is sampled

        fail_delay : float or distribution
            time out

            if the request is not honored before now+fail_delay,
            the request will be cancelled and the
            parameter failed will be set.

            if not specified, the request will not time out.

            if distribution, the distribution is sampled

        mode : str preferred
            mode

            will be used in trace and can be used in animations

            if nothing specified, the mode will be unchanged.

            also mode_time will be set to now, if mode is set.

        cap_now : bool
            indicator whether times (fail_at, fail_delay) in the past are allowed. If, so now() will be used.
            default: sys.default_cap_now(), usualy False

        Note
        ----
        Only allowed for current component

        Only if yieldless is False: Always use as
        use ``yield self.to_store(...)``.

        The parameter failed will be reset by a calling request, wait, from_store, to_store
        """
        if isinstance(store, Store):
            to_stores = [store]
        else:
            to_stores = list(store)
            if len(set(to_stores)) != len(to_stores):
                raise ValueError("one or more stores specified more than once")
            if len(to_stores) == 0:
                raise ValueError("no stores specified")

        if self.status.value != current:
            self._checkisnotdata()
            self._checkisnotmain()
            self._remove()
            self._check_fail()

        if fail_at is None:
            if fail_delay is None:
                scheduled_time = inf
            else:
                if fail_delay == inf:
                    scheduled_time = inf
                else:
                    fail_delay = self.env.spec_to_duration(fail_delay)
                    scheduled_time = self.env._now + fail_delay
        else:
            if fail_delay is None:
                fail_at = self.env.spec_to_time(fail_at)
                scheduled_time = fail_at + self.env._offset
            else:
                raise ValueError("both fail_at and fail_delay specified")

        self.set_mode(mode)

        self._failed = False

        if self.env._trace:
            self.env.print_trace("", "", self.name(), f"{item._name} to_store ({', '.join(store._name for store in to_stores)})")
        for store in to_stores:
            q = store
            if q.available_quantity() > 0:
                item.enter_sorted(q, priority)
                self._to_store_item = None
                self._to_store_store = store
                self._to_stores = []
                self._remove()
                self.status._value = scheduled
                self._reschedule(self.env._now, 0, False, f"to_store ({store.name()}) honor with {item.name()}", False, s0=self.env.last_s0)
                return

        for store in to_stores:
            self.enter(store._to_store_requesters)
        self.status._value = requesting
        self._to_store_item = item
        self._to_store_priority = priority
        self._to_stores = to_stores

        if self._to_store_item:
            self._reschedule(scheduled_time, fail_priority, urgent, "request to_store", cap_now)

    def filter(self, value: callable):
        """
        updates the filter used in self.from_to

        Parameters
        ----------
        value : callable
            new filter, which should be a function with one parameter(component) and returning a bool

        Note
        ----
        After applying the new filter, items (components) may leave or enter the store
        """
        self._from_store_filter = value
        for store in self._from_stores:
            store.rescan()

    def request(self, *args, **kwargs) -> None:
        """
        request from a resource or resources

        Parameters
        ----------
        args : sequence of items where each item can be:
            - resource, where quantity=1, priority=tail of requesters queue
            - tuples/list containing a resource, a quantity and optionally a priority.
                if the priority is not specified, the request
                for the resource be added to the tail of
                the requesters queue


        priority : float
            priority of the fail event

            default: 0

            if a component has the same time on the event list, this component is sorted according to
            the priority.

        urgent : bool
            urgency indicator

            if False (default), the component will be scheduled
            behind all other components scheduled
            for the same time and priority

            if True, the component will be scheduled
            in front of all components scheduled
            for the same time and priority

        fail_at : float or distribution
            time out

            if the request is not honored before fail_at,
            the request will be cancelled and the
            parameter failed will be set.

            if not specified, the request will not time out.

            if distribution, the distribution is sampled

        fail_delay : float or distribution
            time out

            if the request is not honored before now+fail_delay,
            the request will be cancelled and the
            parameter failed will be set.

            if not specified, the request will not time out.

            if distribution, the distribution is sampled

        oneof : bool
            if oneof is True, just one of the requests has to be met (or condition),
            where honoring follows the order given.

            if oneof is False (default), all requests have to be met to be honored

        mode : str preferred
            mode

            will be used in trace and can be used in animations

            if nothing specified, the mode will be unchanged.

            also mode_time will be set to now, if mode is set.

        cap_now : bool
            indicator whether times (fail_at, fail_delay) in the past are allowed. If, so now() will be used.
            default: sys.default_cap_now(), usualy False

        Note
        ----
        Not allowed for data components or main.

        Only if yieldless is False: If to be used for the current component
        (which will be nearly always the case),
        use ``yield self.request(...)``.

        If the same resource is specified more that once, the quantities are summed


        The requested quantity may exceed the current capacity of a resource


        The parameter failed will be reset by a calling request or wait

        Example
        -------
        For yieldless, refrain from yield !

        ``yield self.request(r1)``

        --> requests 1 from r1

        ``yield self.request(r1,r2)``

        --> requests 1 from r1 and 1 from r2

        ``yield self.request(r1,(r2,2),(r3,3,100))``

        --> requests 1 from r1, 2 from r2 and 3 from r3 with priority 100

        ``yield self.request((r1,1),(r2,2))``

        --> requests 1 from r1, 2 from r2

        ``yield self.request(r1, r2, r3, oneoff=True)``

        --> requests 1 from r1, r2 or r3

        """
        fail_at = kwargs.pop("fail_at", None)
        fail_delay = kwargs.pop("fail_delay", None)
        mode = kwargs.pop("mode", None)
        urgent = kwargs.pop("urgent", False)
        schedule_priority = kwargs.pop("priority", 0)
        cap_now = kwargs.pop("cap_now", None)
        oneof = kwargs.pop("oneof", False)
        called_from = kwargs.pop("called_from", "request")
        self.oneof_request = oneof
        if kwargs:
            raise TypeError(called_from + "() got an unexpected keyword argument '" + tuple(kwargs)[0] + "'")

        if self.status.value != current:
            self._checkisnotdata()
            self._checkisnotmain()
            self._remove()
            self._check_fail()
        if fail_at is None:
            if fail_delay is None:
                scheduled_time = inf
            else:
                if fail_delay == inf:
                    scheduled_time = inf
                else:
                    fail_delay = self.env.spec_to_duration(fail_delay)
                    scheduled_time = self.env._now + fail_delay
        else:
            if fail_delay is None:
                fail_at = self.env.spec_to_time(fail_at)
                scheduled_time = fail_at + self.env._offset
            else:
                raise ValueError("both fail_at and fail_delay specified")

        self.set_mode(mode)

        self._failed = False

        if not args:
            honoredstr = "-"
            self._reschedule(self.env._now, 0, False, "request honor " + honoredstr, False, s0=self.env.last_s0)
            return
        for arg in args:
            q = 1
            priority = inf
            if isinstance(arg, Resource):
                r = arg
            elif isinstance(arg, (tuple, list)):
                r = arg[0]
                if len(arg) >= 2:
                    q = arg[1]
                if len(arg) >= 3:
                    priority = arg[2]
            else:
                raise TypeError("incorrect specifier", arg)

            if r._preemptive:
                if len(args) > 1:
                    raise ValueError("preemptive resources do not support multiple resource requests")

            if called_from == "put":
                q = -q

            if q < 0 and not r._anonymous:
                raise ValueError("quantity " + str(q) + " <0")
            if r in self._requests:
                self._requests[r] += q  # is same resource is specified several times, just add them up
            else:
                self._requests[r] = q
            if called_from == "request":
                req_text = "request " + str(q) + " from "
            elif called_from == "put":
                req_text = "put (request) " + str(-q) + " to "
            elif called_from == "get":
                req_text = "get (request) " + str(q) + " from "

            addstring = ""
            addstring += " priority=" + str(priority)

            if self.oneof_request:
                addstring += " (oneof)"

            self.enter_sorted(r._requesters, priority)
            if self.env._trace:
                self.env.print_trace("", "", self.name(), req_text + r.name() + addstring)

            if r._preemptive:
                av = r.available_quantity()
                this_claimers = r.claimers()
                bump_candidates = []
                for c in reversed(r.claimers()):
                    if av >= q:
                        break
                    if priority >= c.priority(this_claimers):
                        break
                    av += c.claimed_quantity(this_claimers)
                    bump_candidates.append(c)
                if av >= 0:
                    for c in bump_candidates:
                        c._release(r, bumped_by=self)
                        c.activate()
        for r, q in self._requests.items():
            if q < r._minq:
                r._minq = q

        self._remaining_duration = scheduled_time - self.env._now

        self._tryrequest()

        if self._requests:
            self.status._value = requesting
            self._reschedule(scheduled_time, schedule_priority, urgent, "request", cap_now)

    def isbumped(self, resource: "Resource" = None) -> bool:
        """
        check whether component is bumped from resource

        Parameters
        ----------
        resource : Resource
            resource to be checked
            if omitted, checks whether component belongs to any resource claimers

        Returns
        -------
        True if this component is not in the resource claimers : bool
            False otherwise
        """
        return not self.isclaiming(resource)

    def isclaiming(self, resource: "Resource" = None) -> bool:
        """
        check whether component is claiming from resource

        Parameters
        ----------
        resource : Resource
            resource to be checked
            if omitted, checks whether component is in any resource claimers

        Returns
        -------
        True if this component is in the resource claimers : bool
            False otherwise
        """
        if resource is None:
            for q in self._qmembers:
                if hasattr(q, "_isclaimers"):
                    return True
            return False
        else:
            return self in resource.claimers()

    def get(self, *args, **kwargs) -> None:
        """
        equivalent to request
        """
        return self.request(*args, called_from="get", **kwargs)

    def put(self, *args, **kwargs) -> None:
        """
        equivalent to request, but anonymous quantities are negated
        """
        return self.request(*args, called_from="put", **kwargs)

    def honor_all(self):
        for r in self._requests:
            if r._honor_only_first and r._requesters[0] != self:
                return []
            self_prio = self.priority(r._requesters)
            if r._honor_only_highest_priority and self_prio != r._requesters._head.successor.priority:
                return []
            if self._requests[r] > 0:
                if self._requests[r] > (r._capacity - r._claimed_quantity + 1e-8):
                    return []
            else:
                if -self._requests[r] > r._claimed_quantity + 1e-8:
                    return []
        return list(self._requests.keys())

    def honor_any(self):
        for r in self._requests:
            if r._honor_only_first and r._requesters[0] != self:
                continue
            self_prio = self.priority(r._requesters)
            if r._honor_only_highest_priority and self_prio != r._requesters._head.successor.priority:
                continue

            if self._requests[r] > 0:
                if self._requests[r] <= (r._capacity - r._claimed_quantity + 1e-8):
                    return [r]
            else:
                if -self._requests[r] <= r._claimed_quantity + 1e-8:
                    return [r]
        return []

    def _tryrequest(self):
        # this is Component._tryrequest
        if self.status.value == interrupted:
            return False

        if self.oneof_request:
            r_honor = self.honor_any()
        else:
            r_honor = self.honor_all()

        if r_honor:
            anonymous_resources = []
            for r in list(self._requests):
                if r._anonymous:
                    anonymous_resources.append(r)
                if r in r_honor:
                    r._claimed_quantity += self._requests[r]
                    this_prio = self.priority(r._requesters)
                    if r._anonymous:
                        prio_trace = ""
                    else:
                        if r in self._claims:
                            self._claims[r] += self._requests[r]
                        else:
                            self._claims[r] = self._requests[r]
                        mx = self._member(r._claimers)
                        if mx is None:
                            self.enter_sorted(r._claimers, this_prio)
                        prio_trace = " priority=" + str(this_prio)
                    r.claimed_quantity.tally(r._claimed_quantity)
                    r.occupancy.tally(0 if r._capacity <= 0 else r._claimed_quantity / r._capacity)
                    r.available_quantity.tally(r._capacity - r._claimed_quantity)
                    if self.env._trace:
                        self.env.print_trace("", "", self.name(), f"claim {self._requests[r]} from {r.name()} {prio_trace}")
                self.leave(r._requesters)
                if r._requesters._length == 0:
                    r._minq = inf

            self._requests = collections.OrderedDict()
            self._remove()
            honoredstr = r_honor[0].name() + (len(r_honor) > 1) * " ++"
            self.status._value = scheduled
            self._reschedule(self.env._now, 0, False, "request honor " + honoredstr, False, s0=self.env.last_s0)
            for r in anonymous_resources:
                r._tryrequest()
            return True
        else:
            return False

    def _release(self, r, q=None, s0=None, bumped_by=None):
        if r not in self._claims:
            raise ValueError(self.name() + " not claiming from resource " + r.name())
        if q is None:
            q = self._claims[r]
        if q > self._claims[r]:
            q = self._claims[r]
        r._claimed_quantity -= q
        self._claims[r] -= q
        if self._claims[r] < 1e-8:
            self.leave(r._claimers)
            if r._claimers._length == 0:
                r._claimed_quantity = 0  # to avoid rounding problems
            del self._claims[r]
        r.claimed_quantity.tally(r._claimed_quantity)
        r.occupancy.tally(0 if r._capacity <= 0 else r._claimed_quantity / r._capacity)
        r.available_quantity.tally(r._capacity - r._claimed_quantity)
        extra = " bumped by " + bumped_by.name() if bumped_by else ""
        if self.env._trace:
            if bumped_by:
                self.env.print_trace("", "", self.name(), "bumped from " + r.name() + " by " + bumped_by.name() + " (release " + str(q) + ")", s0=s0)
            else:
                self.env.print_trace("", "", self.name(), "release " + str(q) + " from " + r.name() + extra, s0=s0)
        if not bumped_by:
            r._tryrequest()

    def release(self, *args) -> None:
        """
        release a quantity from a resource or resources

        Parameters
        ----------
        args : sequence of items, where each items can be
            - a resource, where quantity=current claimed quantity
            - a tuple/list containing a resource and the quantity to be released

        Note
        ----
        It is not possible to release from an anonymous resource, this way.
        Use Resource.release() in that case.

        Example
        -------
        If yieldless, refrain from yield,

        yield self.request(r1,(r2,2),(r3,3,100))

        --> requests 1 from r1, 2 from r2 and 3 from r3 with priority 100


        c1.release

        --> releases 1 from r1, 2 from r2 and 3 from r3


        yield self.request(r1,(r2,2),(r3,3,100))

        c1.release((r2,1))

        --> releases 1 from r2


        yield self.request(r1,(r2,2),(r3,3,100))

        c1.release((r2,1),r3)

        --> releases 2 from r2,and 3 from r3
        """
        if args:
            for arg in args:
                q = None
                if isinstance(arg, Resource):
                    r = arg
                elif isinstance(arg, (tuple, list)):
                    r = arg[0]
                    if len(arg) >= 2:
                        q = arg[1]
                else:
                    raise TypeError("incorrect specifier" + arg)
                if r._anonymous:
                    raise ValueError("not possible to release anonymous resources " + r.name())
                self._release(r, q)
        else:
            for r in list(self._claims):
                self._release(r)

    def wait_for(self,cond, states, priority=0, urgent=False, mode=None,fail_delay=None, fail_at=None, cap_now=None):
        schedule_priority = priority
        """
        wait for any or all of the given state values are met

        Parameters
        ----------
        cond : callable
            parameterless function that return True if wait is over

        states : iterable
            specicies which states should trigger the cond to be checked

        priority : float
            priority of the fail event

            default: 0

            if a component has the same time on the event list, this component is sorted accoring to
            the priority.

        urgent : bool
            urgency indicator

            if False (default), the component will be scheduled
            behind all other components scheduled
            for the same time and priority

            if True, the component will be scheduled
            in front of all components scheduled
            for the same time and priority

        fail_at : float or distribution
            time out

            if the wait is not honored before fail_at,
            the wait will be cancelled and the
            parameter failed will be set.

            if not specified, the wait will not time out.

            if distribution, the distribution is sampled

        fail_delay : float or distribution
            time out

            if the wait is not honored before now+fail_delay,
            the request will be cancelled and the
            parameter failed will be set.

            if not specified, the wait will not time out.

            if distribution, the distribution is sampled

        mode : str preferred
            mode

            will be used in trace and can be used in animations

            if nothing specified, the mode will be unchanged.

            also mode_time will be set to now, if mode is set.

        cap_now : bool
            indicator whether times (fail_at, fail_duration) in the past are allowed. If, so now() will be used.
            default: sys.default_cap_now(), usualy False

        Note
        ----
        Not allowed for data components or main.

        Only if yieldless is False: If to be used for the current component
        (which will be nearly always the case),
        use ``yield self.wait(...)``.

        """
        if self.status.value != current:
            self._checkisnotdata()
            self._checkisnotmain()
            self._remove()
            self._check_fail()

        self._failed = False

        if fail_at is None:
            if fail_delay is None:
                scheduled_time = inf
            else:
                if fail_delay == inf:
                    scheduled_time = inf
                else:
                    fail_delay = self.env.spec_to_duration(fail_delay)
                    scheduled_time = self.env._now + fail_delay
        else:
            if fail_delay is None:
                fail_at = self.env.spec_to_time(fail_at)
                scheduled_time = fail_at + self.env._offset
            else:
                raise ValueError("both fail_at and fail_delay specified")

        self.set_mode(mode)

        self._cond=cond  # add test ***
        for state in states:
            self._waits.append((state, None,None))
            if priority is None:
                self.enter(state._waiters)
            else:
                self.enter_sorted(state._waiters, priority)

        if not self._waits:
            raise TypeError("no states specified")

        self._remaining_duration = scheduled_time - self.env._now

        self._trywait()

        if self._waits:
            self.status._value = waiting
            self._reschedule(scheduled_time, schedule_priority, urgent, "wait_for", cap_now)
        else:
            return



    def wait(self, *args, **kwargs) -> None:
        """
        wait for any or all of the given state values are met

        Parameters
        ----------
        args : sequence of items, where each item can be
            - a state, where value=True, priority=tail of waiters queue)
            - a tuple/list containing

                state, a value and optionally a priority.

                if the priority is not specified, this component will
                be added to the tail of
                the waiters queue


        priority : float
            priority of the fail event

            default: 0

            if a component has the same time on the event list, this component is sorted accoring to
            the priority.

        urgent : bool
            urgency indicator

            if False (default), the component will be scheduled
            behind all other components scheduled
            for the same time and priority

            if True, the component will be scheduled
            in front of all components scheduled
            for the same time and priority

        fail_at : float or distribution
            time out

            if the wait is not honored before fail_at,
            the wait will be cancelled and the
            parameter failed will be set.

            if not specified, the wait will not time out.

            if distribution, the distribution is sampled

        fail_delay : float or distribution
            time out

            if the wait is not honored before now+fail_delay,
            the request will be cancelled and the
            parameter failed will be set.

            if not specified, the wait will not time out.

            if distribution, the distribution is sampled

        all : bool
            if False (default), continue, if any of the given state/values is met

            if True, continue if all of the given state/values are met

        mode : str preferred
            mode

            will be used in trace and can be used in animations

            if nothing specified, the mode will be unchanged.

            also mode_time will be set to now, if mode is set.

        cap_now : bool
            indicator whether times (fail_at, fail_duration) in the past are allowed. If, so now() will be used.
            default: sys.default_cap_now(), usualy False

        Note
        ----
        Not allowed for data components or main.

        Only if yieldless is False: If to be used for the current component
        (which will be nearly always the case),
        use ``yield self.wait(...)``.

        It is allowed to wait for more than one value of a state

        the parameter failed will be reset by a calling wait

        If you want to check for all components to meet a value (and clause),
        use Component.wait(..., all=True)

        The value may be specified in three different ways:

        * constant, that value is just compared to state.value()

          yield self.wait((light,"red"))
        * an expression, containg one or more $-signs
          the $ is replaced by state.value(), each time the condition is tested.

          self refers to the component under test, state refers to the state
          under test.

          yield self.wait((light,'$ in ("red","yellow")'))

          yield self.wait((level,"$<30"))

        * a function. In that case the parameter should function that
          should accept three arguments: the value, the component under test and the
          state under test.

          usually the function will be a lambda function, but that's not
          a requirement.

          yield self.wait((light,lambda t, comp, state: t in ("red","yellow")))

          yield self.wait((level,lambda t, comp, state: t < 30))


        Example
        -------
        If yieldless, refrain from yield.

        ``yield self.wait(s1)``

        --> waits for s1.value()==True

        ``yield self.wait(s1,s2)``

        --> waits for s1.value()==True or s2.value==True

        ``yield self.wait((s1,False,100),(s2,"on"),s3)``

        --> waits for s1.value()==False or s2.value=="on" or s3.value()==True

        s1 is at the tail of waiters, because of the set priority

        ``yield self.wait(s1,s2,all=True)``

        --> waits for s1.value()==True and s2.value==True

        """
        fail_at = kwargs.pop("fail_at", None)
        fail_delay = kwargs.pop("fail_delay", None)
        all = kwargs.pop("all", False)
        mode = kwargs.pop("mode", None)
        urgent = kwargs.pop("urgent", False)
        schedule_priority = kwargs.pop("priority", 0)
        cap_now = kwargs.pop("cap_now", None)

        self._cond=None

        if kwargs:
            raise TypeError("wait() got an unexpected keyword argument '" + tuple(kwargs)[0] + "'")

        if self.status.value != current:
            self._checkisnotdata()
            self._checkisnotmain()
            self._remove()
            self._check_fail()

        self._wait_all = all
        self._failed = False

        if fail_at is None:
            if fail_delay is None:
                scheduled_time = inf
            else:
                if fail_delay == inf:
                    scheduled_time = inf
                else:
                    fail_delay = self.env.spec_to_duration(fail_delay)
                    scheduled_time = self.env._now + fail_delay
        else:
            if fail_delay is None:
                fail_at = self.env.spec_to_time(fail_at)
                scheduled_time = fail_at + self.env._offset
            else:
                raise ValueError("both fail_at and fail_delay specified")

        self.set_mode(mode)

        for arg in args:
            value = True
            priority = None
            if isinstance(arg, State):
                state = arg
            elif isinstance(arg, (tuple, list)):
                state = arg[0]
                if not isinstance(state, State):
                    raise TypeError("incorrect specifier", arg)
                if len(arg) >= 2:
                    value = arg[1]
                if len(arg) >= 3:
                    priority = arg[2]
                if len(arg) >= 4:
                    raise TypeError("incorrect specifier", arg)
            else:
                raise TypeError("incorrect specifier", arg)

            for statex, _, _ in self._waits:
                if statex == state:
                    break
            else:
                if priority is None:
                    self.enter(state._waiters)
                else:
                    self.enter_sorted(state._waiters, priority)
            if inspect.isfunction(value):
                self._waits.append((state, value, 2))
            elif "$" in str(value):
                self._waits.append((state, value, 1))
            else:
                self._waits.append((state, value, 0))

        if not self._waits:
            raise TypeError("no states specified")

        self._remaining_duration = scheduled_time - self.env._now

        self._trywait()

        if self._waits:
            self.status._value = waiting
            self._reschedule(scheduled_time, schedule_priority, urgent, "wait", cap_now)
        else:
            return

    def _trywait(self):

        if self.status.value == interrupted:
            return False
        if self._cond:
            honored = self._cond()
        else:
            if self._wait_all:
                honored = True
                for state, value, valuetype in self._waits:
                    if valuetype == 0:
                        if value != state._value:
                            honored = False
                            break
                    elif valuetype == 1:
                        if not eval(value.replace("$", "state._value")):
                            honored = False
                            break
                    elif valuetype == 2:
                        if not value(state._value, self, state):
                            honored = False
                            break

            else:
                honored = False
                for state, value, valuetype in self._waits:
                    if valuetype == 0:
                        if value == state._value:
                            honored = True
                            break
                    elif valuetype == 1:
                        if eval(value.replace("$", str(state._value))):
                            honored = True
                            break
                    elif valuetype == 2:
                        if value(state._value, self, state):
                            honored = True
                            break

        if honored:
            for s, _, _ in self._waits:
                if self in s._waiters:  # there might be more values for this state
                    self.leave(s._waiters)
            self._waits = []
            self._remove()
            self.status._value = scheduled
            self._reschedule(self.env._now, 0, False, "wait honor", False, s0=self.env.last_s0)

        return honored

    def claimed_quantity(self, resource: "Resource" = None) -> float:
        """
        Parameters
        ----------
        resource : Resoure
            resource to be queried

        Returns
        -------
        the claimed quantity from a resource : float or int
            if the resource is not claimed, 0 will be returned
        """
        return self._claims.get(resource, 0)

    def claimed_resources(self) -> List:
        """
        Returns
        -------
        list of claimed resources : list
        """
        return list(self._claims)

    def requested_resources(self) -> List:
        """
        Returns
        -------
        list of requested resources : list
        """
        return list(self._requests)

    def requested_quantity(self, resource: "Resource" = None) -> float:
        """
        Parameters
        ----------
        resource : Resoure
            resource to be queried

        Returns
        -------
        the requested (not yet honored) quantity from a resource : float or int
            if there is no request for the resource, 0 will be returned
        """
        return self._requests.get(resource, 0)

    def failed(self) -> bool:
        """
        Returns
        -------
        True, if the latest request/wait has failed (either by timeout or external) : bool
        False, otherwise
        """
        return self._failed

    def name(self, value: str = None) -> str:
        """
        Parameters
        ----------
        value : str
            new name of the component
            if omitted, no change

        Returns
        -------
        Name of the component : str

        Note
        ----
        base_name and sequence_number are not affected if the name is changed
        """
        if value is not None:
            self._name = value
        return self._name

    def base_name(self) -> str:
        """
        Returns
        -------
        base name of the component (the name used at initialization): str
        """
        return getattr(self, "_base_name", self._name)

    def sequence_number(self) -> int:
        """
        Returns
        -------
        sequence_number of the component : int
            (the sequence number at initialization)

            normally this will be the integer value of a serialized name.

            Non serialized names (without a dot or a comma at the end)
            will return 1)
        """
        return getattr(self, "_sequence_number", 1)

    def running_process(self) -> str:
        """
        Returns
        -------
        name of the running process : str
            if data component, None
        """
        if self._process is None:
            return None
        else:
            return self._process.__name__

    def remove_animation_children(self) -> None:
        """
        removes animation children

        Note
        ----
        Normally, the animation_children are removed automatically upon termination of a component (when it terminates)
        """
        for ao in self._animation_children:
            ao.remove()
        self._animation_children = set()

    def suppress_trace(self, value: bool = None) -> bool:
        """
        Parameters
        ----------
        value: bool
            new suppress_trace value

            if omitted, no change

        Returns
        -------
        suppress_trace : bool
            components with the suppress_status of True, will be ignored in the trace
        """
        if value is not None:
            self._suppress_trace = value
        return self._suppress_trace

    def suppress_pause_at_step(self, value: bool = None) -> bool:
        """
        Parameters
        ----------
        value: bool
            new suppress_trace value

            if omitted, no change

        Returns
        -------
        suppress_pause_at_step : bool
            components with the suppress_pause_at_step of True, will be ignored in a step
        """
        if value is not None:
            self._suppress_pause_at_step = value
        return self._suppress_pause_at_step

    def skip_standby(self, value: bool = None) -> bool:
        """
        Parameters
        ----------
        value: bool
            new skip_standby value

            if omitted, no change

        Returns
        -------
        skip_standby indicator : bool
            components with the skip_standby indicator of True, will not activate standby components after
            the component became current.
        """
        if value is not None:
            self._skip_standby = value
        return self._skip_standby

    def set_mode(self, value: str = None) -> None:
        """
        Parameters
        ----------
        value: any, str recommended
            new mode

            mode_time will be set to now
            if omitted, no change
        """
        if value is not None:
            self._mode_time = self.env._now
            self.mode.tally(value)

    def _modetxt(self) -> str:
        if self.mode() == "":
            return ""
        else:
            return "mode=" + str(self.mode())

    def ispassive(self) -> bool:
        """
        Returns
        -------
        True if status is passive, False otherwise : bool

        Note
        ----
        Be sure to always include the parentheses, otherwise the result will be always True!
        """
        return self.status.value == passive

    def iscurrent(self) -> bool:
        """
        Returns
        -------
        True if status is current, False otherwise : bool

        Note
        ----
        Be sure to always include the parentheses, otherwise the result will be always True!
        """
        return self.status.value == current

    def isrequesting(self):
        """
        Returns
        -------
        True if status is requesting, False otherwise : bool

        Note
        ----
        Be sure to always include the parentheses, otherwise the result will be always True!
        """
        return self.status.value == requesting

    def iswaiting(self) -> bool:
        """
        Returns
        -------
        True if status is waiting, False otherwise : bool

        Note
        ----
        Be sure to always include the parentheses, otherwise the result will be always True!
        """
        return self.status.value == waiting

    def isscheduled(self) -> bool:
        """
        Returns
        -------
        True if status is scheduled, False otherwise : bool

        Note
        ----
        Be sure to always include the parentheses, otherwise the result will be always True!
        """
        return self.status.value == scheduled

    def isstandby(self) -> bool:
        """
        Returns
        -------
        True if status is standby, False otherwise : bool

        Note
        ----
        Be sure to always include the parentheses, otherwise the result will be always True
        """
        return self.status.value == standby

    def isinterrupted(self) -> bool:
        """
        Returns
        -------
        True if status is interrupted, False otherwise : bool

        Note
        ----
        Be sure to always include the parentheses, otherwise the result will be always True
        """
        return self.status.value == interrupted

    def isdata(self) -> bool:
        """
        Returns
        -------
        True if status is data, False otherwise : bool

        Note
        ----
        Be sure to always include the parentheses, otherwise the result will be always True!
        """
        return self.status.value == data

    def queues(self) -> Set:
        """
        Returns
        -------
        set of queues where the component belongs to : set
        """
        return set(self._qmembers)

    def count(self, q: "Queue" = None) -> int:
        """
        queue count

        Parameters
        ----------
        q : Queue
            queue to check or

            if omitted, the number of queues where the component is in

        Returns
        -------
        1 if component is in q, 0 otherwise : int


            if q is omitted, the number of queues where the component is in
        """
        if q is None:
            return len(self._qmembers)
        else:
            return 1 if self in q else 0

    def index(self, q: "Queue") -> int:
        """
        Parameters
        ----------
        q : Queue
            queue to be queried

        Returns
        -------
        index of component in q : int
            if component belongs to q

            -1 if component does not belong to q
        """
        m1 = self._member(q)
        if m1 is None:
            return -1
        else:
            mx = q._head.successor
            index = 0
            while mx != m1:
                mx = mx.successor
                index += 1
            return index

    def enter(self, q: "Queue") -> "Component":
        """
        enters a queue at the tail

        Parameters
        ----------
        q : Queue
            queue to enter

        Note
        ----
        the priority will be set to
        the priority of the tail component of the queue, if any
        or 0 if queue is empty
        """
        self._checknotinqueue(q)
        priority = q._tail.predecessor.priority
        Qmember().insert_in_front_of(q._tail, self, q, priority)
        return self

    def enter_at_head(self, q: "Queue") -> "Component":
        """
        enters a queue at the head

        Parameters
        ----------
        q : Queue
            queue to enter

        Note
        ----
        the priority will be set to
        the priority of the head component of the queue, if any
        or 0 if queue is empty
        """

        self._checknotinqueue(q)
        priority = q._head.successor.priority
        Qmember().insert_in_front_of(q._head.successor, self, q, priority)
        return self

    def enter_in_front_of(self, q: "Queue", poscomponent: "Component") -> "Component":
        """
        enters a queue in front of a component

        Parameters
        ----------
        q : Queue
            queue to enter

        poscomponent : Component
            component to be entered in front of

        Note
        ----
        the priority will be set to the priority of poscomponent
        """

        self._checknotinqueue(q)
        m2 = poscomponent._checkinqueue(q)
        priority = m2.priority
        Qmember().insert_in_front_of(m2, self, q, priority)
        return self

    def enter_behind(self, q: "Queue", poscomponent: "Component") -> "Component":
        """
        enters a queue behind a component

        Parameters
        ----------
        q : Queue
            queue to enter

        poscomponent : Component
            component to be entered behind

        Note
        ----
        the priority will be set to the priority of poscomponent
        """

        self._checknotinqueue(q)
        m1 = poscomponent._checkinqueue(q)
        priority = m1.priority
        Qmember().insert_in_front_of(m1.successor, self, q, priority)
        return self

    def enter_sorted(self, q: "Queue", priority: float) -> "Component":
        """
        enters a queue, according to the priority

        Parameters
        ----------
        q : Queue
            queue to enter

        priority: type that can be compared with other priorities in the queue
            priority in the queue

        Note
        ----
        The component is placed just before the first component with a priority > given priority
        """
        self._checknotinqueue(q)
        if q._length >= 1 and priority < q._head.successor.priority:  # direct enter component that's smaller than the rest
            m2 = q._head.successor
        else:
            m2 = q._tail
            while (m2.predecessor != q._head) and (m2.predecessor.priority > priority):
                m2 = m2.predecessor
        Qmember().insert_in_front_of(m2, self, q, priority)
        return self

    def leave(self, q: "Queue" = None) -> "Component":
        """
        leave queue

        Parameters
        ----------
        q : Queue
            queue to leave

        Note
        ----
        statistics are updated accordingly
        """
        if q is None:
            for q in list(self._qmembers):
                if not q._isinternal:
                    self.leave(q)
            return self

        mx = self._checkinqueue(q)
        m1 = mx.predecessor
        m2 = mx.successor
        m1.successor = m2
        m2.predecessor = m1
        mx.component = None
        # signal for components method that member is not in the queue
        q._length -= 1
        del self._qmembers[q]
        if self.env._trace:
            if not q._isinternal:
                self.env.print_trace("", "", self.name(), "leave " + q.name())
        length_of_stay = self.env._now - mx.enter_time
        q.length_of_stay.tally(length_of_stay)
        q.length.tally(q._length)
        q.available_quantity.tally(q.capacity._tally - q._length)
        q.number_of_departures += 1

        if isinstance(q, Store):
            store = q
            available_quantity = q.capacity._tally - q._length
            if available_quantity > 0:
                if len(store._to_store_requesters) > 0:
                    requester = store._to_store_requesters[0]
                    with self.env.suppress_trace():
                        requester._to_store_item.enter_sorted(q, requester._to_store_priority)
                    for store0 in requester._to_stores:
                        requester.leave(store0._to_store_requesters)
                    requester._to_stores = []
                    requester._remove()
                    requester.status._value = scheduled
                    requester._reschedule(requester.env._now, 0, False, f"to_store ({store.name()}) honor ", False, s0=requester.env.last_s0)
                    requester._to_store_item = None
                    requester._to_store_store = store
        return self

    def priority(self, q: "Queue", priority: float = None) -> float:
        """
        gets/sets the priority of a component in a queue

        Parameters
        ----------
        q : Queue
            queue where the component belongs to

        priority : type that can be compared with other priorities in the queue
            priority in queue

            if omitted, no change

        Returns
        -------
        the priority of the component in the queue : float

        Note
        ----
        if you change the priority, the order of the queue may change
        """

        mx = self._checkinqueue(q)
        if priority is not None:
            if priority != mx.priority:
                # leave.sort is not possible, because statistics will be affected
                mx.predecessor.successor = mx.successor
                mx.successor.predecessor = mx.predecessor

                m2 = q._head.successor
                while (m2 != q._tail) and (m2.priority <= priority):
                    m2 = m2.successor

                m1 = m2.predecessor
                m1.successor = mx
                m2.predecessor = mx
                mx.predecessor = m1
                mx.successor = m2
                mx.priority = priority
                for iter in q._iter_touched:
                    q._iter_touched[iter] = True
        return mx.priority

    def successor(self, q: "Queue") -> "Component":
        """
        Parameters
        ----------
        q : Queue
            queue where the component belongs to

        Returns
        -------
        the successor of the component in the queue: Component
            if component is not at the tail.

            returns None if component is at the tail.
        """

        mx = self._checkinqueue(q)
        return mx.successor.component

    def predecessor(self, q: "Queue") -> "Component":
        """
        Parameters
        ----------
        q : Queue
            queue where the component belongs to

        Returns : Component
            predecessor of the component in the queue
            if component is not at the head.

            returns None if component is at the head.
        """

        mx = self._checkinqueue(q)
        return mx.predecessor.component

    def enter_time(self, q: "Queue") -> float:
        """
        Parameters
        ----------
        q : Queue
            queue where component belongs to

        Returns
        -------
        time the component entered the queue : float
        """
        mx = self._checkinqueue(q)
        return mx.enter_time - self.env._offset

    def creation_time(self) -> float:
        """
        Returns
        -------
        time the component was created : float
        """
        return self._creation_time - self.env._offset

    def scheduled_time(self) -> float:
        """
        Returns
        -------
        time the component scheduled for, if it is scheduled : float
            returns inf otherwise
        """
        return self._scheduled_time - self.env._offset

    def scheduled_priority(self) -> float:
        """
        Returns
        -------
        priority the component is scheduled with : float
            returns None otherwise

        Note
        ----
        The method has to traverse the event list, so performance may be an issue.
        """
        for t, priority, seq, component, return_value in self.env._event_list:
            if component is self:
                return priority
        return None

    def remaining_duration(self, value: float = None, priority: float = 0, urgent: bool = False) -> float:
        """
        Parameters
        ----------
        value : float
            set the remaining_duration

            The action depends on the status where the component is in:

            - passive: the remaining duration is update according to the given value

            - standby and current: not allowed

            - scheduled: the component is rescheduled according to the given value

            - waiting or requesting: the fail_at is set according to the given value

            - interrupted: the remaining_duration is updated according to the given value


        priority : float
            priority

            default: 0

            if a component has the same time on the event list, this component is sorted accoring to
            the priority.

        urgent : bool
            urgency indicator

            if False (default), the component will be scheduled
            behind all other components scheduled
            for the same time and priority

            if True, the component will be scheduled
            in front of all components scheduled
            for the same time and priority

        Returns
        -------
        remaining duration : float
            if passive, remaining time at time of passivate

            if scheduled, remaing time till scheduled time

            if requesting or waiting, time till fail_at time

            else: 0

        Note
        ----
        This method is useful for interrupting a process and then resuming it,
        after some (breakdown) time
        """
        if value is not None:
            if self.status.value in (passive, interrupted):
                self._remaining_duration = value
            elif self.status.value == current:
                raise ValueError("setting remaining_duration not allowed for current component (" + self.name() + ")")
            elif self.status.value == standby:
                raise ValueError("setting remaining_duration not allowed for standby component (" + self.name() + ")")
            else:
                self._remove()
                self._reschedule(value + self.env._now, priority, urgent, "set remaining_duration", False, extra="")

        if self.status.value in (passive, interrupted):
            return self._remaining_duration
        elif self.status.value in (scheduled, waiting, requesting):
            return self._scheduled_time - self.env._now
        else:
            return 0

    def mode_time(self) -> float:
        """
        Returns
        -------
        time the component got it's latest mode : float
            For a new component this is
            the time the component was created.

            this function is particularly useful for animations.
        """
        return self._mode_time - self.env._offset

    def interrupted_status(self) -> Any:
        """
        returns the original status of an interrupted component

        possible values are
            - passive
            - scheduled
            - requesting
            - waiting
            - standby
        """
        if self.status.value != interrupted:
            raise ValueError(self.name() + "not interrupted")

        return self._interrupted_status

    def interrupt_level(self) -> int:
        """
        returns interrupt level of an interrupted component

        non interrupted components return 0
        """
        if self.status.value == interrupted:
            return self._interrupt_level
        else:
            return 0

    def _member(self, q):
        return self._qmembers.get(q, None)

    def _checknotinqueue(self, q):
        mx = self._member(q)
        if mx is None:
            pass
        else:
            raise ValueError(self.name() + " is already member of " + q.name())

    def _checkinqueue(self, q):
        mx = self._member(q)
        if mx is None:
            raise ValueError(self.name() + " is not member of " + q.name())
        else:
            return mx

    def _checkisnotdata(self):
        if self.status.value == data:
            raise ValueError(self.name() + " data component not allowed")

    def _checkisnotmain(self):
        if self == self.env._main:
            raise ValueError(self.name() + " main component not allowed")

    def lineno_txt(self, add_at=False):
        if self.env._suppress_trace_linenumbers:
            return ""
        if self.overridden_lineno:
            return ""

        plus = "+"
        if self.env._yieldless:
            if self.isdata():
                return ""
            if self._process is None:
                s0 = ""
                frame = _get_caller_frame()
                lineno = inspect.getframeinfo(frame).lineno
                s0 = rpad(str(lineno) + "+", 6)

            else:
                frame = self._glet.gr_frame
                if frame is None:
                    if not bool(self._glet):
                        lineno = self._process.__code__.co_firstlineno
                    else:
                        frame = _get_caller_frame()
                        lineno = inspect.getframeinfo(frame).lineno
                else:
                    frame_last = frame
                    i = 20  # prevent infinite loop
                    while frame.f_back is not None and i:
                        frame_last = frame
                        frame = frame.f_back
                        i -= 1
                    if inspect.getframeinfo(frame).filename == __file__:  # one up if we landed in salabim.py
                        frame = frame_last
                    lineno = inspect.getframeinfo(frame).lineno
                s0 = self.env.filename_lineno_to_str(self._process.__code__.co_filename, lineno) + "+"

            return un_na(f"{'@' if add_at else ''}{s0}")

        else:
            if self == self.env._main:
                frame = self.frame
            else:
                if self.isdata():
                    return ""
                if self._process_isgenerator:
                    frame = self._process.gi_frame
                    if frame.f_lasti == -1:  # checks whether generator is created
                        plus = " "
                else:
                    try:
                        gs = inspect.getsourcelines(self._process)
                        s0 = self.env.filename_lineno_to_str(self._process.__code__.co_filename, gs[1]) + " "
                    except OSError:
                        s0 = "n/a"

                    return un_na(f"{'@' if add_at else ''}{s0}")

            return un_na(f"{'@' if add_at else ''}{self.env._frame_to_lineno(frame)}{plus}")

    def line_number(self) -> str:
        """
        current line number of the process

        Returns
        -------
        Current line number : str
            for data components, "" will be returned
        """
        save_suppress_trace_linenumbers = self.env._suppress_trace_linenumbers
        self.env._suppress_trace_linenumbers = False
        s = self.lineno_txt().strip()
        self.env._suppress_trace_linenumbers = save_suppress_trace_linenumbers
        return s

    def to_store_requesters(self) -> "Queue":
        """
        get the queue holding all to_store requesting components

        Returns
        -------
        queue holding all to_store requesting components : Queue
        """
        return self._to_store_requesters

    def from_store_item(self) -> Optional["Component"]:
        """
        return item returned from a self.from_store(...) if valid

        Returns
        -------
        item returned : Component or None, if not valid
        """
        try:
            return self._from_store_item
        except AttributeError:
            return None

    def from_store_store(self) -> Optional["Component"]:
        """
        return store where item was returned from a self.from_store(...) if valid

        Returns
        -------
        item returned : Component or None, if not valid
        """
        try:
            return self._from_store_store
        except AttributeError:
            return None

    def to_store_store(self) -> Optional["Component"]:
        """
        return store where item was sent to with last self.to_store(...) if valid

        Returns
        -------
        item returned : Component or None, if not valid
        """
        try:
            return self._to_store_store
        except AttributeError:
            return None


class Event(Component):
    """
    Event object

    An event object is a specialized Component that is usually not subclassed.

    Apart from the usual Component parameters it has an action parameter, to specifies what should
    happen after becoming active. This action is usually a lambda function.

    Parameters
    ----------
    action : callable
        function called when the component becomes current.

    action_string : str
        string to be printed in trace when action gets executed (default: "action")

    name : str
        name of the component.

        if the name ends with a period (.),
        auto serializing will be applied

        if the name end with a comma,
        auto serializing starting at 1 will be applied

        if omitted, the name will be derived from the class
        it is defined in (lowercased)

    at : float or distribution
        schedule time

        if omitted, now is used

        if distribution, the distribution is sampled

    delay : float or distributiom
        schedule with a delay

        if omitted, no delay

        if distribution, the distribution is sampled

    priority : float
        priority

        default: 0

        if a component has the same time on the event list, this component is sorted accoring to
        the priority.

    urgent : bool
        urgency indicator

        if False (default), the component will be scheduled
        behind all other components scheduled
        for the same time and priority

        if True, the component will be scheduled
        in front of all components scheduled
        for the same time and priority

    suppress_trace : bool
        suppress_trace indicator

        if True, this component will be excluded from the trace

        If False (default), the component will be traced

        Can be queried or set later with the suppress_trace method.

    suppress_pause_at_step : bool
        suppress_pause_at_step indicator

        if True, if this component becomes current, do not pause when stepping

        If False (default), the component will be paused when stepping

        Can be queried or set later with the suppress_pause_at_step method.

    skip_standby : bool
        skip_standby indicator

        if True, after this component became current, do not activate standby components

        If False (default), after the component became current  activate standby components

        Can be queried or set later with the skip_standby method.

    mode : str preferred
        mode

        will be used in trace and can be used in animations

        if omitted, the mode will be "".

        also mode_time will be set to now.

    cap_now : bool
        indicator whether times (at, delay) in the past are allowed. If, so now() will be used.
        default: sys.default_cap_now(), usualy False

    env : Environment
        environment where the component is defined

        if omitted, default_env will be used
    """

    def __init__(
        self,
        action: Callable,
        action_string="action",
        name: str = None,
        at: Union[float, Callable] = None,
        delay: Union[float, Callable] = None,
        priority: float = None,
        urgent: bool = None,
        suppress_trace: bool = False,
        suppress_pause_at_step: bool = False,
        skip_standby: bool = False,
        mode: str = "",
        cap_now: bool = None,
        env: "Environment" = None,
        **kwargs,
    ):
        self._action = action
        self._action_string = action_string
        self._action_taken = False
        if env is None:
            env = g.default_env
        super().__init__(
            name=name,
            at=at,
            delay=delay,
            priority=priority,
            urgent=urgent,
            suppress_trace=suppress_trace,
            suppress_pause_at_step=suppress_pause_at_step,
            skip_standby=skip_standby,
            mode=mode,
            cap_now=cap_now,
            env=env,
            process="process" if env._yieldless else "process_yield",
            **kwargs,
        )

    def process_yield(self):
        self.env.print_trace("", "", self._action_string, "")
        self._action()
        self._action_taken = True
        return
        yield 1  # just to make it a generator

    def process(self):
        self.env.print_trace("", "", self._action_string, "")
        self._action()
        self._action_taken = True

    def action(self, value=None):
        """
        action

        Parameters
        ----------
        value : callable
            new action callable

        Returns
        -------
        current action : callable
        """
        if value is not None:
            self._action = value
        return self._action

    def action_string(self, value=None):
        """
        action_string

        Parameters
        ----------
        value : string
            new action_string

        Returns
        -------
        current action_string : string
        """

        if value is not None:
            self._action_string = value
        return self._action_string

    def action_taken(self):
        """
        action_taken

        Returns
        -------
        action taken: bool
            True if action has been taken, False if not
        """
        return self._action_taken


class Environment:
    """
    environment object

    Parameters
    ----------
    trace : bool or file handle
        defines whether to trace or not

        if this a file handle (open for write), the trace output will be sent to this file.

        if omitted, False

    random_seed : hashable object, usually int
        the seed for random, equivalent to random.seed()

        if "*", a purely random value (based on the current time) will be used
        (not reproducable)

        if the null string, no action on random is taken

        if None (the default), 1234567 will be used.

    time_unit : str
        Supported time_units:

        "years", "weeks", "days", "hours", "minutes", "seconds", "milliseconds", "microseconds", "n/a"

        default: "n/a"

    datetime0: bool or datetime.datetime
        display time and durations as datetime.datetime/datetime.timedelta

        if falsy (default), disabled

        if True, the t=0 will correspond to 1 January 1970

        if no time_unit is specified, but datetime0 is not falsy, time_unit will be set to seconds

    name : str
        name of the environment

        if the name ends with a period (.),
        auto serializing will be applied

        if the name end with a comma,
        auto serializing starting at 1 will be applied

        if omitted, the name will be derived from the class (lowercased)
        or "default environment" if isdefault_env is True.

    print_trace_header : bool
        if True (default) print a (two line) header line as a legend

        if False, do not print a header

        note that the header is only printed if trace=True

    isdefault_env : bool
        if True (default), this environment becomes the default environment

        if False, this environment will not be the default environment

        if omitted, this environment becomes the default environment


    set_numpy_random_seed : bool
        if True (default), numpy.random.seed() will be called with the given seed.

        This is particularly useful when using External distributions.

        If numpy is not installed, this parameter is ignored

        if False, numpy.random.seed is not called.

    do_reset : bool
        if True, reset the simulation environment

        if False, do not reset the simulation environment

        if None (default), reset the simulation environment when run under Pythonista, otherwise no reset

    blind_animation : bool
        if False (default), animation will be performed as expected

        if True, animations will run silently. This is useful to make videos when tkinter is not installed (installable).
        This is particularly useful when running a simulation on a server.
        Note that this will show a slight performance increase, when creating videos.

    Any valid parameter for Environment.animation_parameters() will be forwarded to animation_parameters(), e.g.
        env = sim.Environment(trace=True, animation=True, speed=5)

    Note
    ----
    The trace may be switched on/off later with trace

    The seed may be later set with random_seed()

    Initially, the random stream will be seeded with the value 1234567.
    If required to be purely, not reproducable, values, use
    random_seed="*".
    """

    def __init__(
        self,
        trace: bool = False,
        random_seed: Hashable = None,
        set_numpy_random_seed: bool = True,
        time_unit: str = "n/a",
        datetime0: Union[bool, datetime.datetime, str] = False,
        name: str = None,
        print_trace_header: bool = True,
        isdefault_env: bool = True,
        retina: bool = False,
        do_reset: bool = None,
        blind_animation: bool = False,
        yieldless: bool = None,
        *args,
        **kwargs,
    ):
        if name is None:
            if isdefault_env:
                name = "default environment"
        _set_name(name, Environment._nameserialize, self)

        self._nameserializeMonitor = {}  # required here for to_freeze functionality
        self._time_unit = _time_unit_lookup(time_unit)
        self._time_unit_name = time_unit
        if yieldless is None:
            self._yieldless = _yieldless
        else:
            self._yieldless = yieldless
        self._any_yield = False
        if datetime0 is None:
            datetime0 = False
        self.datetime0(datetime0)

        if "to_freeze" in kwargs:
            self.isfrozen = True
            return
        self._ui = False
        self._step_n = 0
        self._ui_granularity = 1

        if do_reset is None:
            do_reset = Pythonista
        if do_reset:
            reset()
        if isdefault_env:
            g.default_env = self
        self.trace(trace)
        self._source_files = {inspect.getframeinfo(_get_caller_frame()).filename: 0}
        _random_seed(random_seed, set_numpy_random_seed=set_numpy_random_seed)
        self._suppress_trace_standby = True
        self._suppress_trace_linenumbers = False
        if self._trace:
            if print_trace_header:
                self.print_trace_header()
            self.print_trace("", "", self.name() + " initialize")
        self.env = self
        # just to allow main to be created; will be reset later
        self._nameserializeComponent = {}
        self._now = 0
        self._t = 0
        self._offset = 0
        self._main = Component(name="main", env=self, process=None)
        self._main.status._value = current
        self._main.frame = _get_caller_frame()
        self._current_component = self._main
        if self._trace:
            self.print_trace(self.time_to_str(0), "main", "current")
        self._nameserializeQueue = {}
        self._nameserializeComponent = {}
        self._nameserializeResource = {}
        self._nameserializeStore = {}
        self._nameserializeState = {}
        self._seq = 0
        self._event_list = []
        self._standbylist = []
        self._pendingstandbylist = []

        self.an_objects = set()
        self.an_objects_over3d = set()

        self.an_objects3d = set()
        self.ui_objects = []
        self.sys_objects = set()
        self.serial = 0
        self._speed = 1.0
        self._animate = False
        self._animate3d = False
        if "_AnimateIntro" in globals():  # in case of minimized, _AnimateIntro and _AnimateExtro are not available
            self.view = _AnimateIntro(env=self)
            _AnimateExtro(env=self)
        self._gl_initialized = False
        self._camera_auto_print = False
        self.obj_filenames = {}
        self.running = False
        self._maximum_number_of_bitmaps = 4000
        self._t = 0
        self.video_t = 0
        self.frame_number = 0
        self._exclude_from_animation = "only in video"
        self._audio = None
        self._audio_speed = 1.0
        self._animate_debug = False
        self._synced = True
        self._step_pressed = False
        self.stopped = False
        self._paused = False
        self.last_s0 = ""
        if PythonInExcel or AnacondaCode:
            self._blind_animation = True
        else:
            self._blind_animation = blind_animation

        if self._blind_animation:
            with self.suppress_trace():
                self._blind_video_maker = _BlindVideoMaker(process="", suppress_trace=True)
        if PyDroid:
            if g.tkinter_loaded == "?":
                g.tkinter_loaded = "tkinter" in sys.modules

        if Pythonista:
            self._width, self._height = ui.get_screen_size()
            if retina:
                self.retina = int(scene.get_screen_scale())
            else:
                self.retina = 1
            self._width = int(self._width) * self.retina
            self._height = int(self._height) * self.retina
        else:
            self.retina = 1  # not sure whether this is required
            self._width = 1024
            self._height = 768
        self.root = None
        self._position = (0, 0)
        self._position3d = (0, 0)
        self._width3d = 1024
        self._height3d = 768
        self._video_width: Union[int, str] = "auto"
        self._video_height: Union[int, str] = "auto"
        self._video_mode = "2d"
        self._background3d_color = "black"
        self._title = "salabim"
        self._show_menu_buttons = True
        self._x0 = 0.0
        self._y0 = 0.0
        self._x1: float = self._width
        self._scale = 1.0
        self._y1 = self._y0 + self._height
        self._background_color = "white"
        self._foreground_color = "black"
        self._fps = 30.0
        self._modelname = ""
        self.use_toplevel = False
        self._show_fps = False
        self._show_time = True
        self._video = ""
        self._video_out = None
        self._video_repeat = 1
        self._video_pingpong = False
        if self.env._yieldless:
            global greenlet
            import greenlet

            self._glet = greenlet.greenlet(self.do_simulate)

        if Pythonista:
            can_animate()
            fonts()  # this speeds up for strange reasons
        self.an_modelname()

        self.an_clocktext()

        ap_parameters = [parameter for parameter in inspect.signature(self.animation_parameters).parameters]
        ap_kwargs = {}
        for k, v in list(kwargs.items()):
            if k in ap_parameters:
                del kwargs[k]
                ap_kwargs[k] = v
        if ap_kwargs:
            self.animation_parameters(**ap_kwargs)

        self.setup(*args, **kwargs)

    # ENVIRONMENT ANNOTATION START
    # ENVIRONMENT ANNOTATION END

    _nameserialize = {}
    cached_modelname_width = [None, None]

    def setup(self) -> None:
        """
        called immediately after initialization of an environment.

        by default this is a dummy method, but it can be overridden.

        only keyword arguments are passed
        """
        pass

    def serialize(self) -> int:
        self.serial += 1
        return self.serial

    def __repr__(self):
        return object_to_str(self) + " (" + self.name() + ")"

    def yieldless(self) -> bool:
        """
        yieldless status

        Returns
        -------
        yieldless status: bool

        Note
        ----
        It is not possible to change the yieldless status
        """
        return self._yieldless

    def animation_pre_tick(self, t: float) -> None:
        """
        called just before the animation object loop.

        Default behaviour: just return

        Parameters
        ----------
        t : float
            Current (animation) time.
        """
        if self._ui:
            self._handle_ui_event()

    def animation_post_tick(self, t: float) -> None:
        """
        called just after the animation object loop.

        Default behaviour: just return

        Parameters
        ----------
        t : float
            Current (animation) time.
        """
        ...

    def animation_pre_tick_sys(self, t: float) -> None:
        for ao in self.sys_objects.copy():  # copy required as ao's may be removed due to keep
            ao.update(t)

    def animation3d_init(self):
        can_animate3d(try_only=False)
        glut.glutInit()
        glut.glutInitDisplayMode(glut.GLUT_RGBA | glut.GLUT_DOUBLE | glut.GLUT_DEPTH)
        glut.glutInitWindowSize(self._width3d, self._height3d)
        glut.glutInitWindowPosition(*self._position3d)

        self.window3d = glut.glutCreateWindow("salabim3d")

        gl.glClearDepth(1.0)
        gl.glDepthFunc(gl.GL_LESS)
        gl.glEnable(gl.GL_DEPTH_TEST)
        gl.glShadeModel(gl.GL_SMOOTH)

        #        glut.glutReshapeFunc(lambda width, height: glut.glutReshapeWindow(640, 480))
        self._opengl_key_press_bind = {}
        self._opengl_key_press_special_bind = {}

        glut.glutKeyboardFunc(self._opengl_key_pressed)
        glut.glutSpecialFunc(self._opengl_key_pressed_special)

        glut.glutDisplayFunc(lambda: None)
        self._gl_initialized = True

    def _opengl_key_pressed(self, *args):
        key = args[0]
        if key in self._opengl_key_press_bind:
            self._opengl_key_press_bind[key]()

    def _opengl_key_pressed_special(self, *args):
        special_keys = glut.glutGetModifiers()
        alt_active = glut.GLUT_ACTIVE_ALT & special_keys
        shift_active = glut.GLUT_ACTIVE_SHIFT & special_keys
        ctrl_active = glut.GLUT_ACTIVE_CTRL & special_keys
        spec_keys = []
        if alt_active:
            spec_keys.append("Alt")
        if shift_active:
            spec_keys.append("Shift")
        if ctrl_active:
            spec_keys.append("Control")
        spec_key = "-".join(spec_keys)
        key = (args[0], spec_key)

        if key in self._opengl_key_press_special_bind:
            self._opengl_key_press_special_bind[key]()

    def camera_move(self, spec: str = "", lag: float = 1, offset: float = 0, enabled: bool = True):
        """
        Moves the camera according to the given spec, which is normally a collection of camera_print
        outputs.

        Parameters
        ----------
        spec : str
            output normally obtained from camera_auto_print lines

        lag : float
            lag time (for smooth camera movements) (default: 1))

        offset : float
            the duration (can be negative) given is added to the times given in spec. Default: 0

        enabled : bool
            if True (default), move camera according to spec/lag

            if False, freeze camera movement
        """
        if not has_numpy():
            raise ImportError("camera move requires numpy")

        props = "x_eye y_eye z_eye x_center y_center z_center field_of_view_y".split()

        build_values = collections.defaultdict(list)
        build_times = collections.defaultdict(list)
        values = collections.defaultdict(list)
        times = collections.defaultdict(list)

        if enabled:
            for prop in props:
                build_values[prop].append(getattr(self.view, prop)(t=offset))
                build_times[prop].append(offset)

            for prop in props:
                setattr(self.view, prop, lambda arg, t, prop=prop: interp(t, times[prop], values[prop]))  # default argument prop is evaluated at start!

            for line in spec.split("\n"):
                line = line.strip()
                if line.startswith("view("):
                    line = line[5:]
                    line0, line1 = line.split(")  # t=")
                    time = float(line1) + offset
                    parts = line0.replace(" ", "").split(",")
                    for part in parts:
                        prop, value = part.split("=")
                        if prop in props:
                            build_times[prop].append(time)
                            build_values[prop].append(float(value))
                        else:
                            raise ValueError(f"incorrect line in spec: {line}")

            for prop in props:
                pending_value = build_values[prop][0]
                pending_time = build_times[prop][0]
                values[prop].append(pending_value)
                times[prop].append(pending_time)
                build_values[prop].append(build_values[prop][-1])
                build_times[prop].append(build_times[prop][-1] + lag)

                for value, time in zip(build_values[prop], build_times[prop]):
                    if time > pending_time:
                        values[prop].append(pending_value)
                        times[prop].append(pending_time)

                    values[prop].append(interpolate(time, times[prop][-1], pending_time, values[prop][-1], pending_value))
                    times[prop].append(time)
                    pending_value = value
                    pending_time = time + lag

        else:
            for prop in props:
                setattr(self.view, prop, getattr(self.view, prop)(self.t()))

    def camera_rotate(self, event=None, delta_angle=None):
        t = self.t()
        adjusted_x = self.view.x_eye(t) - self.view.x_center(t)
        adjusted_y = self.view.y_eye(t) - self.view.y_center(t)
        cos_rad = math.cos(math.radians(delta_angle))
        sin_rad = math.sin(math.radians(delta_angle))
        self.view.x_eye = self.view.x_center(t) + cos_rad * adjusted_x + sin_rad * adjusted_y
        self.view.y_eye = self.view.y_center(t) - sin_rad * adjusted_x + cos_rad * adjusted_y

        if self._camera_auto_print:
            self.camera_print(props="x_eye y_eye")

    def camera_zoom(self, event=None, factor_xy=None, factor_z=None):
        t = self.t()
        self.view.x_eye = self.view.x_center(t) - (self.view.x_center(t) - self.view.x_eye(t)) * factor_xy
        self.view.y_eye = self.view.y_center(t) - (self.view.y_center(t) - self.view.y_eye(t)) * factor_xy
        self.view.z_eye = self.view.z_center(t) - (self.view.z_center(t) - self.view.z_eye(t)) * factor_z
        if self._camera_auto_print:
            self.camera_print(props="x_eye y_eye z_eye")

    def camera_xy_center(self, event=None, x_dis=None, y_dis=None):
        t = self.t()
        self.view.x_center = self.view.x_center(t) + x_dis
        self.view.y_center = self.view.y_center(t) + y_dis
        if self._camera_auto_print:
            self.camera_print(props="x_center y_center")

    def camera_xy_eye(self, event=None, x_dis=None, y_dis=None):
        t = self.t()
        self.view.x_eye = self.view.x_eye(t) + x_dis
        self.view.y_eye = self.view.y_eye(t) + y_dis
        if self._camera_auto_print:
            self.camera_print(props="x_eye y_eye")

    def camera_field_of_view(self, event=None, factor=None):
        t = self.t()
        self.view.field_of_view_y = self.view.field_of_view_y(t) * factor
        if self._camera_auto_print:
            self.camera_print(props="field_of_view_y")

    def camera_tilt(self, event=None, delta_angle=None):
        t = self.t()
        x_eye = self.view.x_eye(t)
        y_eye = self.view.y_eye(t)
        z_eye = self.view.z_eye(t)
        x_center = self.view.x_center(t)
        y_center = self.view.y_center(t)
        z_center = self.view.z_center(t)

        dx = x_eye - x_center
        dy = y_eye - y_center
        dz = z_eye - z_center
        dxy = math.hypot(dx, dy)
        if dy > 0:
            dxy = -dxy
        alpha = math.degrees(math.atan2(dxy, dz))
        alpha_new = alpha + delta_angle
        dxy_new = math.tan(math.radians(alpha_new)) * dz

        self.view.x_center = x_eye + (dxy_new / dxy) * (x_center - x_eye)
        self.view.y_center = y_eye + (dxy_new / dxy) * (y_center - y_eye)
        if self._camera_auto_print:
            self.camera_print(props="x_center y_center")

    def camera_rotate_axis(self, event=None, delta_angle=None):
        t = self.t()
        adjusted_x = self.view.x_center(t) - self.view.x_eye(t)
        adjusted_y = self.view.y_center(t) - self.view.y_eye(t)
        cos_rad = math.cos(math.radians(delta_angle))
        sin_rad = math.sin(math.radians(delta_angle))
        self.view.x_center = self.view.x_eye(t) + cos_rad * adjusted_x + sin_rad * adjusted_y
        self.view.y_center = self.view.y_eye(t) - sin_rad * adjusted_x + cos_rad * adjusted_y
        if self._camera_auto_print:
            self.camera_print(props="x_eye y_eye")

    def camera_print(self, event=None, props=None):
        t = self.t()
        if props is None:
            props = "x_eye y_eye z_eye x_center y_center z_center field_of_view_y"
        s = "view("
        items = []
        for prop in props.split():
            items.append(f"{getattr(self.view,prop)(t):.4f}")
        print("view(" + (",".join(f"{prop}={getattr(self.view,prop)(t):.4f}" for prop in props.split())) + f")  # t={t:.4f}")

    def _bind(self, tkinter_event, func):
        self.root.bind(tkinter_event, func)
        if len(tkinter_event) == 1:
            opengl_key = bytes(tkinter_event, "utf-8")
            self._opengl_key_press_bind[opengl_key] = func
        else:
            tkinter_event = tkinter_event[1:-1]  # get rid of <>
            if "-" in tkinter_event:
                spec_key, key = tkinter_event.split("-")
            else:
                key = tkinter_event
                spec_key = ""
            if key == "Up":
                opengl_key = (glut.GLUT_KEY_UP, spec_key)
            elif key == "Down":
                opengl_key = (glut.GLUT_KEY_DOWN, spec_key)
            elif key == "Left":
                opengl_key = (glut.GLUT_KEY_LEFT, spec_key)
            elif key == "Right":
                opengl_key = (glut.GLUT_KEY_RIGHT, spec_key)
            self._opengl_key_press_special_bind[opengl_key] = func

    def camera_auto_print(self, value: bool = None) -> bool:
        """
        queries or set camera_auto_print

        Parameters
        ----------
        value : boolean
            if None (default), no action

            if True, camera_print will be called on each camera control keypress

            if False, no automatic camera_print

        Returns
        -------
        Current status : bool

        Note
        ----
        The camera_auto_print functionality is useful to get the spec for camera_move()
        """
        if value is not None:
            self._camera_auto_print = value
            if value:
                self.camera_print()
        return self._camera_auto_print

    def _camera_control(self):
        self._bind("<Left>", functools.partial(self.camera_rotate, delta_angle=-1))
        self._bind("<Right>", functools.partial(self.camera_rotate, delta_angle=+1))

        self._bind("<Up>", functools.partial(self.camera_zoom, factor_xy=0.9, factor_z=0.9))
        self._bind("<Down>", functools.partial(self.camera_zoom, factor_xy=1 / 0.9, factor_z=1 / 0.9))

        self._bind("z", functools.partial(self.camera_zoom, factor_xy=1, factor_z=0.9))
        self._bind("Z", functools.partial(self.camera_zoom, factor_xy=1, factor_z=1 / 0.9))

        self._bind("<Shift-Up>", functools.partial(self.camera_zoom, factor_xy=0.9, factor_z=1))
        self._bind("<Shift-Down>", functools.partial(self.camera_zoom, factor_xy=1 / 0.9, factor_z=1))

        self._bind("<Alt-Left>", functools.partial(self.camera_xy_eye, x_dis=-10, y_dis=0))
        self._bind("<Alt-Right>", functools.partial(self.camera_xy_eye, x_dis=10, y_dis=0))
        self._bind("<Alt-Down>", functools.partial(self.camera_xy_eye, x_dis=0, y_dis=-10))
        self._bind("<Alt-Up>", functools.partial(self.camera_xy_eye, x_dis=0, y_dis=10))

        self._bind("<Control-Left>", functools.partial(self.camera_xy_center, x_dis=-10, y_dis=0))
        self._bind("<Control-Right>", functools.partial(self.camera_xy_center, x_dis=10, y_dis=0))
        self._bind("<Control-Down>", functools.partial(self.camera_xy_center, x_dis=0, y_dis=-10))
        self._bind("<Control-Up>", functools.partial(self.camera_xy_center, x_dis=0, y_dis=10))

        self._bind("o", functools.partial(self.camera_field_of_view, factor=0.9))
        self._bind("O", functools.partial(self.camera_field_of_view, factor=1 / 0.9))

        self._bind("t", functools.partial(self.camera_tilt, delta_angle=-1))
        self._bind("T", functools.partial(self.camera_tilt, delta_angle=1))

        self._bind("r", functools.partial(self.camera_rotate_axis, delta_angle=1))
        self._bind("R", functools.partial(self.camera_rotate_axis, delta_angle=-1))

        self._bind("p", functools.partial(self.camera_print))

    def show_camera_position(self, over3d: bool = None) -> None:
        """
        show camera position on the tkinter window or over3d window

        The 7 camera settings will be shown in the top left corner.

        Parameters
        ----------
        over3d : bool
            if False (default), present on 2D screen

            if True, present on 3D overlay
        """
        if over3d is None:
            over3d = default_over3d()
        top = self.height3d() - 40 if over3d else self.height() - 90
        for i, prop in enumerate("x_eye y_eye z_eye x_center y_center z_center field_of_view_y".split()):
            ao = AnimateRectangle(spec=(0, 0, 75, 35), fillcolor="30%gray", x=5 + i * 80, y=top, screen_coordinates=True, over3d=over3d)
            ao = AnimateText(
                text=lambda arg, t: f"{arg.label}",
                x=5 + i * 80 + 70,
                y=top + 15,
                font="calibri",
                text_anchor="se",
                textcolor="white",
                screen_coordinates=True,
                over3d=over3d,
            )
            ao.label = "fovy" if prop == "field_of_view_y" else prop

            ao = AnimateText(
                text=lambda arg, t: f"{getattr(self.view,arg.prop)(t):11.3f}",
                x=5 + i * 80 + 70,
                y=top,
                font="calibri",
                text_anchor="se",
                textcolor="white",
                screen_coordinates=True,
                over3d=over3d,
            )
            ao.prop = prop

    def print_info(self, as_str: bool = False, file: TextIO = None) -> str:
        """
        prints information about the environment

        Parameters
        ----------
        as_str: bool
            if False (default), print the info
            if True, return a string containing the info

        file: file
            if None(default), all output is directed to stdout

            otherwise, the output is directed to the file

        Returns
        -------
        info (if as_str is True) : str
        """
        result = []
        result.append(object_to_str(self) + " " + hex(id(self)))
        result.append("  name=" + self.name())
        result.append("  now=" + self.time_to_str(self._now - self._offset))
        result.append("  current_component=" + self._current_component.name())
        result.append("  trace=" + str(self._trace))
        return return_or_print(result, as_str, file)

    def step(self) -> None:
        """
        executes the next step of the future event list

        for advanced use with animation / GUI loops
        """
        if self._ui:
            if self._pause_at_each_step:
                animate = self._ui_window["-ANIMATE-"].get()
                self.animate(True)
                self.paused(True)
                self._ui_window["-ANIMATE-"].update(animate)
                self._handle_ui_event()
            else:
                if self._step_n >= self._ui_granularity:
                    self._step_n = 0
                    self._handle_ui_event()
                else:
                    self._step_n += 1

        try:
            if not self._current_component._skip_standby:
                if len(self.env._pendingstandbylist) > 0:
                    c = self.env._pendingstandbylist.pop(0)
                    if c.status.value == standby:  # skip cancelled components
                        c.status._value = current
                        c._scheduled_time = inf
                        self.env._current_component = c
                        if self._trace:
                            self.print_trace(
                                self.time_to_str(self._now - self.env._offset),
                                c.name(),
                                "current (standby)",
                                s0=c.lineno_txt(),
                                _optional=self._suppress_trace_standby,
                            )
                        if self.env._yieldless:
                            c._glet.switch()
                            if c._glet.dead:
                                self._terminate(c)
                            return
                        else:
                            try:
                                next(c._process)
                                return
                            except TypeError:
                                c._process(**c._process_kwargs)
                                self._terminate(c)
                                return
                            except StopIteration:
                                self._terminate(c)
                                return

            if len(self.env._standbylist) > 0:
                self._pendingstandbylist = list(self.env._standbylist)
                self.env._standbylist = []

            if self._event_list:
                (t, priority, seq, c, return_value) = heapq.heappop(self._event_list)
            else:
                c = self._main
                if self.end_on_empty_eventlist:
                    t = self.env._now
                    self.print_trace("", "", "run ended", "no events left", s0=un_na(c.lineno_txt()))
                else:
                    t = inf
            c._on_event_list = False
            self.env._now = t

            self._current_component = c

            c.status._value = current
            c._scheduled_time = inf
            if self._trace:
                if c.overridden_lineno:
                    self.print_trace(self.time_to_str(self._now - self._offset), c.name(), "current", s0=un_na(c.overridden_lineno))
                else:
                    self.print_trace(self.time_to_str(self._now - self._offset), c.name(), "current", s0=un_na(c.lineno_txt()))
            if c == self._main:
                self.running = False
                return
            c._check_fail()
            if self.env._yieldless:
                if PyPy:
                    try:
                        c._glet.switch()
                    except greenlet._continuation.error:
                        ...  # this is to prevent a strange error (bug) in PyPy/greenlet
                else:
                    c._glet.switch()
                if c._glet.dead:
                    self._terminate(c)
            else:
                if c._process_isgenerator:
                    try:
                        try:
                            c._process.send(return_value)
                        except TypeError as e:
                            if "just-started" in str(e):  # only do this for just started generators
                                c._process.send(None)
                            else:
                                raise e
                    except StopIteration:
                        self._terminate(c)
                else:
                    c._process(**c._process_kwargs)

                    self._terminate(c)
        except Exception as e:
            if self._animate:
                self.an_quit()
            raise e

    def _terminate(self, c):
        if self.env._yieldless:
            s0 = ""
        else:
            if c._process_isgenerator:
                if self._trace and not self._suppress_trace_linenumbers:
                    gi_code = c._process.gi_code
                    try:
                        gs = inspect.getsourcelines(gi_code)
                        s0 = un_na(c.overridden_lineno or self.filename_lineno_to_str(gi_code.co_filename, len(gs[0]) + gs[1] - 1) + "+")
                    except OSError:
                        s0 = "n/a"
                else:
                    s0 = None
            else:
                if self._trace and not self._suppress_trace_linenumbers:
                    try:
                        gs = inspect.getsourcelines(c._process)
                        s0 = un_na(c.overridden_lineno or self.filename_lineno_to_str(c._process.__code__.co_filename, len(gs[0]) + gs[1] - 1) + "+")
                    except OSError:
                        s0 = "n/a"
                else:
                    s0 = None

        for r in list(c._claims):
            c._release(r, s0=s0)
        if self._trace:
            self.print_trace("", "", c.name() + " ended", s0=un_na(s0))
        c.remove_animation_children()
        c._from_store_item = None  # to avoid memory leak
        c._to_store_item = None  # to avoid memory leak

        c.status._value = data
        c._scheduled_time = inf
        c._process = None

    def _print_event_list(self, s: str = "") -> None:
        print("eventlist ", s)
        for t, priority, sequence, comp, return_value in self._event_list:
            print("    ", self.time_to_str(t), comp.name(), "priority", priority, "return_value", return_value)

    def on_closing(self):
        self.an_quit()

    def animation_parameters(
        self,
        animate: Union[bool, str] = None,
        synced: bool = None,
        speed: float = None,
        width: int = None,
        height: int = None,
        title: str = None,
        show_menu_buttons: bool = None,
        x0: float = None,
        y0: float = None,
        x1: float = None,
        background_color: ColorType = None,
        foreground_color: ColorType = None,
        background3d_color: ColorType = None,
        fps: float = None,
        modelname: str = None,
        use_toplevel: bool = None,
        show_fps: bool = None,
        show_time: bool = None,
        maximum_number_of_bitmaps: int = None,
        video: Any = None,
        video_repeat: int = None,
        video_pingpong: bool = None,
        audio: str = None,
        audio_speed: float = None,
        animate_debug: bool = None,
        animate3d: bool = None,
        width3d: int = None,
        height3d: int = None,
        video_width: Union[int, str] = None,
        video_height: Union[int, str] = None,
        video_mode: str = None,
        position: Any = None,
        position3d: Any = None,
        visible: bool = None,
    ):
        """
        set animation parameters

        Parameters
        ----------
        animate : bool
            animate indicator

            new animate indicator

            if '?', animation will be set, possible

            if not specified, no change

        animate3d : bool
            animate3d indicator

            new animate3d indicator

            if '?', 3D-animation will be set, possible

            if not specified, no change

        synced : bool
            specifies whether animation is synced

            if omitted, no change. At init of the environment synced will be set to True

        speed : float
            speed

            specifies how much faster or slower than real time the animation will run.
            e.g. if 2, 2 simulation time units will be displayed per second.

        width : int
            width of the animation in screen coordinates

            if omitted, no change. At init of the environment, the width will be
            set to 1024 for non Pythonista and the current screen width for Pythonista.

        height : int
            height of the animation in screen coordinates

            if omitted, no change. At init of the environment, the height will be
            set to 768 for non Pythonista and the current screen height for Pythonista.

        position : tuple(x,y)
            position of the animation window

            if omitted, no change. At init of the environment, the position will be
            set to (0, 0)

            no effect for Pythonista

        width3d : int
            width of the 3d animation in screen coordinates

            if omitted, no change. At init of the environment, the 3d width will be
            set to 1024.

        height3d : int
            height of the 3d animation in screen coordinates

            if omitted, no change. At init of the environment, the 3d height will be
            set to 768.

        position3d : tuple(x,y)
            position of the 3d animation window

            At init of the environment, the position will be set to (0, 0)

            This has to be set before the 3d animation starts as the window can only be postioned at initialization

        title : str
            title of the canvas window

            if omitted, no change. At init of the environment, the title will be
            set to salabim.

            if "", the title will be suppressed.

        x0 : float
            user x-coordinate of the lower left corner

            if omitted, no change. At init of the environment, x0 will be set to 0.

        y0 : float
            user y_coordinate of the lower left corner

            if omitted, no change. At init of the environment, y0 will be set to 0.

        x1 : float
            user x-coordinate of the lower right corner

            if omitted, no change. At init of the environment, x1 will be set to 1024
            for non Pythonista and the current screen width for Pythonista.

        background_color : colorspec
            color of the background

            if omitted, no change. At init of the environment, this will be set to white.

        foreground_color : colorspec
            color of foreground (texts)

            if omitted and background_color is specified, either white of black will be used,
            in order to get a good contrast with the background color.

            if omitted and background_color is also omitted, no change. At init of the
            environment, this will be set to black.

        background3d_color : colorspec
            color of the 3d background

            if omitted, no change. At init of the environment, this will be set to black.

        fps : float
            number of frames per second

        modelname : str
            name of model to be shown in upper left corner,
            along with text "a salabim model"

            if omitted, no change. At init of the environment, this will be set
            to the null string, which implies suppression of this feature.

        use_toplevel : bool
            if salabim animation is used in parallel with
            other modules using tkinter, it might be necessary to
            initialize the root with tkinter.TopLevel().
            In that case, set this parameter to True.

            if False (default), the root will be initialized with tkinter.Tk()

        show_fps : bool
            if True, show the number of frames per second

            if False, do not show the number of frames per second (default)

        show_time : bool
            if True, show the time (default)

            if False, do not show the time

        show_menu_buttons : bool
            if True, show the menu buttons (default)

            if False, do not show the menu buttons

        maximum_number_of_bitmaps : int
            maximum number of tkinter bitmaps (default 4000)

        video : str
            if video is not omitted, a video with the name video
            will be created.

            Normally, use .mp4 as extension.

            If the extension is .gif or .png an animated gif / png file will be written, unless there
            is a * in the filename

            If the extension is .gif, .jpg, .png, .bmp, .ico or .tiff and one * appears in the filename,
            individual frames will be written with
            a six digit sequence at the place of the asteriks in the file name.
            If the video extension is not .gif, .jpg, .png, .bmp, .ico or .tiff, a codec may be added
            by appending a plus sign and the four letter code name,
            like "myvideo.avi+DIVX".

            If no codec is given, MJPG will be used for .avi files, otherwise .mp4v

            Under PyDroid only .avi files are supported.

        video_repeat : int
            number of times animated gif or png should be repeated

            0 means inifinite

            at init of the environment video_repeat is 1

            this only applies to gif and png files production.

        video_pingpong : bool
            if True, all frames will be added reversed at the end of the video (useful for smooth loops)
            at init of the environment video_pingpong is False

            this only applies to gif and png files production.

        audio : str
            name of file to be played (mp3 or wav files)

            if the none string, the audio will be stopped

            default: no change

            for more information, see Environment.audio()

        visible : bool
            if True (start condition), the animation window will be visible

            if False, the animation window will be hidden ('withdrawn')

        Note
        ----
        The y-coordinate of the upper right corner is determined automatically
        in such a way that the x and y scaling are the same.

        """
        frame_changed = False
        width_changed = False
        height_changed = False
        fps_changed = False

        if speed is not None:
            self._speed = speed
            self.set_start_animation()

        if show_fps is not None:
            self._show_fps = show_fps

        if show_time is not None:
            self._show_time = show_time

        if maximum_number_of_bitmaps is not None:
            self._maximum_number_of_bitmaps = maximum_number_of_bitmaps

        if synced is not None:
            self._synced = synced
            if self._ui:
                self._ui_window["-SYNCED-"].update(synced)
            self.set_start_animation()

        if width is not None:
            if self._width != width:
                self._width = int(width)
                frame_changed = True
                width_changed = True

        if height is not None:
            if self._height != height:
                self._height = int(height)
                frame_changed = True
                height_changed = True

        if width3d is not None:
            if self._width != width:
                self._width3d = width3d
                if self._gl_initialized:
                    glut.glutReshapeWindow(self._width3d, self._height3d)

        if height3d is not None:
            if self._height != height:
                self._height3d = height3d
                if self._gl_initialized:
                    glut.glutReshapeWindow(self._width3d, self._height3d)

        if position is not None:
            if self._position != position:
                self._position = position
                if self.root is not None:
                    self.root.geometry(f"+{self._position[0]}+{self._position[1]}")

        if position3d is not None:
            self._position3d = position3d

        if video_width is not None:
            if self._video:
                raise ValueError("video_width may not be changed while recording video")
            self._video_width = video_width

        if video_height is not None:
            if self._video:
                raise ValueError("video_height may not be changed while recording video")
            self._video_height = video_height

        if video_mode is not None:
            if video_mode not in ("2d", "screen", "3d"):
                raise ValueError("video_mode " + video_mode + " not recognized")
            self._video_mode = video_mode

        if title is not None:
            if self._title != title:
                self._title = title
                frame_changed = True

        if show_menu_buttons is not None:
            if self._show_menu_buttons != show_menu_buttons:
                self._show_menu_buttons = show_menu_buttons
                frame_changed = True

        if fps is not None:
            if self._video:
                raise ValueError("video_repeat may not be changed while recording video")
            self._fps = fps

        if x0 is not None:
            if self._x0 != x0:
                self._x0 = x0
                self.uninstall_uios()

        if x1 is not None:
            if self._x1 != x1:
                self._x1 = x1
                self.uninstall_uios()

        if y0 is not None:
            if self._y0 != y0:
                self._y0 = y0
                self.uninstall_uios()

        if background_color is not None:
            if background_color in ("fg", "bg"):
                raise ValueError(f"{background_color} not allowed for background_color")
            if self._background_color != background_color:
                self._background_color = background_color
                frame_changed = True
            if foreground_color is None:
                self._foreground_color = "white" if self.is_dark("bg") else "black"

        if foreground_color is not None:
            if foreground_color in ("fg", "bg"):
                raise ValueError(f"{foreground_color} not allowed for foreground_color")
            self._foreground_color = foreground_color

        if background3d_color is not None:
            self._background3d_color = background3d_color

        if modelname is not None:
            self._modelname = modelname

        if use_toplevel is not None:
            self.use_toplevel = use_toplevel

        if animate_debug is not None:
            self._animate_debug = animate_debug

        if audio_speed is not None:
            if self._audio_speed != audio_speed:
                self._audio_speed = audio_speed
                if audio_speed != self._speed:
                    if self._audio is not None:
                        if Pythonista:
                            self._audio.player.pause()
                        if Windows:
                            self._audio.stop()
                self.set_start_animation()

        if audio is not None:
            if (self._audio is None and audio != "") or (self.audio is not None and self._audio.filename != audio):
                if self._audio is not None:
                    if Pythonista:
                        self._audio.player.pause()
                    if Windows:
                        self._audio.stop()
                    if self._video_out is not None:
                        self.audio_segments[-1].t1 = self.frame_number / self._fps
                if audio == "":
                    self._audio = None
                else:
                    if ">" not in audio:
                        audio = audio + ">0"
                    audio_filename, startstr = audio.split(">")
                    if not os.path.isfile(audio_filename):
                        raise FileNotFoundError(audio_filename)
                    if Pythonista:
                        import sound  # type: ignore

                        class Play:
                            def __init__(self, s, repeat=-1):
                                self.player = sound.Player(s)
                                self.player.number_of_loops = repeat

                        self._audio = Play(audio_filename, repeat=0)
                        self._audio.duration = self._audio.player.duration
                        self._audio.player.play()
                        self._audio.player.current_time = 0

                    else:
                        self._audio = AudioClip(audio_filename)

                    self._audio.start = float(startstr)

                    self._audio.t0 = self._t

                    self._audio.filename = audio_filename

                    if self._video_out is not None:  # if video ist started here as well, the audio_segment is created later
                        self._audio.t0 = self.frame_number / self._fps
                        self.audio_segments.append(self._audio)
                    self.set_start_animation()

        if animate3d is not None:
            if animate3d == "?":
                animate3d = can_animate3d(try_only=True)
            self._animate3d = animate3d
            if not animate3d:
                glut.glutDestroyWindow(self.window3d)
                glut.glutMainLoopEvent()
                self._gl_initialized = False

        if animate is not None:
            if animate == "?":
                animate = can_animate(try_only=True)
            if animate != self._animate:
                frame_changed = True
                self._animate = animate
                if self._ui and "-ANIMATE-" in self._ui_keys:
                    self._ui_window["-ANIMATE-"].update(animate)

        self._scale = self._width / (self._x1 - self._x0)
        self._y1 = self._y0 + self._height / self._scale

        if g.animation_env is not self:
            if g.animation_env is not None:
                g.animation_env.video_close()
            if self._animate:
                frame_changed = True
            else:
                frame_changed = False  # no animation required, so leave running animation_env untouched

        if video_repeat is not None:
            if self._video:
                raise ValueError("video_repeat may not be changed while recording video")
            self._video_repeat = video_repeat

        if video_pingpong is not None:
            if self._video:
                raise ValueError("video_pingpong may not be changed while recording video")
            self._video_pingpong = video_pingpong

        if video is not None:
            if video != self._video:
                if self._video:
                    self.video_close()
                self._video = video

                if video:
                    if self._video_mode == "screen" and ImageGrab is None:
                        raise ValueError("video_mode='screen' not supported on this platform (ImageGrab does not exist)")
                    if self._video_width == "auto":
                        if self._video_mode == "3d":
                            self._video_width_real = self._width3d
                        elif self._video_mode == "2d":
                            self._video_width_real = self._width
                        else:
                            img = ImageGrab.grab()
                            self._video_width_real = img.size[0]
                    else:
                        self._video_width_real = self._video_width

                    if self._video_height == "auto":
                        if self._video_mode == "3d":
                            self._video_height_real = self._height3d
                        elif self._video_mode == "2d":
                            self._video_height_real = self._height
                        else:
                            img = ImageGrab.grab()
                            self._video_height_real = img.size[1]
                    else:
                        self._video_height_real = self._video_height
                    can_animate(try_only=False)

                    video_path = Path(video)
                    extension = video_path.suffix.lower()
                    self._video_name = video
                    self._real_fps = self._fps  # only overridden for animated gifs
                    video_path.parent.mkdir(parents=True, exist_ok=True)
                    if extension in (".gif", ".webp") and not ("*" in video_path.stem):
                        self._video_out = extension[1:]  # get rid of the leading .
                        self._images = []
                        self._real_fps = 100 / int(100 / self._fps)  # duration is always in 10 ms increments
                    elif extension == ".png" and not ("*" in video_path.stem):
                        self._video_out = "png"
                        self._images = []
                    elif extension.lower() in (".jpg", ".png", ".bmp", ".ico", ".tiff", ".gif", ".webp"):
                        if "*" in video_path.stem:
                            if video.count("*") > 1:
                                raise ValueError("more than one * found in " + video)
                            if "?" in video:
                                raise ValueError("? found in " + video)
                            self.video_name_format = video.replace("*", "{:06d}")
                            for file in video_path.parent.glob(video_path.name.replace("*", "??????")):
                                file.unlink()
                        else:
                            raise ValueError("incorrect video name (should contain a *) " + video)

                        self._video_out = "snapshots"

                    else:
                        if "+" in extension:
                            extension, codec = extension.split("+")
                            self._video_name = self._video_name[:-5]  # get rid of codec
                        else:
                            codec = "MJPG" if extension.lower() == ".avi" else "mp4v"
                        if PyDroid and extension.lower() != ".avi":
                            raise ValueError("PyDroid can only produce .avi videos, not " + extension)
                        can_video(try_only=False)
                        fourcc = cv2.VideoWriter_fourcc(*codec)
                        if video_path.is_file():
                            video_path.unlink()
                        self._video_name_temp = tempfile.NamedTemporaryFile(suffix=extension, delete=False).name
                        self._video_out = cv2.VideoWriter(self._video_name_temp, fourcc, self._fps, (self._video_width_real, self._video_height_real))
                        self.frame_number = 0
                        self.audio_segments = []
                        if self._audio is not None:
                            self._audio.start += self._t - self._audio.t0
                            self._audio.t0 = self.frame_number / self._fps
                            self.audio_segments.append(self._audio)
                            if Pythonista:
                                self._audio.player.pause()
                            if Windows:
                                self._audio.stop()

        if self._video:
            self.video_t = self._now

        if frame_changed:
            if g.animation_env is not None:
                g.animation_env._animate = self._animate
                if not Pythonista:
                    if g.animation_env.root is not None:  # for blind animation to work properly
                        if self._ui:
                            self.root.withdraw()
                        else:
                            g.animation_env.root.destroy()
                            g.animation_env.root = None
                g.animation_env = None

            if self._blind_animation:
                if self._animate:
                    if self._video != "":
                        with self.suppress_trace():
                            if self.env._yieldless:
                                self._blind_video_maker.activate(process="process_yieldless")
                            else:
                                self._blind_video_maker.activate(process="process_yielded")
                else:
                    self._blind_video_maker.cancel()
            else:
                if self._animate:
                    can_animate(try_only=False)  # install modules

                    g.animation_env = self
                    self._t = self._now  # for the call to set_start_animation
                    self._paused = False
                    self.set_start_animation()

                    if Pythonista:
                        if g.animation_scene is None:
                            g.animation_scene = AnimationScene(env=self)
                            scene.run(g.animation_scene, frame_interval=1, show_fps=False)

                    else:
                        if self.use_toplevel:
                            self.root = tkinter.Toplevel()
                        else:
                            self.root = tkinter.Tk()

                        if self._title:
                            self.root.title(self._title)
                        else:
                            self.root.overrideredirect(1)
                        self.root.geometry(f"+{self._position[0]}+{self._position[1]}")
                        self.root.protocol("WM_DELETE_WINDOW", self.on_closing)

                        self.root.bind("-", lambda self: g.animation_env.an_half())
                        self.root.bind("+", lambda self: g.animation_env.an_double())
                        self.root.bind("<space>", lambda self: g.animation_env.an_menu_go())
                        self.root.bind("s", lambda self: g.animation_env.an_single_step())
                        self.root.bind("<Control-c>", lambda self: g.animation_env.an_quit())

                        g.canvas = tkinter.Canvas(self.root, width=self._width, height=self._height)
                        g.canvas.configure(background=self.colorspec_to_hex("bg", False))
                        g.canvas.pack()
                        g.canvas_objects = []
                        g.canvas_object_overflow_image = None

                    self.uninstall_uios()  # this causes all ui objects to be (re)installed

                    if self._show_menu_buttons and not self._ui:
                        self.an_menu_buttons()
        if visible is not None:
            if Pythonista:
                raise ValueError("Pythonista does not support visible=False")
            if visible and self.root.wm_state() == "withdrawn":
                self.root.deiconify()
            if not visible and self.root.wm_state() != "withdrawn":
                self.root.withdraw()

    def video_close(self) -> None:
        """
        closes the current animation video recording, if any.
        """
        if self._video_out:
            if self._video_out in ("gif", "webp"):
                if self._images:
                    if self._video_pingpong:
                        self._images.extend(self._images[::-1])
                    if self._video_repeat == 1:  # in case of repeat == 1, loop should not be specified (otherwise, it might show twice)
                        if PythonInExcel or AnacondaCode:
                            with b64_file_handler(self._video_name, mode="b", result=_pie_result) as f:
                                self._images[0].save(
                                    f,
                                    disposal=2,
                                    save_all=True,
                                    append_images=self._images[1:],
                                    duration=round(1000 / self._real_fps),
                                    optimize=False,
                                    format="GIF",
                                )
                        else:
                            for _ in range(2): # normally runs only once
                                try:
                                    self._images[0].save(
                                        self._video_name,
                                        disposal=2,
                                        save_all=True,
                                        append_images=self._images[1:],
                                        duration=round(1000 / self._real_fps),
                                        optimize=False,
                                    )
                                    break
                                except ValueError:  # prevent bug in Python 3.13
                                    self._images=[image.convert("RGB") for image in self._images]

                    else:
                        if PythonInExcel or AnacondaCode:
                            with b64_file_handler(self._video_name, mode="b", result=_pie_result) as f:
                                self._images[0].save(
                                    f,
                                    disposal=2,
                                    save_all=True,
                                    append_images=self._images[1:],
                                    loop=self._video_repeat,
                                    duration=round(1000 / self._real_fps),
                                    optimize=False,
                                    format="GIF",
                                )
                        else:
                            for _ in range(2): # normally runs only once
                                try:

                                    self._images[0].save(
                                        self._video_name,
                                        disposal=2,
                                        save_all=True,
                                        append_images=self._images[1:],
                                        loop=self._video_repeat,
                                        duration=round(1000 / self._real_fps),
                                        optimize=False,
                                    )
                                except ValueError:  # prevent bug in Python 3.13
                                    self._images=[image.convert("RGB") for image in self._images]

                    del self._images
            elif self._video_out == "png":
                if self._video_pingpong:
                    self._images.extend(self._images[::-1])
                this_apng = _APNG(num_plays=self._video_repeat)
                for image in self._images:
                    with io.BytesIO() as png_file:
                        image.save(png_file, "PNG", optimize=True)
                        this_apng.append(_APNG.PNG.from_bytes(png_file.getvalue()), delay=1, delay_den=int(self.fps()))
                this_apng.save(self._video_name)
                del self._images

            elif self._video_out == "snapshots":
                pass
            else:
                self._video_out.release()
                if self.audio_segments:
                    if self._audio:
                        self.audio_segments[-1].t1 = self.frame_number / self._fps
                    self.add_audio()
                shutil.move(self._video_name_temp, self._video_name)

            self._video_out = None
            self._video = ""

    def _capture_image(self, mode="RGBA", video_mode="2d", include_topleft=True):
        if video_mode == "3d":
            if not self._animate3d:
                raise ValueError("video_mode=='3d', but animate3d is not True")

            width = self._width3d
            height = self._height3d

            # https://stackoverflow.com/questions/41126090/how-to-write-pyopengl-in-to-jpg-image
            gl.glPixelStorei(gl.GL_PACK_ALIGNMENT, 1)
            data = gl.glReadPixels(0, 0, width, height, gl.GL_RGB, gl.GL_UNSIGNED_BYTE)
            image = Image.frombytes("RGB", (width, height), data)
            image = image.transpose(Image.FLIP_TOP_BOTTOM)
        elif video_mode == "screen":
            image = ImageGrab.grab()
        else:
            an_objects = sorted(self.an_objects, key=lambda obj: (-obj.layer(self._t), obj.sequence))
            image = Image.new("RGBA", (self._width, self._height), self.colorspec_to_tuple("bg"))
            for ao in an_objects:
                ao.make_pil_image(self.t())
                if ao._image_visible and (include_topleft or not ao.getattr("in_topleft", False)):
                    image.paste(ao._image, (int(ao._image_x), int(self._height - ao._image_y - ao._image.size[1])), ao._image.convert("RGBA"))
        return image.convert(mode)

    def insert_frame(self, image: Any, number_of_frames: int = 1) -> None:
        """
        Insert image as frame(s) into video

        Parameters
        ----------
        image : Pillow image, str or Path object
            Image to be inserted

        nuumber_of_frames: int
            Number of 1/30 second long frames to be inserted
        """


        if self._video_out is None:
            raise ValueError("video not set")
        if isinstance(image, (Path, str)):
            image = Image.open(image)

        image = resize_with_pad(image, self._video_width_real, self._video_height_real)
        for _ in range(number_of_frames):
            if self._video_out == "gif":
                self._images.append(image.convert("RGBA"))
            elif self._video_out == "webp":
                self._images.append(image.convert("RGBA"))
            elif self._video_out == "png":
                self._images.append(image.convert("RGBA"))
            elif self._video_out == "snapshots":
                serialized_video_name = self.video_name_format.format(self.frame_number)
                if self._video_name.lower().endswith(".jpg"):
                    image.convert("RGB").save(serialized_video_name)
                else:
                    image.convert("RGBA").save(serialized_video_name)
                self.frame_number += 1
            else:
                image = image.convert("RGB")
                open_cv_image = cv2.cvtColor(numpy.array(image), cv2.COLOR_RGB2BGR)
                self._video_out.write(open_cv_image)


    def _save_frame(self):
        self._exclude_from_animation = "not in video"
        image = self._capture_image("RGBA", self._video_mode)
        self._exclude_from_animation = "only in video"
        self.insert_frame(image)

    def add_audio(self):
        if not Windows:
            return

        with tempfile.TemporaryDirectory() as tempdir:
            last_t = 0
            seq = 0
            for audio_segment in self.audio_segments:
                if hasattr(self, "debug_ffmpeg"):
                    print(
                        " audio_segment.filename = "
                        + str(audio_segment.filename)
                        + " .t0 = "
                        + str(audio_segment.t0)
                        + " .t1 = "
                        + str(audio_segment.t1)
                        + " .start = "
                        + str(audio_segment.start)
                    )
                end = min(audio_segment.duration, audio_segment.t1 - audio_segment.t0 + audio_segment.start)
                if end > audio_segment.start:
                    if audio_segment.t0 - last_t > 0:
                        command = (
                            "-f",
                            "lavfi",
                            "-i",
                            "aevalsrc=0:0::duration=" + str(audio_segment.t0 - last_t),
                            "-ab",
                            "128k",
                            tempdir + "\\temp" + str(seq) + ".mp3",
                        )
                        self.ffmpeg_execute(command)
                        seq += 1
                    command = (
                        "-ss",
                        str(audio_segment.start),
                        "-i",
                        audio_segment.filename,
                        "-t",
                        str(end - audio_segment.start),
                        "-c",
                        "copy",
                        tempdir + "\\temp" + str(seq) + ".mp3",
                    )
                    self.ffmpeg_execute(command)
                    seq += 1
                    last_t = audio_segment.t1

            if seq > 0:
                temp_filename = tempdir + "\\temp" + os.path.splitext(self._video_name)[1]
                shutil.copyfile(self._video_name_temp, temp_filename)

                with open(tempdir + "\\temp.txt", "w") as f:
                    f.write("\n".join("file '" + tempdir + "\\temp" + str(i) + ".mp3'" for i in range(seq)))
                if hasattr(self, "debug_ffmpeg"):
                    print("contents of temp.txt file")
                    with open(tempdir + "\\temp.txt", "r") as f:
                        print(f.read())
                command = (
                    "-i",
                    temp_filename,
                    "-f",
                    "concat",
                    "-safe",
                    "0",
                    "-i",
                    tempdir + "\\temp.txt",
                    "-map",
                    "0:v",
                    "-map",
                    "1:a",
                    "-c",
                    "copy",
                    self._video_name_temp,
                )
                self.ffmpeg_execute(command)

    def ffmpeg_execute(self, command):
        command = ("ffmpeg", "-y") + command + ("-loglevel", "quiet")
        if hasattr(self, "debug_ffmpeg"):
            print("command=" + str(command))
        try:
            subprocess.call(command, shell=False)
        except FileNotFoundError:
            raise FileNotFoundError("ffmpeg could not be loaded (refer to install procedure).")

    def uninstall_uios(self):
        for uio in self.ui_objects:
            uio.installed = False

    def x0(self, value: float = None) -> float:
        """
        x coordinate of lower left corner of animation

        Parameters
        ----------
        value : float
            new x coordinate

        Returns
        -------
        x coordinate of lower left corner of animation : float
        """
        if value is not None:
            self.animation_parameters(x0=value, animate=None)
        return self._x0

    def x1(self, value: float = None) -> float:
        """
        x coordinate of upper right corner of animation : float

        Parameters
        ----------
        value : float
            new x coordinate

            if not specified, no change

        Returns
        -------
        x coordinate of upper right corner of animation : float
        """
        if value is not None:
            self.animation_parameters(x1=value, animate=None)
        return self._x1

    def y0(self, value: float = None) -> float:
        """
        y coordinate of lower left corner of animation

        Parameters
        ----------
        value : float
            new y coordinate

            if not specified, no change

        Returns
        -------
        y coordinate of lower left corner of animation : float
        """
        if value is not None:
            self.animation_parameters(y0=value, animate=None)
        return self._y0

    def y1(self) -> float:
        """
        y coordinate of upper right corner of animation

        Returns
        -------
        y coordinate of upper right corner of animation : float

        Note
        ----
        It is not possible to set this value explicitely.
        """
        return self._y1

    def scale(self) -> float:
        """
        scale of the animation, i.e. width / (x1 - x0)

        Returns
        -------
        scale : float

        Note
        ----
        It is not possible to set this value explicitely.
        """
        return self._scale

    def user_to_screen_coordinates_x(self, userx: float) -> float:
        """
        converts a user x coordinate to a screen x coordinate

        Parameters
        ----------
        userx : float
            user x coordinate to be converted

        Returns
        -------
        screen x coordinate : float
        """
        return (userx - self._x0) * self._scale

    def user_to_screen_coordinates_y(self, usery: float) -> float:
        """
        converts a user x coordinate to a screen x coordinate

        Parameters
        ----------
        usery : float
            user y coordinate to be converted

        Returns
        -------
        screen y coordinate : float
        """
        return (usery - self._y0) * self._scale

    def user_to_screen_coordinates_size(self, usersize: float) -> float:
        """
        converts a user size to a value to be used with screen coordinates

        Parameters
        ----------
        usersize : float
            user size to be converted

        Returns
        -------
        value corresponding with usersize in screen coordinates : float
        """
        return usersize * self._scale

    def screen_to_user_coordinates_x(self, screenx: float) -> float:
        """
        converts a screen x coordinate to a user x coordinate

        Parameters
        ----------
        screenx : float
            screen x coordinate to be converted

        Returns
        -------
        user x coordinate : float
        """
        return self._x0 + screenx / self._scale

    def screen_to_user_coordinates_y(self, screeny: float) -> float:
        """
        converts a screen x coordinate to a user x coordinate

        Parameters
        ----------
        screeny : float
            screen y coordinate to be converted

        Returns
        -------
        user y coordinate : float
        """
        return self._y0 + screeny / self._scale

    def screen_to_user_coordinates_size(self, screensize: float) -> float:
        """
        converts a screen size to a value to be used with user coordinates

        Parameters
        ----------
        screensize : float
            screen size to be converted

        Returns
        -------
        value corresponding with screensize in user coordinates : float
        """
        return screensize / self._scale

    def width(self, value: int = None, adjust_x0_x1_y0: bool = False) -> int:
        """
        width of the animation in screen coordinates

        Parameters
        ----------
        value : int
            new width

            if not specified, no change

        adjust_x0_x1_y0 : bool
            if False (default), x0, x1 and y0 are not touched

            if True, x0 and y0 will be set to 0 and x1 will be set to the given width

        Returns
        -------
        width of animation : int
        """
        if value is not None:
            self.animation_parameters(width=value, animate=None)

        if adjust_x0_x1_y0:
            self._x0 = 0
            self._y0 = 0
            self._x1 = self._width
            self._scale = self._width / (self._x1 - self._x0)
            self._y1 = self._y0 + self._height / self._scale
        return self._width

    def height(self, value: int = None) -> int:
        """
        height of the animation in screen coordinates

        Parameters
        ----------
        value : int
            new height

            if not specified, no change

        Returns
        -------
        height of animation : int
        """
        if value is not None:
            self.animation_parameters(height=value, animate=None)
        return self._height

    def width3d(self, value: int = None) -> int:
        """
        width of the 3d animation in screen coordinates

        Parameters
        ----------
        value : int
            new 3d width

            if not specified, no change


        Returns
        -------
        width of 3d animation : int
        """
        if value is not None:
            self.animation_parameters(width3d=value, animate=None)
        return self._width3d

    def height3d(self, value: int = None) -> int:
        """
        height of the 3d animation in screen coordinates

        Parameters
        ----------
        value : int
            new 3d height

            if not specified, no change

        Returns
        -------
        height of 3d animation : int
        """
        if value is not None:
            self.animation_parameters(height3d=value, animate=None)
        return self._height3d

    def visible(self, value: bool = None) -> bool:
        """
        controls visibility of the animation window

        Parameters
        ----------
        value : bool
            if True, the animation window will be visible

            if False, the animation window will be hidden ('withdrawn')
            if None (default), no change

        Returns
        -------
        current visibility : bool
        """
        self.animation_parameters(visible=value)
        if Pythonista:
            return True
        else:
            return self.root.wm_state() != "withdrawn"

    def video_width(self, value: Union[int, str] = None):
        """
        width of the video animation in screen coordinates

        Parameters
        ----------
        value : int
            new width

            if not specified, no change


        Returns
        -------
        width of video animation : int
        """
        if value is not None:
            self.animation_parameters(video_width=value, animate=None)
        return self._video_width

    def video_height(self, value: Union[int, str] = None):
        """
        height of the video animation in screen coordinates

        Parameters
        ----------
        value : int
            new width

            if not specified, no change


        Returns
        -------
        height of video animation : int
        """
        if value is not None:
            self.animation_parameters(video_height=value, animate=None)
        return self._video_height

    def video_mode(self, value: str = None):
        """
        video_mode

        Parameters
        ----------
        value : int
            new video mode ("2d", "3d" or "screen")

            if not specified, no change

        Returns
        -------
        video_mode : int
        """
        if value is not None:
            self.animation_parameters(video_mode=value, animate=None)
        return self._video_mode

    def position(self, value: Any = None):
        """
        position of the animation window

        Parameters
        ----------
        value : tuple (x, y)
            new position

            if not specified, no change

        Returns
        -------
        position of animation window: tuple (x,y)
        """
        if value is not None:
            self.animation_parameters(position=value, animate=None)
        return self._position

    def position3d(self, value: Any = None):
        """
        position of the 3d animation window

        Parameters
        ----------
        value : tuple (x, y)
            new position

            if not specified, no change

        Returns
        -------
        position of th 3d animation window: tuple (x,y)

        Note
        ----
        This must be given before the 3d animation is started.
        """
        if value is not None:
            self.animation_parameters(position3d=value, animate=None)
        return self._position3d

    def title(self, value=None):
        """
        title of the canvas window

        Parameters
        ----------
        value : str
            new title

            if "", the title will be suppressed

            if not specified, no change

        Returns
        -------
        title of canvas window : str

        Note
        ----
        No effect for Pythonista
        """
        if value is not None:
            self.animation_parameters(title=value, animate=None)
        return self._title

    def background_color(self, value=None):
        """
        background_color of the animation

        Parameters
        ----------
        value : colorspec
            new background_color

            if not specified, no change

        Returns
        -------
        background_color of animation : colorspec
        """
        if value is not None:
            self.animation_parameters(background_color=value, animate=None)
        return self._background_color

    def background3d_color(self, value: ColorType = None):
        """
        background3d_color of the animation

        Parameters
        ----------
        value : colorspec
            new background_color

            if not specified, no change

        Returns
        -------
        background3d_color of animation : colorspec
        """
        if value is not None:
            self.animation_parameters(background3d_color=value)
        return self._background3d_color

    def foreground_color(self, value: ColorType = None):
        """
        foreground_color of the animation

        Parameters
        ----------
        value : colorspec
            new foreground_color

            if not specified, no change

        Returns
        -------
        foreground_color of animation : colorspec
        """
        if value is not None:
            self.animation_parameters(foreground_color=value, animate=None)
        return self._foreground_color

    def animate(self, value: Union[str, bool] = None):
        """
        animate indicator

        Parameters
        ----------
        value : bool
            new animate indicator

            if '?', animation will be set, if possible
            if not specified, no change

        Returns
        -------
        animate status : bool

        Note
        ----
        When the run is not issued, no action will be taken.
        """
        if value is not None:
            self.animation_parameters(animate=value)
        return self._animate

    def animate3d(self, value: bool = None):
        """
        animate3d indicator

        Parameters
        ----------
        value : bool
            new animate3d indicator

            if '?', 3D-animation will be set, if possible
            if not specified, no change

        Returns
        -------
        animate3d status : bool

        Note
        ----
        When the animate is not issued, no action will be taken.
        """

        if value is not None:
            self.animation_parameters(animate3d=value, animate=None)
        return self._animate3d

    def full_screen(self):
        """
        sets the animation window to full screen.

        Note
        ----
        This sets the title to "", so the title bar will be hidden

        Note
        ----
        x0 and y0 will be set to 0, x1 will be set to the screen width
        """
        self.width(self.screen_width(), adjust_x0_x1_y0=True)
        self.height(self.screen_height())
        self.title("")

    def modelname(self, value: str = None):
        """
        modelname

        Parameters
        ----------
        value : str
            new modelname

            if not specified, no change

        Returns
        -------
        modelname : str

        Note
        ----
        If modelname is the null string, nothing will be displayed.
        """
        if value is not None:
            self.animation_parameters(modelname=value, animate=None)
        return self._modelname

    def audio(self, filename: str):
        """
        Play audio during animation

        Parameters
        ----------
        filename : str
            name of file to be played (mp3 or wav files)

            if "", the audio will be stopped

            optionaly, a start time in seconds  may be given by appending the filename a > followed
            by the start time, like 'mytune.mp3>12.5'
            if not specified (None), no change

        Returns
        -------
        filename being played ("" if nothing is being played): str

        Note
        ----
        Only supported on Windows and Pythonista platforms. On other platforms, no effect.

        Variable bit rate mp3 files may be played incorrectly on Windows platforms.
        Try and use fixed bit rates (e.g. 128 or 320 kbps)
        """
        self.animation_parameters(audio=filename, animate=None)
        if self._audio:
            return self._audio.filename
        return ""

    def audio_speed(self, value: float = None):
        """
        Play audio during animation

        Parameters
        ----------
        value : float
            animation speed at which the audio should be played

            default: no change

            initially: 1

        Returns
        -------
        speed being played: int
        """
        self.animation_parameters(audio_speed=value, animate=None)
        return self._audio_speed

    def animate_debug(self, value: bool = None):
        """
        Animate debug

        Parameters
        ----------
        value : bool
            animate_debug

            default: no change

            initially: False

        Returns
        -------
        animate_debug : bool
        """
        self.animation_parameters(animate_debug=value, animate=None)
        return self._animate_debug

    class _Video:
        def __init__(self, env):
            self.env = env

        def __enter__(self):
            return self

        def __exit__(self, type, value, traceback):
            self.env.video_close()

    def is_videoing(self) -> bool:
        """
        video recording status

        returns
        -------
        video recording status : bool

            True, if video is being recorded

            False, otherwise
        """
        return bool(self._video)

    def video(self, value: Union[str, Iterable] = None) -> Any:
        """
        video name

        Parameters
        ----------
        value : str, list or tuple
            new video name

            for explanation see animation_parameters()

        Note
        ----
        If video is the null string or None, the video (if any) will be closed.

        The call can be also used as a context manager, which automatically opens and
        closes a file. E.g. ::

            with video("test.mp4"):
                env.run(100)
        """
        if value is None:
            value = ""
        self.animation_parameters(video=value, animate=None)
        return self._Video(env=self)

    def video_repeat(self, value: int = None) -> int:
        """
        video repeat

        Parameters
        ----------
        value : int
            new video repeat

            if not specified, no change

        Returns
        -------
        video repeat : int

        Note
        ----
        Applies only to gif animation.
        """
        if value is not None:
            self.animation_parameters(video_repeat=value, animate=None)
        return self._video_repeat

    def video_pingpong(self, value: bool = None) -> bool:
        """
        video pingpong

        Parameters
        ----------
        value : bool
            new video pingpong

            if not specified, no change

        Returns
        -------
        video pingpong : bool

        Note
        ----
        Applies only to gif animation.
        """
        if value is not None:
            self.animation_parameters(video_pingpong=value, animate=None)
        return self._video_pingpong

    def fps(self, value: float = None) -> float:
        """
        fps

        Parameters
        ----------
        value : float
            new fps

            if not specified, no change

        Returns
        -------
        fps : bool
        """
        if value is not None:
            self.animation_parameters(fps=value, animate=None)
        return self._fps

    def show_time(self, value: bool = None) -> bool:
        """
        show_time

        Parameters
        ----------
        value : bool
            new show_time

            if not specified, no change

        Returns
        -------
        show_time : bool
        """
        if value is not None:
            self.animation_parameters(show_time=value, animate=None)
        return self._show_time

    def show_fps(self, value: bool = None) -> bool:
        """
        show_fps

        Parameters
        ----------
        value : bool
            new show_fps

            if not specified, no change

        Returns
        -------
        show_fps : bool
        """
        if value is not None:
            self.animation_parameters(show_fps=value, animate=None)
        return self._show_fps

    def show_menu_buttons(self, value: bool = None) -> bool:
        """
        controls menu buttons

        Parameters
        ----------
        value : bool
            if True, menu buttons are shown

            if False, menu buttons are hidden

            if not specified, no change

        Returns
        -------
        show menu button status : bool
        """
        if value is not None:
            self.animation_parameters(show_menu_buttons=value, animate=None)
        return self._show_menu_buttons

    def maximum_number_of_bitmaps(self, value: int = None) -> int:
        """
        maximum number of bitmaps (applies to animation with tkinter only)

        Parameters
        ----------
        value : int
            new maximum_number_of_bitmaps

            if not specified, no change

        Returns
        -------
        maximum number of bitmaps : int
        """
        if value is not None:
            self.animation_parameters(maximum_number_of_bitmaps=value, animate=None)
        return self._maximum_number_of_bitmaps

    def synced(self, value: bool = None) -> bool:
        """
        synced

        Parameters
        ----------
        value : bool
            new synced

            if not specified, no change

        Returns
        -------
        synced : bool
        """
        if value is not None:
            self.animation_parameters(synced=value, animate=None)
        return self._synced

    def minimized(self, value: bool=None)-> bool:
        """
        minimized

        Parameters
        ----------
        value : bool
            if True, minimize the curent animation window

            if False, (re)show the current animation window

            if None (default): no action

        Returns
        -------
        current state of the animation window : bool
            True if current animation windows is minimized, False otherwise
        """
        if value is not None:
            if value:
                self.root.withdraw()
            else:
                self.root.deiconify()
        return not bool(self.root.winfo_viewable())

    def speed(self, value: float = None) -> float:
        """
        speed

        Parameters
        ----------
        value : float
            new speed

            if not specified, no change

        Returns
        -------
        speed : float
        """
        if value is not None:
            self.animation_parameters(speed=value, animate=None)
        return self._speed

    def peek(self) -> float:
        """
        returns the time of the next component to become current

        if there are no more events, peek will return inf

        Only for advance use with animation / GUI event loops
        """
        if len(self.env._pendingstandbylist) > 0:
            return self.env._now
        else:
            if self._event_list:
                return self._event_list[0][0]
            else:
                if self.end_on_empty_eventlist:
                    return self._now
                else:
                    return inf

    def main(self) -> "Component":
        """
        Returns
        -------
        the main component : Component
        """
        return self._main

    def now(self) -> float:
        """
        Returns
        -------
        the current simulation time : float
        """
        return self._now - self._offset

    def t(self) -> float:
        """
        Returns
        -------
        the current simulation animation time : float
        """
        return (self._t if self._animate else self._now) - self._offset

    def reset_now(self, new_now: float = 0) -> None:
        """
        reset the current time

        Parameters
        ----------
        new_now : float or distribution
            now will be set to new_now

            default: 0

            if distribution, the distribution is sampled

        Note
        ----
        Internally, salabim still works with the 'old' time. Only in the interface
        from and to the user program, a correction will be applied.

        The registered time in monitors will be always is the 'old' time.
        This is only relevant when using the time value in Monitor.xt() or Monitor.tx().
        """
        offset_before = self._offset
        new_now = self.spec_to_time(new_now)
        self._offset = self._now - new_now

        if self._trace:
            self.print_trace("", "", f"now reset to {new_now:0.3f}", f"(all times are reduced by {(self._offset - offset_before):0.3f})")

        if self._datetime0:
            self._datetime0 += datetime.timedelta(seconds=self.to_seconds(self._offset - offset_before))
            self.print_trace("", "", "", f"(t=0 ==> to {self.time_to_str(0)})")

    def trace(self, value: Union[bool, "filehandle"] = None) -> bool:
        """
        trace status

        Parameters
        ----------
        value : bool or file handle
            new trace status

            defines whether to trace or not

            if this a file handle (open for write), the trace output will be sent to this file.

            if omitted, no change

        Returns
        -------
        trace status : bool or file handle

        Note
        ----
        If you want to test the status, always include
        parentheses, like

            ``if env.trace():``
        """
        if value is not None:
            self._trace = value
            self._buffered_trace = False
            if self._ui:
                self._ui_window["-TRACE-"].update(value)
        return self._trace

    @contextlib.contextmanager
    def suppress_trace(self):
        """
        context manager to the trace temporarily

        Note
        ----
        To be used as ::

            with env.suppress_trace():
                ...
        """
        save_trace = self._trace
        self._trace = False
        yield
        self._trace = save_trace

    def suppress_trace_linenumbers(self, value: bool = None) -> bool:
        """
        indicates whether line numbers should be suppressed (False by default)

        Parameters
        ----------
        value : bool
            new suppress_trace_linenumbers status

            if omitted, no change

        Returns
        -------
        suppress_trace_linenumbers status : bool

        Note
        ----
        By default, suppress_trace_linenumbers is False, meaning that line numbers are shown in the trace.
        In order to improve performance, line numbers can be suppressed.
        """
        if value is not None:
            self._suppress_trace_linenumbers = value
        return self._suppress_trace_linenumbers

    def suppress_trace_standby(self, value: bool = None) -> bool:
        """
        suppress_trace_standby status

        Parameters
        ----------
        value : bool
            new suppress_trace_standby status

            if omitted, no change

        Returns
        -------
        suppress trace status : bool

        Note
        ----
        By default, suppress_trace_standby is True, meaning that standby components are
        (apart from when they become non standby) suppressed from the trace.

        If you set suppress_trace_standby to False, standby components are fully traced.
        """
        if value is not None:
            self._suppress_trace_standby = value
            self._buffered_trace = False
        return self._suppress_trace_standby

    def paused(self, value: bool = None) -> bool:
        """
        paused status

        Parameters
        ----------
        value : bool
            new paused status

            defines whether to be paused or not

            if omitted, no change

        Returns
        -------
        paused status : bool

        Note
        ----
        If you want to test the status, always include
        parentheses, like

            ``if env.paused():``
        """

        if value is not None:
            self._paused = bool(value)
            self.set_start_animation()

            if self._ui:
                self.set_pause_go_button()
                if not self.animate():
                    if value:
                        self.animate(True)
                        self._paused = True
                        if "-ANIMATE-" in self._ui_keys:
                            self.env._ui_window["-ANIMATE-"].update(False)  # this is required as the self.animate() also sets the value

        return self._paused

    def current_component(self) -> "Component":
        """
        Returns
        -------
        the current_component : Component
        """
        return self._current_component

    def run(self, duration: float = None, till: float = None, priority: Any = inf, urgent: bool = False, cap_now: bool = None):
        """
        start execution of the simulation

        Parameters
        ----------
        duration : float or distribution
            schedule with a delay of duration

            if 0, now is used

            if distribution, the distribution is sampled

        till : float or distribution
            schedule time

            if omitted, inf is assumed. See also note below

            if distribution, the distribution is sampled

        priority : float
            priority

            default: inf

            if a component has the same time on the event list, main is sorted accoring to
            the priority. The default value of inf makes that all components will finish before
            the run is ended

        urgent : bool
            urgency indicator

            if False (default), main will be scheduled
            behind all other components scheduled with the same time and priority

            if True, main will be scheduled
            in front of all components scheduled
            for the same time and priority

        cap_now : bool
            indicator whether times (till, duration) in the past are allowed. If, so now() will be used.
            default: sys.default_cap_now(), usualy False

        Note
        ----
        if neither till nor duration is specified, the main component will be reactivated at
        the time there are no more events on the eventlist, i.e. possibly not at inf.

        if you want to run till inf (particularly when animating), issue run(sim.inf)

        only issue run() from the main level
        """
        self.end_on_empty_eventlist = False
        extra = ""
        if till is None:
            if duration is None:
                scheduled_time = inf
                self.end_on_empty_eventlist = True
                extra = "*"
            else:
                if duration == inf:
                    scheduled_time = inf
                else:
                    duration = self.env.spec_to_duration(duration)
                    scheduled_time = self.env._now + duration
        else:
            till = self.env.spec_to_time(till)
            if duration is None:
                scheduled_time = till + self.env._offset
            else:
                raise ValueError("both duration and till specified")
        if self._yieldless:
            self._main.status._value = scheduled
            self._main._reschedule(scheduled_time, priority, urgent, "run", cap_now, extra=extra)
            self.running = False
            self.env._glet.switch()  # for proper handling of no events left run (?)
            self.running = True
            self.env._glet.switch()
        else:
            self._main.frame = _get_caller_frame()
            self._main.status._value = scheduled
            self._main._reschedule(scheduled_time, priority, urgent, "run", cap_now, extra=extra)
            self.running = True

        while self.running:
            if self._animate and not self._blind_animation:
                self.do_simulate_and_animate()
            else:
                self.do_simulate()
        if self.stopped:
            self.quit()
            if self._video:
                self.video_close()
            raise SimulationStopped

    def do_simulate(self):
        if self._blind_animation:
            while self.running:
                self.step()
        else:
            while g.in_draw:
                pass
            while self.running and not self._animate:
                self.step()

    def do_simulate_and_animate(self):
        if Pythonista:
            if self._animate3d:
                self.running = False
                raise ImportError("3d animation not supported under Pythonista")
            while self.running and self._animate:
                pass
            if self.stopped:
                raise SimulationStopped
        else:
            self.root.after(0, self.simulate_and_animate_loop)
            self.root.mainloop()
            if self._animate and self.running:
                if self._video:
                    self.video_close()
                raise SimulationStopped

    def simulate_and_animate_loop(self):
        while True:
            if self._animate3d and not self._gl_initialized:
                self.animation3d_init()
                self._camera_control()
                self.animation_start_clocktime = time.time()
                self.animation_start_time = self._t

            tick_start = time.time()

            if self._synced or self._video:  # video forces synced
                if self._video:
                    self._t = self.video_t
                else:
                    if self._paused:
                        self._t = self.animation_start_time
                    else:
                        self._t = self.animation_start_time + ((time.time() - self.animation_start_clocktime) * self._speed)

                while self.peek() < self._t:
                    self.step()
                    if not (self.running and self._animate):
                        if self.root is not None:
                            self.root.quit()
                        return
            else:
                if self._step_pressed or (not self._paused):
                    self.step()

                    if not self._current_component._suppress_pause_at_step:
                        self._step_pressed = False
                    self._t = self._now

            if not (self.running and self._animate):
                if self.root is not None:
                    self.root.quit()
                return

            if not self._paused:
                self.frametimes.append(time.time())

            t = self.t()

            self.animation_pre_tick(t)
            self.animation_pre_tick_sys(t)

            an_objects = sorted(self.an_objects, key=lambda obj: (-obj.layer(self._t), obj.sequence))

            canvas_objects_iter = iter(g.canvas_objects[:])
            co = next(canvas_objects_iter, None)
            overflow_image = None
            for ao in an_objects:
                ao.make_pil_image(t)
                if ao._image_visible:
                    if co is None:
                        if len(g.canvas_objects) >= self._maximum_number_of_bitmaps:
                            if overflow_image is None:
                                overflow_image = Image.new("RGBA", (int(self._width), int(self._height)), (0, 0, 0, 0))
                            overflow_image.paste(ao._image, (int(ao._image_x), int(self._height - ao._image_y - ao._image.size[1])), ao._image)
                            ao.canvas_object = None
                        else:
                            ao.im = ImageTk.PhotoImage(ao._image)
                            co1 = g.canvas.create_image(ao._image_x, self._height - ao._image_y, image=ao.im, anchor=tkinter.SW)
                            g.canvas_objects.append(co1)
                            ao.canvas_object = co1

                    else:
                        if ao.canvas_object == co:
                            if ao._image_ident != ao._image_ident_prev:
                                ao.im = ImageTk.PhotoImage(ao._image)
                                g.canvas.itemconfig(ao.canvas_object, image=ao.im)

                            if (ao._image_x != ao._image_x_prev) or (ao._image_y != ao._image_y_prev):
                                g.canvas.coords(ao.canvas_object, (ao._image_x, self._height - ao._image_y))

                        else:
                            ao.im = ImageTk.PhotoImage(ao._image)
                            ao.canvas_object = co
                            g.canvas.itemconfig(ao.canvas_object, image=ao.im)
                            g.canvas.coords(ao.canvas_object, (ao._image_x, self._height - ao._image_y))

                    co = next(canvas_objects_iter, None)
                else:
                    ao.canvas_object = None

            if overflow_image is None:
                if g.canvas_object_overflow_image is not None:
                    g.canvas.delete(g.canvas_object_overflow_image)
                    g.canvas_object_overflow_image = None

            else:
                im = ImageTk.PhotoImage(overflow_image)
                if g.canvas_object_overflow_image is None:
                    g.canvas_object_overflow_image = g.canvas.create_image(0, self._height, image=im, anchor=tkinter.SW)
                else:
                    g.canvas.itemconfig(g.canvas_object_overflow_image, image=im)

            if self._animate3d:
                self._exclude_from_animation = "*"  # makes that both video and non video over2d animation objects are shown
                an_objects3d = sorted(self.an_objects3d, key=lambda obj: (obj.layer(self._t), obj.sequence))
                for an in an_objects3d:
                    if an.keep(t):
                        if an.visible(t):
                            an.draw(t)
                    else:
                        an.remove()
                self._exclude_from_animation = "only in video"

            self.animation_post_tick(t)

            while co is not None:
                g.canvas.delete(co)
                g.canvas_objects.remove(co)
                co = next(canvas_objects_iter, None)

            for uio in self.ui_objects:
                if not uio.installed:
                    uio.install()

            for uio in self.ui_objects:
                if uio.type == "button":
                    thistext = uio.text()
                    if thistext != uio.lasttext:
                        uio.lasttext = thistext
                        uio.button.config(text=thistext)

            if self._video:
                if not self._paused:
                    self._save_frame()
                    self.video_t += self._speed / self._real_fps
                    self.frame_number += 1
            else:
                if self._synced:
                    tick_duration = time.time() - tick_start
                    if tick_duration < 1 / self._fps:
                        time.sleep(((1 / self._fps) - tick_duration) * 0.8)
                        # 0.8 compensation because of clock inaccuracy

            g.canvas.update()

    def snapshot(self, filename: str, video_mode: str = "2d") -> None:
        """
        Takes a snapshot of the current animated frame (at time = now()) and saves it to a file

        Parameters
        ----------
        filename : str
            file to save the current animated frame to.

            The following formats are accepted: .png, .jpg, .bmp, .ico, .gif, .webp and .tiff.
            Other formats are not possible.
            Note that, apart from .JPG files. the background may be semi transparent by setting
            the alpha value to something else than 255.

        video_mode : str
            specifies what to save

            if "2d" (default), the tkinter window will be saved

            if "3d", the OpenGL window will be saved (provided animate3d is True)

            if "screen" the complete screen will be saved (no need to be in animate mode)

            no scaling will be applied.
        """
        if video_mode not in ("2d", "3d", "screen"):
            raise ValueError("video_mode " + video_mode + " not recognized")
        can_animate(try_only=False)

        if video_mode == "screen" and ImageGrab is None:
            raise ValueError("video_mode='screen' not supported on this platform (ImageGrab does not exist)")

        filename_path = Path(filename)
        extension = filename_path.suffix.lower()
        if extension in (".png", ".gif", ".webp", ".bmp", ".ico", ".tiff"):
            mode = "RGBA"
        elif extension == ".jpg":
            mode = "RGB"
        else:
            raise ValueError("extension " + extension + "  not supported")
        if PythonInExcel or AnacondaCode:
            with b64_file_handler(str(filename), mode="b", result=_pie_result) as f:
                format = "jpeg" if extension == ".jpg" else extension[1:]
                self._capture_image(mode, video_mode).save(f, format=format)
        else:
            filename_path.parent.mkdir(parents=True, exist_ok=True)
            self._capture_image(mode, video_mode).save(str(filename))

    def modelname_width(self):
        if Environment.cached_modelname_width[0] != self._modelname:
            Environment.cached_modelname_width = [self._modelname, self.env.getwidth(self._modelname + " : a ", font="", fontsize=18)]
        return Environment.cached_modelname_width[1]

    def an_modelname(self) -> None:
        """
        function to show the modelname

        may be overridden to change the standard behaviour.
        """
        y = -68
        AnimateText(
            text=lambda: self._modelname + " : a",
            x=8,
            y=y,
            text_anchor="w",
            fontsize=18,
            font="",
            screen_coordinates=True,
            xy_anchor="nw",
            env=self,
            visible=lambda: self._modelname,
        )
        AnimateImage(
            image=lambda: self.salabim_logo(),
            x=lambda: self.modelname_width() + 10,
            y=y - 4,
            offsety=5,
            anchor="w",
            width=61,
            screen_coordinates=True,
            xy_anchor="nw",
            visible=lambda: self._modelname,
        )
        an = AnimateText(
            text=" model",
            x=lambda: self.modelname_width() + 72,
            y=y,
            text_anchor="w",
            fontsize=18,
            font="",
            screen_coordinates=True,
            xy_anchor="nw",
            visible=lambda: self._modelname,
            env=self,
        )

    def an_menu_buttons(self) -> None:
        """
        function to initialize the menu buttons

        may be overridden to change the standard behaviour.
        """
        self.remove_topleft_buttons()
        if self.colorspec_to_tuple("bg")[:-1] == self.colorspec_to_tuple("blue")[:-1]:
            fillcolor = "white"
            color = "blue"
        else:
            fillcolor = "blue"
            color = "white"

        uio = AnimateButton(x=38, y=-21, text="Menu", width=50, action=self.env.an_menu, env=self, fillcolor=fillcolor, color=color, xy_anchor="nw")

        uio.in_topleft = True

    def an_unsynced_buttons(self) -> None:
        """
        function to initialize the unsynced buttons

        may be overridden to change the standard behaviour.
        """
        self.remove_topleft_buttons()
        if self.colorspec_to_tuple("bg")[:-1] == self.colorspec_to_tuple("green")[:-1]:
            fillcolor = "lightgreen"
            color = "green"
        else:
            fillcolor = "green"
            color = "white"
        uio = AnimateButton(x=38, y=-21, text="Go", width=50, action=self.env.an_go, env=self, fillcolor=fillcolor, color=color, xy_anchor="nw")
        uio.in_topleft = True

        uio = AnimateButton(x=38 + 1 * 60, y=-21, text="Step", width=50, action=self.env.an_step, env=self, xy_anchor="nw")
        uio.in_topleft = True

        uio = AnimateButton(x=38 + 3 * 60, y=-21, text="Synced", width=50, action=self.env.an_synced_on, env=self, xy_anchor="nw")
        uio.in_topleft = True

        uio = AnimateButton(x=38 + 4 * 60, y=-21, text="Trace", width=50, action=self.env.an_trace, env=self, xy_anchor="nw")
        uio.in_topleft = True

        if self.colorspec_to_tuple("bg")[:-1] == self.colorspec_to_tuple("red")[:-1]:
            fillcolor = "lightsalmon"
            color = "white"
        else:
            fillcolor = "red"
            color = "white"

        uio = AnimateButton(x=38 + 5 * 60, y=-21, text="Stop", width=50, action=self.env.an_quit, env=self, fillcolor=fillcolor, color=color, xy_anchor="nw")
        uio.in_topleft = True

        ao = AnimateText(x=38 + 3 * 60, y=-35, text=self.syncedtext, text_anchor="N", fontsize=15, font="", screen_coordinates=True, xy_anchor="nw")
        ao.in_topleft = True

        ao = AnimateText(x=38 + 4 * 60, y=-35, text=self.tracetext, text_anchor="N", fontsize=15, font="", screen_coordinates=True, xy_anchor="nw")
        ao.in_topleft = True

    def an_synced_buttons(self) -> None:
        """
        function to initialize the synced buttons

        may be overridden to change the standard behaviour.
        """
        self.remove_topleft_buttons()
        if self.colorspec_to_tuple("bg")[:-1] == self.colorspec_to_tuple("green")[:-1]:
            fillcolor = "lightgreen"
            color = "green"
        else:
            fillcolor = "green"
            color = "white"

        uio = AnimateButton(x=38, y=-21, text="Go", width=50, action=self.env.an_go, env=self, fillcolor=fillcolor, color=color, xy_anchor="nw")
        uio.in_topleft = True

        uio = AnimateButton(x=38 + 1 * 60, y=-21, text="/2", width=50, action=self.env.an_half, env=self, xy_anchor="nw")
        uio.in_topleft = True

        uio = AnimateButton(x=38 + 2 * 60, y=-21, text="*2", width=50, action=self.env.an_double, env=self, xy_anchor="nw")
        uio.in_topleft = True

        uio = AnimateButton(x=38 + 3 * 60, y=-21, text="Synced", width=50, action=self.env.an_synced_off, env=self, xy_anchor="nw")
        uio.in_topleft = True

        uio = AnimateButton(x=38 + 4 * 60, y=-21, text="Trace", width=50, action=self.env.an_trace, env=self, xy_anchor="nw")
        uio.in_topleft = True

        if self.colorspec_to_tuple("bg") == self.colorspec_to_tuple("red"):
            fillcolor = "lightsalmon"
            color = "white"
        else:
            fillcolor = "red"
            color = "white"
        uio = AnimateButton(x=38 + 5 * 60, y=-21, text="Stop", width=50, action=self.env.an_quit, env=self, fillcolor=fillcolor, color=color, xy_anchor="nw")
        uio.in_topleft = True

        ao = AnimateText(
            x=38 + 1.5 * 60, y=-35, text=self.speedtext, textcolor="fg", text_anchor="N", fontsize=15, font="", screen_coordinates=True, xy_anchor="nw"
        )
        ao.in_topleft = True

        ao = AnimateText(x=38 + 3 * 60, y=-35, text=self.syncedtext, text_anchor="N", fontsize=15, font="", screen_coordinates=True, xy_anchor="nw")
        ao.in_topleft = True

        ao = AnimateText(x=38 + 4 * 60, y=-35, text=self.tracetext, text_anchor="N", fontsize=15, font="", screen_coordinates=True, xy_anchor="nw")
        ao.in_topleft = True

    def remove_topleft_buttons(self):
        for uio in self.ui_objects[:]:
            if getattr(uio, "in_topleft", False):
                uio.remove()

        for ao in self.an_objects.copy():
            if getattr(ao, "in_topleft", False):
                ao.remove()

    def an_clocktext(self) -> None:
        """
        function to initialize the system clocktext

        called by run(), if animation is True.

        may be overridden to change the standard behaviour.
        """
        ao = AnimateText(
            x=-30 if Pythonista else 0,
            y=-11 if Pythonista else 0,
            textcolor="fg",
            text=self.clocktext,
            fontsize=15,
            font="mono",
            text_anchor="ne",
            screen_coordinates=True,
            xy_anchor="ne",
            env=self,
        )
        ao.text = self.clocktext

    def an_half(self):
        self._speed /= 2
        self.set_start_animation()

    def an_double(self):
        self._speed *= 2
        self.set_start_animation()

    def an_go(self):
        self.paused(False)
        if self._synced:
            self.set_start_animation()
        else:
            self._step_pressed = True  # force to next event
        self.an_menu_buttons()

    def an_quit(self):
        self._animate = False
        self.running = False
        self.stopped = True
        if not Pythonista:
            if self.root is not None:  # for blind animation to work properly
                self.root.destroy()
                self.root = None
        self.quit()

    def quit(self):
        if g.animation_env is not None:
            g.animation_env.animation_parameters(animate=False, video="")  # stop animation
        if Pythonista:
            if g.animation_scene is not None:
                g.animation_scene.view.close()
        try:
            self.stop_ui()
        except AttributeError:  # in case start_ui is not (yet) called
            ...

    def an_trace(self):
        self._trace = not self._trace

    def an_synced_on(self):
        self._synced = True
        self.an_synced_buttons()

    def an_synced_off(self):
        self._synced = False
        self.an_unsynced_buttons()

    def an_step(self):
        self._step_pressed = True

    def an_single_step(self):
        self._step_pressed = True
        self.step()
        self.paused(True)
        self._t = self._now
        self.set_start_animation()

    def an_menu_go(self):
        if self._paused:
            self.an_go()
        else:
            self.an_menu()

    def an_menu(self):
        self.paused(True)
        self.set_start_animation()
        if self._synced:
            self.an_synced_buttons()
        else:
            self.an_unsynced_buttons()

    def clocktext(self, t):
        s = ""
        if self._synced and (not self._paused) and self._show_fps:
            if len(self.frametimes) >= 2:
                fps = (len(self.frametimes) - 1) / (self.frametimes[-1] - self.frametimes[0])
            else:
                fps = 0
            s += f"fps={fps:.1f}"
        if self._show_time:
            if s != "":
                s += " "
            s += "t=" + self.time_to_str(t).lstrip()
        return s

    def tracetext(self, t):
        if self._trace:
            return "= on"
        else:
            return "= off"

    def syncedtext(self, t):
        if self._synced:
            return "= on"
        else:
            return "= off"

    def speedtext(self, t):
        return f"speed = {self._speed:.3f}"

    def set_start_animation(self):
        self.frametimes = collections.deque(maxlen=30)
        self.animation_start_time = self._t
        self.animation_start_clocktime = time.time()
        if self._audio:
            start_time = self._t - self._audio.t0 + self._audio.start
            if Pythonista:
                if self._animate and self._synced and (not self._video):
                    if self._paused:
                        self._audio.player.pause()
                    else:
                        if self._speed == self._audio_speed:
                            self._audio.player.current_time = start_time
                            self._audio.player.play()
            if Windows:
                if self._animate and self._synced and (not self._video):
                    if self._paused:
                        self._audio.pause()
                    else:
                        if self._speed == self._audio_speed:
                            if start_time < self._audio.duration:
                                self._audio.play(start=start_time)

    def xy_anchor_to_x(self, xy_anchor, screen_coordinates, over3d=False, retina_scale=False):
        scale = self.retina if (retina_scale and self.retina > 1) else 1
        if over3d:
            width = self._width3d
        else:
            width = self._width
        if xy_anchor in ("nw", "w", "sw"):
            if screen_coordinates:
                return 0
            else:
                return self._x0 / scale

        if xy_anchor in ("n", "c", "center", "s"):
            if screen_coordinates:
                return (width / 2) / scale
            else:
                return ((self._x0 + self._x1) / 2) / scale

        if xy_anchor in ("ne", "e", "se", ""):
            if screen_coordinates:
                return width / scale
            else:
                return self._x1 / scale

        raise ValueError("incorrect xy_anchor", xy_anchor)

    def xy_anchor_to_y(self, xy_anchor, screen_coordinates, over3d=False, retina_scale=False):
        scale = self.retina if (retina_scale and self.retina > 1) else 1
        if over3d:
            height = self._height3d
        else:
            height = self._height

        if xy_anchor in ("nw", "n", "ne"):
            if screen_coordinates:
                return height / scale
            else:
                return self._y1 / scale

        if xy_anchor in ("w", "c", "center", "e"):
            if screen_coordinates:
                return (height / 2) / scale
            else:
                return ((self._y0 + self._y1) / 2) / scale

        if xy_anchor in ("sw", "s", "se", ""):
            if screen_coordinates:
                return 0
            else:
                return self._y0 / scale

        raise ValueError("incorrect xy_anchor", xy_anchor)

    def salabim_logo(self):
        if "salabim_logo_200" in globals():  # test for availabiitly, because of minimized version
            return salabim_logo_200()
        else:
            return Image.new("RGBA", (1, 1), (0, 0, 0, 0))

    def colorspec_to_tuple(self, colorspec: ColorType) -> Tuple:
        """
        translates a colorspec to a tuple

        Parameters
        ----------
        colorspec: tuple, list or str
            ``#rrggbb`` ==> alpha = 255 (rr, gg, bb in hex)

            ``#rrggbbaa`` ==> alpha = aa (rr, gg, bb, aa in hex)

            ``colorname`` ==> alpha = 255

            ``(colorname, alpha)``

            ``(r, g, b)`` ==> alpha = 255

            ``(r, g, b, alpha)``

            ``"fg"`` ==> foreground_color

            ``"bg"`` ==> background_color

        Returns
        -------
        (r, g, b, a)
        """
        if colorspec is None:
            colorspec = ""
        if colorspec == "fg":
            colorspec = self.colorspec_to_tuple(self._foreground_color)
        elif colorspec == "bg":
            colorspec = self.colorspec_to_tuple(self._background_color)
        if isinstance(colorspec, (tuple, list)):
            if len(colorspec) == 2:
                c = self.colorspec_to_tuple(colorspec[0])
                return (c[0], c[1], c[2], colorspec[1])
            elif len(colorspec) == 3:
                return (colorspec[0], colorspec[1], colorspec[2], 255)
            elif len(colorspec) == 4:
                return tuple(colorspec)
        else:
            if (colorspec != "") and (colorspec[0]) == "#":
                if len(colorspec) == 7:
                    return (int(colorspec[1:3], 16), int(colorspec[3:5], 16), int(colorspec[5:7], 16), 255)
                elif len(colorspec) == 9:
                    return (int(colorspec[1:3], 16), int(colorspec[3:5], 16), int(colorspec[5:7], 16), int(colorspec[7:9], 16))
            else:
                s = colorspec.split("#")
                if len(s) == 2:
                    alpha = s[1]
                    colorspec = s[0]
                else:
                    alpha = "FF"
                try:
                    colorhex = colornames()[colorspec.replace(" ", "").lower()]
                    if len(colorhex) == 7:
                        colorhex = colorhex + alpha
                    return self.colorspec_to_tuple(colorhex)
                except KeyError:
                    pass

        raise ValueError("wrong color specification: " + str(colorspec))

    def colorinterpolate(self, t: float, t0: float, t1: float, v0: Any, v1: Any) -> Any:
        """
        does linear interpolation of colorspecs

        Parameters
        ----------
        t : float
            value to be interpolated from

        t0: float
            f(t0)=v0

        t1: float
            f(t1)=v1

        v0: colorspec
            f(t0)=v0

        v1: colorspec
            f(t1)=v1

        Returns
        -------
        linear interpolation between v0 and v1 based on t between t0 and t : colorspec

        Note
        ----
        Note that no extrapolation is done, so if t<t0 ==> v0  and t>t1 ==> v1

        This function is heavily used during animation
        """
        if v0 == v1:
            return v0
        if t1 == inf:
            return v0
        if t0 == t1:
            return v1
        vt0 = self.colorspec_to_tuple(v0)
        vt1 = self.colorspec_to_tuple(v1)
        return tuple(int(c) for c in interpolate(t, t0, t1, vt0, vt1))

    def color_interp(self, x: float, xp: Iterable, fp: Iterable):
        """
        linear interpolation of a color

        Parameters
        ----------
        x : float
            target x-value

        xp : list of float, tuples or lists
            values on the x-axis

        fp : list of colorspecs
            values on the y-axis

            should be same length as xp

        Returns
        -------
        interpolated color value : tuple

        Notes
        -----
        If x < xp[0], fp[0] will be returned

        If x > xp[-1], fp[-1] will be returned

        """
        fp_resolved = [self.colorspec_to_tuple(el) for el in fp]
        return tuple(map(int, interp(x, xp, fp_resolved)))

    def colorspec_to_hex(self, colorspec, withalpha=True):
        v = self.colorspec_to_tuple(colorspec)
        if withalpha:
            return f"#{int(v[0]):02x}{int(v[1]):02x}{int(v[2]):02x}{int(v[3]):02x}"
        else:
            return f"#{int(v[0]):02x}{int(v[1]):02x}{int(v[2]):02x}"

    def colorspec_to_gl_color(self, colorspec):
        color_tuple = self.colorspec_to_tuple(colorspec)
        return (color_tuple[0] / 255, color_tuple[1] / 255, color_tuple[2] / 255)

    def colorspec_to_gl_color_alpha(self, colorspec):
        color_tuple = self.colorspec_to_tuple(colorspec)
        return ((color_tuple[0] / 255, color_tuple[1] / 255, color_tuple[2] / 255), color_tuple[3])

    def pythonistacolor(self, colorspec):
        c = self.colorspec_to_tuple(colorspec)
        return (c[0] / 255, c[1] / 255, c[2] / 255, c[3] / 255)

    def is_dark(self, colorspec: ColorType) -> bool:
        """
        Arguments
        ---------
        colorspec : colorspec
            color to check

        Returns
        -------
        : bool
            True, if the colorspec is dark (rather black than white)

            False, if the colorspec is light (rather white than black

            if colorspec has alpha=0 (total transparent), the background_color will be tested
        """
        rgba = self.colorspec_to_tuple(colorspec)
        if rgba[3] == 0:
            return self.is_dark(self.colorspec_to_tuple(("bg", 255)))
        luma = ((0.299 * rgba[0]) + (0.587 * rgba[1]) + (0.114 * rgba[2])) / 255
        if luma > 0.5:
            return False
        else:
            return True

    def getwidth(self, text, font, fontsize, screen_coordinates=False):
        if not screen_coordinates:
            fontsize = fontsize * self._scale
        f, heightA = getfont(font, fontsize)
        if text == "":  # necessary because of bug in PIL >= 4.2.1
            thiswidth, thisheight = (0, 0)
        else:
            thiswidth, thisheight = f.getbbox(text)[2:]
        if screen_coordinates:
            return thiswidth
        else:
            return thiswidth / self._scale

    def getheight(self, font, fontsize, screen_coordinates=False):
        if not screen_coordinates:
            fontsize = fontsize * self._scale
        f, heightA = getfont(font, fontsize)
        thiswidth, thisheight = f.getbbox("Ap")[2:]
        if screen_coordinates:
            return thisheight
        else:
            return thisheight / self._scale

    def getfontsize_to_fit(self, text, width, font, screen_coordinates=False):
        if not screen_coordinates:
            width = width * self._scale

        lastwidth = 0
        for fontsize in range(1, 300):
            f, heightA = getfont(font, fontsize)
            thiswidth, thisheight = f.getbbox(text)[2:]
            if thiswidth > width:
                break
            lastwidth = thiswidth
        fontsize = interpolate(width, lastwidth, thiswidth, fontsize - 1, fontsize)
        if screen_coordinates:
            return fontsize
        else:
            return fontsize / self._scale

    def name(self, value: str = None) -> str:
        """
        Parameters
        ----------
        value : str
            new name of the environment
            if omitted, no change

        Returns
        -------
        Name of the environment : str

        Note
        ----
        base_name and sequence_number are not affected if the name is changed
        """
        if value is not None:
            self._name = value
        return self._name

    def base_name(self) -> str:
        """
        returns the base name of the environment (the name used at initialization)
        """
        return getattr(self, "_base_name", self._name)

    def sequence_number(self) -> int:
        """
        Returns
        -------
        sequence_number of the environment : int
            (the sequence number at initialization)

            normally this will be the integer value of a serialized name.

            Non serialized names (without a dot or a comma at the end)
            will return 1)
        """
        return getattr(self, "_sequence_number", 1)

    def get_time_unit(self, template: str = None) -> str:
        """
        gets time unit

        Parameters
        ----------
        template : str
            normally only used in UI functions

            default: just return time_unit (including n/a)

            if "d", time_unit as duration

            if "t", time_unit as time

            if "(d)", time_unit as (duration)

            if "(t)", time_unit as (time)

            Note that n/a is suppressed and an extra space is added at the front
            if result is not the null strinf

        Returns
        -------
        Current time unit dimension (default "n/a") : str
        """
        if template is None:
            return self._time_unit_name
        if template not in "d t (d) (t)".split():
            raise ValueError

        result = ""
        if "t" in template and self._datetime0:
            if "(" in template:
                result = "yyyy-mm-dd"
        else:
            if self._time_unit_name != "n/a":
                result = self._time_unit_name
        if result:
            if "(" in template:
                result = f" ({result})"
            else:
                result = f" {result}"
        return result

    def years(self, t: float) -> float:
        """
        convert the given time in years to the current time unit

        Parameters
        ----------
        t : float or distribution
            time in years

            if distribution, the distribution is sampled

        Returns
        -------
        time in years, converted to the current time_unit : float
        """
        self._check_time_unit_na()
        if callable(t):
            t = t()
        return t * 86400 * 365 * self._time_unit

    def weeks(self, t: float) -> float:
        """
        convert the given time in weeks to the current time unit

        Parameters
        ----------
        t : float or distribution
            time in weeks

            if distribution, the distribution is sampled

        Returns
        -------
        time in weeks, converted to the current time_unit : float
        """
        self._check_time_unit_na()
        if callable(t):
            t = t()
        return t * 86400 * 7 * self._time_unit

    def days(self, t: float) -> float:
        """
        convert the given time in days to the current time unit

        Parameters
        ----------
        t : float or distribution
            time in days

            if distribution, the distribution is sampled

        Returns
        -------
        time in days, converted to the current time_unit : float
        """
        self._check_time_unit_na()
        if callable(t):
            t = t()
        return t * 86400 * self._time_unit

    def hours(self, t: float) -> float:
        """
        convert the given time in hours to the current time unit

        Parameters
        ----------
        t : float or distribution
            time in hours

            if distribution, the distribution is sampled

        Returns
        -------
        time in hours, converted to the current time_unit : float
        """
        self._check_time_unit_na()
        if callable(t):
            t = t()
        return t * 3600 * self._time_unit

    def minutes(self, t: float) -> float:
        """
        convert the given time in minutes to the current time unit

        Parameters
        ----------
        t : float or distribution
            time in minutes

            if distribution, the distribution is sampled

        Returns
        -------
        time in minutes, converted to the current time_unit : float
        """
        self._check_time_unit_na()
        if callable(t):
            t = t()
        return t * 60 * self._time_unit

    def seconds(self, t: float) -> float:
        """
        convert the given time in seconds to the current time unit

        Parameters
        ----------
        t : float or distribution
            time in seconds

            if distribution, the distribution is sampled

        Returns
        -------
        time in seconds, converted to the current time_unit : float
        """
        self._check_time_unit_na()
        if callable(t):
            t = t()
        return t * self._time_unit

    def milliseconds(self, t: float) -> float:
        """
        convert the given time in milliseconds to the current time unit

        Parameters
        ----------
        t : float or distribution
            time in milliseconds

            if distribution, the distribution is sampled

        Returns
        -------
        time in milliseconds, converted to the current time_unit : float
        """
        self._check_time_unit_na()
        if callable(t):
            t = t()
        return t * 1e-3 * self._time_unit

    def microseconds(self, t: float) -> float:
        """
        convert the given time in microseconds to the current time unit

        Parameters
        ----------
        t : float or distribution
            time in microseconds

            if distribution, the distribution is sampled

        Returns
        -------
        time in microseconds, converted to the current time_unit : float
        """
        self._check_time_unit_na()
        if callable(t):
            t = t()
        return t * 1e-6 * self._time_unit

    def to_time_unit(self, time_unit: str, t: float) -> float:
        """
        convert time t to the time_unit specified

        Parameters
        ----------
        time_unit : str
            Supported time_units:

            "years", "weeks", "days", "hours", "minutes", "seconds", "milliseconds", "microseconds"

        t : float or distribution
            time to be converted

            if distribution, the distribution is sampled

        Returns
        -------
        Time t converted to the time_unit specified : float
        """
        self._check_time_unit_na()
        if callable(t):
            t = t()
        return t * _time_unit_lookup(time_unit) / self._time_unit

    def to_years(self, t: float) -> float:
        """
        convert time t to years

        Parameters
        ----------
        t : float or distribution
            time to be converted

            if distribution, the distribution is sampled

        Returns
        -------
        Time t converted to years : float
        """
        return self.to_time_unit("years", t)

    def to_weeks(self, t: float) -> float:
        """
        convert time t to weeks

        Parameters
        ----------
        t : float or distribution
            time to be converted

            if distribution, the distribution is sampled

        Returns
        -------
        Time t converted to weeks : float
        """
        return self.to_time_unit("weeks", t)

    def to_days(self, t: float) -> float:
        """
        convert time t to days

        Parameters
        ----------
        t : float or distribution
            time to be converted

            if distribution, the distribution is sampled

        Returns
        -------
        Time t converted to days : float
        """
        return self.to_time_unit("days", t)

    def to_hours(self, t: float) -> float:
        """
        convert time t to hours

        Parameters
        ----------
        t : float or distribution
            time to be converted

            if distribution, the distribution is sampled

        Returns
        -------
        Time t converted to hours : float
        """
        return self.to_time_unit("hours", t)

    def to_minutes(self, t: float) -> float:
        """
        convert time t to minutes

        Parameters
        ----------
        t : float or distribution
            time to be converted

            if distribution, the distribution is sampled

        Returns
        -------
        Time t converted to minutes : float
        """
        return self.to_time_unit("minutes", t)

    def to_seconds(self, t: float) -> float:
        """
        convert time t to seconds

        Parameters
        ----------
        t : float or distribution
            time to be converted

            if distribution, the distribution is sampled

        Returns
        -------
        Time t converted to seconds : float
        """
        return self.to_time_unit("seconds", t)

    def to_milliseconds(self, t: float) -> float:
        """
        convert time t to milliseconds

        Parameters
        ----------
        t : float or distribution
            time to be converted

            if distribution, the distribution is sampled

        Returns
        -------
        Time t converted to milliseconds : float
        """
        return self.to_time_unit("milliseconds", t)

    def to_microseconds(self, t: float) -> float:
        """
        convert time t to microseconds

        Parameters
        ----------
        t : float or distribution
            time to be converted

            if distribution, the distribution is sampled

        Returns
        -------
        Time t converted to microseconds : float
        """
        return self.to_time_unit("microseconds", t)

    def _check_time_unit_na(self):
        if self._time_unit is None:
            raise AttributeError("time_unit is not available")

    def print_trace_header(self) -> None:
        """
        print a (two line) header line as a legend

        also the legend for line numbers will be printed

        note that the header is only printed if trace=True
        """
        len_s1 = len(self.time_to_str(0))
        self.print_trace((len_s1 - 4) * " " + "time", "current component", "action", "information", "" if (PythonInExcel or AnacondaCode) else "line#")
        self.print_trace(len_s1 * "-", 20 * "-", 35 * "-", 48 * "-", "" if (PythonInExcel or AnacondaCode) else 6 * "-")
        for ref in range(len(self._source_files)):
            for fullfilename, iref in self._source_files.items():
                if ref == iref:
                    self._print_legend(iref)

    def _print_legend(self, ref):
        if PythonInExcel or AnacondaCode:
            return
        if ref:
            s = "line numbers prefixed by " + chr(ord("A") + ref - 1) + " refer to"
        else:
            s = "line numbers refers to"
        for fullfilename, iref in self._source_files.items():
            if ref == iref:
                self.print_trace("", "", s, (os.path.basename(fullfilename)), "")
                break

    def _frame_to_lineno(self, frame, add_filename=False):
        frameinfo = inspect.getframeinfo(frame)
        if add_filename:
            return str(frameinfo.lineno) + " in " + os.path.basename(frameinfo.filename)
        return self.filename_lineno_to_str(frameinfo.filename, frameinfo.lineno)

    def filename_lineno_to_str(self, filename, lineno):
        if PythonInExcel or AnacondaCode:
            return "n/a"
        if Path(filename).name == Path(__file__).name:  # internal salabim address
            return "n/a"
        ref = self._source_files.get(filename)
        new_entry = False
        if ref is None:
            if self._source_files:
                ref = len(self._source_files)
            self._source_files[filename] = ref
            new_entry = True
        if ref == 0:
            pre = ""
        else:
            pre = chr(ref + ord("A") - 1)
        if new_entry:
            self._print_legend(ref)
        return rpad(pre + str(lineno), 5)

    def print_trace(self, s1: str = "", s2: str = "", s3: str = "", s4: str = "", s0: str = None, _optional: bool = False):
        """
        prints a trace line

        Parameters
        ----------
        s1 : str
            part 1 (usually formatted  now), padded to 10 characters

        s2 : str
            part 2 (usually only used for the compoent that gets current), padded to 20 characters

        s3 : str
            part 3, padded to 35 characters

        s4 : str
            part 4

        s0 : str
            part 0. if omitted, the line number from where the call was given will be used at
            the start of the line. Otherwise s0, left padded to 7 characters will be used at
            the start of the line.

        _optional : bool
            for internal use only. Do not set this flag!

        Note
        ----
        if self.trace is False, nothing is printed

        if the current component's suppress_trace is True, nothing is printed

        """
        len_s1 = len(self.time_to_str(0))
        if self._trace:
            if not (hasattr(self, "_current_component") and self._current_component._suppress_trace):
                if s0 is None:
                    if self._suppress_trace_linenumbers:
                        s0 = ""
                    else:
                        # stack = inspect.stack()
                        # filename0 = inspect.getframeinfo(stack[0][0]).filename
                        # for i in range(len(inspect.stack())):
                        #     frame = stack[i][0]
                        #     if filename0 != inspect.getframeinfo(frame).filename:
                        #         break

                        s0 = un_na(self._frame_to_lineno(_get_caller_frame()))
                self.last_s0 = s0
                line = pad(s0, 7) + pad(s1, len_s1) + " " + pad(s2, 20) + " " + pad(s3, max(len(s3), 36)) + " " + s4.strip()
                if _optional:
                    self._buffered_trace = line
                else:
                    if self._buffered_trace:
                        if hasattr(self._trace, "write"):
                            print(self._buffered_trace, file=self._trace)
                        else:
                            print(self._buffered_trace)
                        logging.debug(self._buffered_trace)
                        self._buffered_trace = False
                    if hasattr(self._trace, "write"):
                        print(line, file=self._trace)
                    else:
                        print(line)
                    logging.debug(line)

    def time_to_str(self, t: float) -> str:
        """
        Parameters
        ----------
        t : float
            time to be converted to string in trace and animation

        Returns
        -------
        t in required format : str
            default: f"{t:10.3f}" if datetime0 is False

            or date in the format "Day YYYY-MM-DD hh:mm:dd" otherwise

        Note
        ----
        May be overrridden. Make sure that the method always returns the same length!
        """
        if self._datetime0:
            if t == inf:
                return f"{'inf':23}"
            date = self.t_to_datetime(t)
            return f"{('Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun')[date.weekday()]} {date.strftime('%Y-%m-%d %H:%M:%S')}"
        return f"{t:10.3f}"

    def duration_to_str(self, duration: float) -> str:
        """
        Parameters
        ----------
        duration : float
            duration to be converted to string in trace

        Returns
        -------
        duration in required format : str
            default: f"{duration:.3f}" if datetime0 is False
            or duration in the format "hh:mm:dd" or "d hh:mm:ss"

        Note
        ----
        May be overrridden.
        """
        if self._datetime0:
            if duration == inf:
                return "inf"
            duration = self.to_seconds(duration)
            days, rem = divmod(duration, 86400)
            hours, rem = divmod(rem, 3600)
            minutes, seconds = divmod(rem, 60)
            if days:
                return f"{int(days)} {int(hours):02d}:{int(minutes):02d}:{int(seconds):02d}"
            else:
                return f"{int(hours):02d}:{int(minutes):02d}:{int(seconds):02d}"
        return f"{duration:.3f}"

    def datetime_to_t(self, datetime: datetime.datetime) -> float:
        """
        Parameters
        ----------
        datetime : datetime.datetime

        Returns
        -------
        datetime translated to simulation time in the current time_unit : float

        Raises
        ------
        ValueError
            if datetime0 is False
        """
        if self._datetime0:
            return self.seconds((datetime - self._datetime0).total_seconds())
        raise ValueError("datetime_to_t only possible if datetime0 is given")

    def timedelta_to_duration(self, timedelta: datetime.timedelta) -> float:
        """
        Parameters
        ----------
        timedelta : datetime.timedelta

        Returns
        -------
        timedelta translated to simulation duration in the current time_unit : float

        Raises
        ------
        ValueError
            if datetime0 is False
        """
        if self._datetime0:
            return self.seconds(timedelta.total_seconds())
        raise ValueError("timestamp_to_duration only possible if datetime0 is given")

    def t_to_datetime(self, t: float) -> Any:
        """
        Parameters
        ----------
        t : float
            time to convert

        Returns
        -------
        t (in the current time unit) translated to the corresponding datetime : float

        Raises
        ------
        ValueError
            if datetime0 is False
        """
        if self._datetime0:
            if t == inf:
                return t
            return self._datetime0 + datetime.timedelta(seconds=self.to_seconds(t))
        raise ValueError("datetime_to_t only possible if datetime0 is given")

    def duration_to_timedelta(self, duration: float) -> datetime.timedelta:
        """
        Parameters
        ----------
        duration : float

        Returns
        -------
        timedelta corresponding to duration : datetime.timedelta

        Raises
        ------
        ValueError
            if time unit is not set
        """
        if self._time_unit:
            return datetime.timedelta(seconds=self.to_seconds(duration))
        raise ValueError("timestamp_to_duration only possible if time unit is given")

    def datetime0(self, datetime0: datetime.datetime = None) -> datetime.datetime:
        """
        Gets and/or sets datetime0

        Parameters
        ----------
        datetime0: bool or datetime.datetime
            if omitted, nothing will be set

            if falsy, disabled

            if True, the t=0 will correspond to 1 January 1970

            if a datetime.datetime value, this will become datetime0

            if no time_unit is specified, but datetime0 is not falsy, time_unit will be set to seconds

        Returns
        -------
        current value of datetime0 : bool or datetime.datetime
        """
        if datetime0 is not None:
            if datetime0:
                if datetime0 is True:
                    self._datetime0 = datetime.datetime(1970, 1, 1)
                else:
                    if isinstance(datetime0, datetime.datetime):
                        self._datetime0 = datetime0
                    elif isinstance(datetime0, str):
                        self._datetime0 = self.dateutil_parse(datetime0)
                    else:
                        raise ValueError(f"datetime0 should be datetime.datetime, str or True, not {type(datetime0)}")
                if self._time_unit is None:
                    self._time_unit = _time_unit_lookup("seconds")
                    self._time_unit_name = "seconds"
            else:
                self._datetime0 = False
        return self._datetime0

    def spec_to_time(self, spec: Union[int, float, str, datetime.datetime, callable]) -> float:
        """
        Converts a generic spec to a proper float time

        Parameters
        ----------
        spec: int, float, str, datetime.datetime or callable

        Returns
        -------
        time : float

        Note
        ----
        Might require dateutil
        """

        if callable(spec):
            spec = spec()
        if spec is None:
            return None
        try:
            return float(spec)
        except:
            ...
        if self._datetime0:
            if isinstance(spec, str):
                return self.datetime_to_t(self.dateutil_parse(spec))
            else:
                return self.datetime_to_t(spec)
        else:
            raise ValueError(f"{spec} is not a proper time specification")

    def spec_to_duration(self, spec: Union[int, float, str, datetime.datetime, callable]) -> float:
        """
        Converts a generic spec to a proper float duration

        Parameters
        ----------
        spec: int, float, str, datetime.timedelta or callable

        Returns
        -------
        duration : float

        Note
        ----
        Might require dateutil
        """
        if callable(spec):
            spec = spec()
        if spec is None:
            return None
        try:
            return float(spec)
        except:
            ...
        if self._datetime0:
            if isinstance(spec, str):
                t1 = self.dateutil_parse(spec)
                t0 = self.dateutil_parse("0:0:0")

                return self.timedelta_to_duration(t1 - t0)
            else:
                return self.timedelta_to_duration(spec)
        raise ValueError(f"{spec} is not a proper duration specification")

    def dateutil_parse(self, spec: str) -> datetime.datetime:
        """
        Parses a string to a datetime, using dateutil.parser.parse with dayfirst=False, yearfirst=True

        Parameters
        ----------
        spec : str
            string to be converted into datetime.datetime

        Returns
        -------
        parsed value of spec : datetime.datetime

        Note
        ----
        The modules dateutil has to be installed, preferably with pip install python-dateutil

        It is possible to override this method, like
            import dateutil.parser
            sim.Environment.dateutil_parse = lambda self, spec: dateutil.parser.parse(spec, dayfirst=True, yearfirst=True)

        or something that does not need dateutil at all
        """
        try:
            import dateutil.parser
        except ImportError:
            raise ImportError("in order to parse a date/time string, install dateutil with pip install python-dateutil")
        try:
            return dateutil.parser.parse(spec, dayfirst=False, yearfirst=True)
        except:
            raise ValueError(f"{spec} is not a valid date/time")

    def beep(self) -> None:
        """
        Beeps

        Works only on Windows and iOS (Pythonista). For other platforms this is just a dummy method.
        """
        if Windows:
            try:
                import winsound

                winsound.Playaudio(os.environ["WINDIR"] + r"\media\Windows Ding.wav", winsound.SND_FILENAME | winsound.SND_ASYNC)
            except Exception:
                pass

        elif Pythonista:
            try:
                import sound  # type: ignore

                sound.stop_all_effects()
                sound.play_effect("game:Beep", pitch=0.3)
            except Exception:
                pass

    # start of PySimpleGUI UI
    def stop_ui(self):
        if self._ui:
            animate = self.animate()
            if not self.pauser.isdata():
                self.pauser.cancel()
            self._ui = False
            self._ui_window.close()
            self.animate(animate)
            self._ui = False

    def start_ui(
        self,
        window_size: Tuple = (None, None),
        window_position: Tuple = (None, None),
        elements: List = None,
        user_handle_event: Callable = None,
        default_elements: bool = True,
        actions: List = None,
    ):
        """
        start the PySimpleGUI UI

        Parameters
        ----------
        window_size : tuple
            width (int) ; default (None): 300

            height (int) ; default (None): 600

        window_position : tuple
            x (int) ; default (None): width of animation

            y (int) ; default (None): 0

        elements : list
            extra elements to add (refer to PySimpleGUI reference)

        user_handle_event : callable
            default: no handler

        default_elements : bool
            if True (default), UI will start with the standard elements

            if False, no standard elements will be used. Use elements
            to add required standard elements
        """
        global sg

        try:
            import PySimpleGUI as sg
        except ImportError:
            raise ImportError("PySimpleGUI required for ui. Install with pip install PySimpleGUI")
        self.remove_topleft_buttons()

        self.animation_parameters(use_toplevel=True)
        self.pauser = _Pauser(at=inf)
        self._ui = True
        if actions is None:
            actions = []
        if user_handle_event is None:
            self.user_handle_event = lambda env, window, event, values: None
        else:
            self.user_handle_event = user_handle_event

        if default_elements:
            frame0 = [
                [sg.Text("", key="-TIME-", metadata=[1, 2], size=200)],
                [sg.Button("Pause", key="-PAUSE-GO-", metadata=[1, 2]), sg.Button("Stop", key="-STOP-", button_color=("white", "firebrick3"), metadata=[1, 2])],
                [sg.Checkbox("Pause at each step", False, key="-PAUSE-AT-EACH-STEP-", enable_events=True, metadata=[1, 2])],
                [sg.Text(f"Pause at{self.get_time_unit(template='(t)')}", key="-PAUSE-AT-TEXT-", size=17), sg.Input("", key="-PAUSE-AT-", size=(10, 20))],
                [sg.Text(f"Pause each{self.get_time_unit(template='(d)')}", key="-PAUSE-EACH-TEXT-", size=17), sg.Input("", key="-PAUSE-EACH-", size=(10, 20))],
                [
                    sg.Text("Speed", key="-SPEED-TEXT-", metadata=[1]),
                    sg.Button("/2", key="-SPEED/2-", metadata=[1]),
                    sg.Button("*2", key="-SPEED*2-", metadata=[1]),
                    sg.Input("", key="-SPEED-", size=(7, 10)),
                ],
                [sg.Checkbox("Trace", self.trace(), key="-TRACE-", metadata=[1, 2], enable_events=True)],
                [sg.Checkbox("Synced", self.synced(), key="-SYNCED-", metadata=[1], enable_events=True)],
                [sg.Checkbox("Animate", True, key="-ANIMATE-", metadata=[1, 2], enable_events=True)],
            ]
        else:
            frame0 = []

        if elements:
            if frame0:
                frame0.append([sg.HorizontalSeparator()])
            frame0.extend(elements)

        if actions:
            if frame0:
                frame0.append([sg.HorizontalSeparator()])
            frame0.extend(actions)

        layout = [[sg.Frame("", frame0, pad=((0, 0), (20, 0)))]]

        window_size = list(window_size)
        if window_size[0] is None:
            window_size[0] = 300
        if window_size[1] is None:
            window_size[1] = self.height() + 33

        window_position = list(window_position)

        if window_position[0] is None:
            window_position[0] = self.width() + 10
        if window_position[1] is None:
            window_position[1] = 0

        self._last_animate = "?"
        self._last_paused = "?"
        self._ui_window = sg.Window("", no_titlebar=True, layout=layout, size=window_size, location=window_position)
        self._ui_window.finalize()
        self._ui_keys = {key.key for key in self._ui_window.element_list()}

        self.pause_at = inf
        self._pause_at_each_step = False
        self.set_start_animation()

    def ui_granularity(self, value: int = None) -> int:
        """
        ui_granularity

        Parameters
        ----------
        value : int
            new ui_granularity

            defines how often the ui_handler is called when animation is off
            (initially 1)

            if omitted, no change

        Returns
        -------
        current ui_granularity : int

        Note
        ----
        If you want to test the status, always include
        parentheses, like

            ``if env.paused():``
        """
        if value is not None:
            self._ui_granularity = value
        return self._ui_granularity

    def ui_window(self) -> "Window":
        return self._ui_window

    def set_pause_go_button(self):
        if "-PAUSE-GO-" in self._ui_keys:
            self._ui_window["-PAUSE-GO-"].Update("Go" if self._paused else "Pause")

    def _handle_ui_event(self):
        if self._last_animate != self._animate or self._last_paused != self._paused:
            for key in self._ui_window.key_dict:
                field = self._ui_window[key]
                if type(field) != sg.HorizontalSeparator:
                    if self._paused:
                        field.update(visible=True)
                    else:
                        if self._animate:
                            field.update(visible=field.metadata is not None and 1 in field.metadata)
                        else:
                            field.update(visible=field.metadata is not None and 2 in field.metadata)
            self._last_animate = self._animate
            self._last_paused = self._paused
            self.set_pause_go_button()

        event, values = self._ui_window.read(timeout=0)

        if values is None:
            return

        if "-SPEED-" in self._ui_keys:
            if values["-SPEED-"] == "":
                self._ui_window["-SPEED-"].update(str(self.speed()))

        if "-TIME-" in self._ui_keys:
            t = self.pauser.scheduled_time()
            s = [f"t={self.time_to_str(self.t()).lstrip()}{self.get_time_unit(template='t')}"]
            if t != inf and not self._paused:
                s.append(f" | Pause at {self.time_to_str(t).lstrip()}{self.get_time_unit(template='t')}")
            if self.animate():
                s.append(f" | Speed={self.speed():.3f}")

            self._ui_window["-TIME-"].Update("".join(s))

        if event in ("__TIMEOUT__", sg.WIN_CLOSED, "Exit"):
            return

        if event == "-STOP-":
            ch = sg.popup_yes_no(f"{chr(160):>50}", title="Stop?")  # chr(160) is a non breaking blank, equivalent to "\0xA0")
            if ch == "Yes":
                sys.exit()

        if event == "-PAUSE-GO-":
            # if not self.animate():
            #     self.animate(True)
            #     self.paused(False)
            #     if "-ANIMATE-" in self._ui_keys:
            #         self._ui_window["-ANIMATE-"].update(animate)  # this is required as the self.animate() also sets the value

            self.set_start_animation()
            if self._paused:
                if "-PAUSE-AT-" in self._ui_keys:
                    pause_at_str = values["-PAUSE-AT-"]
                else:
                    pause_at_str = ""
                if pause_at_str != "":
                    try:
                        self.pause_at = self.spec_to_time(pause_at_str)
                    except ValueError:
                        self.pause_at = None
                else:
                    self.pause_at = inf

                if self.pause_at is None:
                    sg.popup("Pause not valid")
                else:
                    if "-PAUSE-EACH-" in self._ui_keys:
                        pause_each_str = values["-PAUSE-EACH-"]
                    else:
                        pause_each_str = ""
                    if pause_each_str.strip() != "":
                        try:
                            pause_each = self.spec_to_duration(pause_each_str)
                            t = (self.t() // pause_each + 1) * pause_each
                            if t < self.t() or math.isclose(t, self.t()):  # avoid rounding error
                                t = t + pause_each

                            self.pause_at = min(self.pause_at, t)

                        except ValueError:
                            self.pause_at = None

                    if self.pause_at is None:
                        sg.popup("Pause interval not valid")
                    else:
                        if self.pause_at > self.t() * 0.99999999:
                            if "-ANIMATE-" in self._ui_keys:
                                self.animate(values["-ANIMATE-"])
                            self.paused(False)
                            if self.pauser.scheduled_time() != self.pause_at:
                                self.pauser.activate(at=self.pause_at)
                        else:
                            sg.popup(f"Pause at should be > {self.time_to_str(self.t()).lstrip()}")
            else:
                self.paused(True)
                self._ui_window[event].Update("Pause")

            if "-SPEED-" in self._ui_keys:
                new_speed = float(values["-SPEED-"])
                if new_speed != self.speed():
                    self.speed(new_speed)
                    self.set_start_animation()

        if event == "-SPEED*2-":
            self.speed(self.speed() * 2)
            if "-SPEED-" in self._ui_keys:
                self._ui_window["-SPEED-"].update(str(self.speed()))
            self.set_start_animation()

        if event == "-SPEED/2-":
            self.speed(self.speed() / 2)
            if "-SPEED-" in self._ui_keys:
                self._ui_window["-SPEED-"].update(str(self.speed()))
            self.set_start_animation()

        if event == "-SYNCED-":
            if "-SYNCED-" in self._ui_keys:
                self.synced(values["-SYNCED-"])

        if event == "-PAUSE-AT-EACH-STEP-":
            if "-PAUSE-AT-EACH-STEP-" in self._ui_keys:
                self._pause_at_each_step = values["-PAUSE-AT-EACH-STEP-"]

        if event == "-TRACE-":
            if "-TRACE-" in self._ui_keys:
                self.trace(values["-TRACE-"])

        if event == "-ANIMATE-":
            if "-ANIMATE-" in self._ui_keys:
                if values["-ANIMATE-"]:
                    self.animate(True)
                    self.paused(True)
                else:
                    _AnimateOff(urgent=True)

        self.user_handle_event(env=self, window=self._ui_window, event=event, values=values)


class _Pauser(Component):
    def process(self):
        event, values = self.env._ui_window.read(timeout=0)
        if "-ANIMATE-" in self.env._ui_keys:
            animate = values["-ANIMATE-"]
        self.env.animation_start_time = self.env._now
        self.env._t = self.env._now
        self.env.animate(True)
        self.env.paused(True)
        self.env.set_pause_go_button()
        if "-PAUSE-AT-" in self.env._ui_keys:
            if values["-PAUSE-AT-"] != "" and self.env._now >= self.env.spec_to_time(values["-PAUSE-AT-"]):
                self.env._ui_window["-PAUSE-AT-"].Update("")
        if "-ANIMATE-" in self.env._ui_keys:
            self.env._ui_window["-ANIMATE-"].update(animate)  # this is required as the self.animate() also sets the value


class _AnimateOff(Component):
    def process(self):
        self.env.animate(False)
        self._last_animate = None
        self.env._handle_ui_event()


# end of PySimpleGUI UI


class Animate2dBase(DynamicClass):
    def __init__(self, type, locals_, argument_default, attached_to=None, attach_text=True):
        super().__init__()
        self.type = type
        env = locals_["env"]
        arg = locals_["arg"]
        parent = locals_["parent"]
        if attached_to is None and parent is not None:
            if not isinstance(parent, Component):
                raise ValueError(repr(parent) + " is not a component")
            parent._animation_children.add(self)

        screen_coordinates = locals_["screen_coordinates"]
        over3d = locals_["over3d"]

        self.env = _set_env(env)

        self.sequence = self.env.serialize()
        self.arg = self if arg in (None, object) else arg
        self.over3d = _default_over3d if over3d is None else over3d
        self.screen_coordinates = screen_coordinates
        self.attached_to = attached_to
        if attached_to:
            for name in attached_to._dynamics:
                setattr(self, name, lambda arg, t, name=name: getattr(self.attached_to, name)(t))
                self.register_dynamic_attributes(name)

        else:
            for name, default in argument_default.items():
                if locals_[name] is None:
                    if not hasattr(self, name):
                        setattr(self, name, default)
                else:
                    setattr(self, name, locals_[name])
                self.register_dynamic_attributes(name)

        self._image_ident = None  # denotes no image yet
        self._image = None
        self._image_x = 0
        self._image_y = 0
        self.canvas_object = None

        if self.env._animate_debug:
            self.caller = self.env._frame_to_lineno(_get_caller_frame(), add_filename=True)
        else:
            self.caller = "?. use env.animate_debug(True) to get the originating Animate location"

        if attach_text:
            self.depending_object = Animate2dBase(type="text", locals_=locals_, argument_default={}, attached_to=self, attach_text=False)
        else:
            self.depending_object = None
        if not self.attached_to:
            self.show()

    def show(self):
        if self.depending_object:
            if self.over3d:
                self.env.an_objects_over3d.add(self.depending_object)
            else:
                self.env.an_objects.add(self.depending_object)
        if self.over3d:
            self.env.an_objects_over3d.add(self)
        else:
            self.env.an_objects.add(self)

    def remove(self):
        if self.depending_object:
            if self.over3d:
                self.env.an_objects_over3d.discard(self.depending_object)
            else:
                self.env.an_objects.discard(self.depending_object)
                self.canvas_object = None  # safety! even set for non tkinter

        if self.over3d:
            self.env.an_objects_over3d.discard(self)
        else:
            self.env.an_objects.discard(self)
            self.canvas_object = None  # safety! even set for non tkinter

    def is_removed(self):
        if self.over3d:
            return self not in self.env.an_over3d_objects
        else:
            return self not in self.env.an_objects

    def make_pil_image(self, t):
        try:
            if self.keep(t):
                visible = self.visible(t)
                if self.env._exclude_from_animation == visible:
                    visible = False
            else:
                self.remove()
                visible = False

            if visible:
                if self.type == "text":  # checked so early as to avoid evaluation of x, y, angle, ...
                    text = self.text(t)
                    if (text is None) or (text.strip() == ""):
                        self._image_visible = False
                        return

                self._image_ident_prev = self._image_ident

                self._image_x_prev = self._image_x
                self._image_y_prev = self._image_y

                x = self.x(t)
                y = self.y(t)
                xy_anchor = self.xy_anchor(t)
                if xy_anchor:
                    x += self.env.xy_anchor_to_x(xy_anchor, screen_coordinates=self.screen_coordinates, over3d=self.over3d)
                    y += self.env.xy_anchor_to_y(xy_anchor, screen_coordinates=self.screen_coordinates, over3d=self.over3d)

                offsetx = self.offsetx(t)
                offsety = self.offsety(t)
                if not self.screen_coordinates:
                    offsetx = offsetx * self.env._scale
                    offsety = offsety * self.env._scale

                angle = self.angle(t)

                if self.type in ("polygon", "rectangle", "line", "circle"):
                    if self.screen_coordinates:
                        linewidth = self.linewidth(t)
                    else:
                        linewidth = self.linewidth(t) * self.env._scale

                    linecolor = self.env.colorspec_to_tuple(self.linecolor(t))
                    fillcolor = self.env.colorspec_to_tuple(self.fillcolor(t))

                    cosa = math.cos(math.radians(angle))
                    sina = math.sin(math.radians(angle))

                    if self.screen_coordinates:
                        qx = x
                        qy = y
                    else:
                        qx = (x - self.env._x0) * self.env._scale
                        qy = (y - self.env._y0) * self.env._scale

                    if self.type == "rectangle":
                        as_points = self.as_points(t)
                        rectangle = tuple(de_none(self.spec(t)))
                        self._image_ident = (tuple(rectangle), linewidth, linecolor, fillcolor, as_points, angle, self.screen_coordinates)
                    elif self.type == "line":
                        as_points = self.as_points(t)
                        line = tuple(de_none(self.spec(t)))
                        fillcolor = (0, 0, 0, 0)
                        self._image_ident = (tuple(line), linewidth, linecolor, as_points, angle, self.screen_coordinates)
                    elif self.type == "polygon":
                        as_points = self.as_points(t)
                        polygon = tuple(de_none(self.spec(t)))
                        self._image_ident = (tuple(polygon), linewidth, linecolor, fillcolor, as_points, angle, self.screen_coordinates)
                    elif self.type == "circle":
                        as_points = False
                        radius0 = self.radius(t)
                        radius1 = self.radius1(t)
                        if radius1 is None:
                            radius1 = radius0
                        arc_angle0 = self.arc_angle0(t)
                        arc_angle1 = self.arc_angle1(t)
                        draw_arc = bool(self.draw_arc(t))

                        self._image_ident = (
                            radius0,
                            radius1,
                            arc_angle0,
                            arc_angle1,
                            draw_arc,
                            linewidth,
                            linecolor,
                            fillcolor,
                            angle,
                            self.screen_coordinates,
                        )

                    if self._image_ident != self._image_ident_prev:
                        if self.type == "rectangle":
                            px = [rectangle[0], rectangle[2]]
                            py = [rectangle[1], rectangle[3]]

                            if len(rectangle) == 5:
                                r = rectangle[4]
                            else:
                                r = 0
                            if r == 0:
                                p = [px[0], py[0], px[1], py[0], px[1], py[1], px[0], py[1], px[0], py[0]]
                            else:
                                if not self.screen_coordinates:
                                    r *= self.env._scale

                                r = min(r, abs(px[0] - px[1]) / 2, abs(py[0] - py[1]) / 2)  # make sure the arc fits

                                if self.screen_coordinates:
                                    nsteps = int(math.sqrt(r) * 6)
                                else:
                                    nsteps = int(math.sqrt(r * self.env._scale) * 6)
                                tarc_angle = 360 / nsteps

                                p = []

                                for x0, y0, x1, y1, x2, y2, arc_angle0 in [
                                    [px[0] + r, py[0], px[1] - r, py[0], px[1] - r, py[0] + r, -90],
                                    [px[1], py[0] + r, px[1], py[1] - r, px[1] - r, py[1] - r, 0],
                                    [px[1] - r, py[1], px[0] + r, py[1], px[0] + r, py[1] - r, 90],
                                    [px[0], py[1] - r, px[0], py[0] + r, px[0] + r, py[0] + r, 180],
                                ]:
                                    p.append(x0)
                                    p.append(y0)
                                    p.append(x1)
                                    p.append(y1)

                                    arc_angle = arc_angle0

                                    ended = False
                                    while True:
                                        sint = math.sin(math.radians(arc_angle))
                                        cost = math.cos(math.radians(arc_angle))
                                        p.append(x2 + r * cost)
                                        p.append(y2 + r * sint)
                                        if ended:
                                            break
                                        arc_angle += tarc_angle
                                        if arc_angle >= arc_angle0 + 90:
                                            arc_angle = arc_angle0 + 90
                                            ended = True

                        elif self.type == "line":
                            p = line

                        elif self.type == "polygon":
                            p = list(polygon)
                            if p[0:2] != p[-3:-1]:
                                p.append(p[0])  # close the polygon
                                p.append(p[1])

                        elif self.type == "circle":
                            if arc_angle0 > arc_angle1:
                                arc_angle0, arc_angle1 = arc_angle1, arc_angle0
                            arc_angle1 = min(arc_angle1, arc_angle0 + 360)

                            if self.screen_coordinates:
                                nsteps = int(math.sqrt(max(radius0, radius1)) * 6)
                            else:
                                nsteps = int(math.sqrt(max(radius0 * self.env._scale, radius1 * self.env._scale)) * 6)
                            tarc_angle = 360 / nsteps
                            p = [0, 0]

                            arc_angle = arc_angle0
                            ended = False
                            while True:
                                sint = math.sin(math.radians(arc_angle))
                                cost = math.cos(math.radians(arc_angle))
                                x, y = (radius0 * cost, radius1 * sint)
                                p.append(x)
                                p.append(y)
                                if ended:
                                    break
                                arc_angle += tarc_angle
                                if arc_angle >= arc_angle1:
                                    arc_angle = arc_angle1
                                    ended = True
                            p.append(0)
                            p.append(0)

                        r = []
                        minpx = inf
                        minpy = inf
                        maxpx = -inf
                        maxpy = -inf
                        minrx = inf
                        minry = inf
                        maxrx = -inf
                        maxry = -inf
                        for i in range(0, len(p), 2):
                            px = p[i]
                            py = p[i + 1]
                            if not self.screen_coordinates:
                                px *= self.env._scale
                                py *= self.env._scale
                            rx = px * cosa - py * sina
                            ry = px * sina + py * cosa
                            minpx = min(minpx, px)
                            maxpx = max(maxpx, px)
                            minpy = min(minpy, py)
                            maxpy = max(maxpy, py)
                            minrx = min(minrx, rx)
                            maxrx = max(maxrx, rx)
                            minry = min(minry, ry)
                            maxry = max(maxry, ry)
                            r.append(rx)
                            r.append(ry)
                        if maxrx == -inf:
                            maxpx = 0
                            minpx = 0
                            maxpy = 0
                            minpy = 0
                            maxrx = 0
                            minrx = 0
                            maxry = 0
                            minry = 0

                        rscaled = []
                        for i in range(0, len(r), 2):
                            rscaled.append(r[i] - minrx + linewidth)
                            rscaled.append(maxry - r[i + 1] + linewidth)
                        rscaled = tuple(rscaled)  # to make it hashable

                        if as_points:
                            self._image = Image.new("RGBA", (int(maxrx - minrx + 2 * linewidth), int(maxry - minry + 2 * linewidth)), (0, 0, 0, 0))
                            point_image = Image.new("RGBA", (int(linewidth), int(linewidth)), linecolor)

                            for i in range(0, len(r), 2):
                                rx = rscaled[i]
                                ry = rscaled[i + 1]
                                self._image.paste(point_image, (int(rx - 0.5 * linewidth), int(ry - 0.5 * linewidth)), point_image)

                        else:
                            self._image = Image.new("RGBA", (int(maxrx - minrx + 2 * linewidth), int(maxry - minry + 2 * linewidth)), (0, 0, 0, 0))
                            draw = ImageDraw.Draw(self._image)
                            if fillcolor[3] != 0:
                                draw.polygon(rscaled, fill=fillcolor)
                            if (round(linewidth) > 0) and (linecolor[3] != 0):
                                if self.type == "circle" and not draw_arc:
                                    draw.line(rscaled[2:-2], fill=linecolor, width=int(linewidth))
                                    # get rid of the first and last point (=center)
                                else:
                                    draw.line(rscaled, fill=linecolor, width=int(round(linewidth)))
                            del draw
                        self.minrx = minrx
                        self.minry = minry
                        self.maxrx = maxrx
                        self.maxry = maxry
                        self.minpx = minpx
                        self.minpy = minpy
                        self.maxpx = maxpx
                        self.maxpy = maxpy

                    if self.type == "circle":
                        self.env._centerx = qx
                        self.env._centery = qy
                        self.env._dimx = 2 * radius0
                        self.env._dimy = 2 * radius1
                    else:
                        self.env._centerx = qx + (self.minrx + self.maxrx) / 2
                        self.env._centery = qy + (self.minry + self.maxry) / 2
                        self.env._dimx = self.maxpx - self.minpx
                        self.env._dimy = self.maxpy - self.minpy

                    self._image_x = qx + self.minrx - linewidth + (offsetx * cosa - offsety * sina)
                    self._image_y = qy + self.minry - linewidth + (offsetx * sina + offsety * cosa)

                elif self.type == "image":
                    spec = self.image(t)
                    image_container = ImageContainer(spec)
                    width = self.width(t)
                    height = self.height(t)

                    if width is None:
                        if height is None:
                            width = image_container.images[0].size[0]
                            height = image_container.images[0].size[1]
                        else:
                            width = height * image_container.images[0].size[0] / image_container.images[0].size[1]
                    else:
                        if height is None:
                            height = width * image_container.images[0].size[1] / image_container.images[0].size[0]
                        else:
                            ...

                    if not self.screen_coordinates:
                        width *= self.env._scale
                        height *= self.env._scale

                    angle = self.angle(t)
                    anchor = self.anchor(t)
                    flip_horizontal = self.flip_horizontal(t)
                    flip_vertical = self.flip_vertical(t)
                    if self.screen_coordinates:
                        qx = x
                        qy = y
                    else:
                        qx = (x - self.env._x0) * self.env._scale
                        qy = (y - self.env._y0) * self.env._scale
                        offsetx = offsetx * self.env._scale
                        offsety = offsety * self.env._scale

                    alpha = int(self.alpha(t))
                    image, id = image_container.get_image(
                        (t - self.animation_start(t)) * self.animation_speed(t),
                        repeat=self.animation_repeat(t),
                        pingpong=self.animation_pingpong(t),
                        t_from=self.animation_from(t),
                        t_to=self.animation_to(t),
                    )

                    self._image_ident = (spec, id, width, height, angle, alpha, flip_horizontal, flip_vertical)

                    if self._image_ident != self._image_ident_prev:
                        if flip_horizontal:
                            image = image.transpose(method=Image.FLIP_LEFT_RIGHT)
                        if flip_vertical:
                            image = image.transpose(method=Image.FLIP_TOP_BOTTOM)

                        if int(width) == 0 or int(height) == 0:
                            im1 = g.dummy_image  # prevent a ValueError
                        else:
                            im1 = image.resize((int(width), int(height)), Image.LANCZOS)
                        self.imwidth, self.imheight = im1.size
                        if alpha != 255:
                            if has_numpy():
                                arr = numpy.asarray(im1).copy()
                                arr_alpha = arr[:, :, 3]
                                arr[:, :, 3] = arr_alpha * (alpha / 255)
                                im1 = Image.fromarray(numpy.uint8(arr))
                            else:
                                pix = im1.load()
                                for x in range(self.imwidth):
                                    for y in range(self.imheight):
                                        c = pix[x, y]
                                        pix[x, y] = (c[0], c[1], c[2], int(c[3] * alpha / 255))
                        self._image = im1.rotate(angle, expand=1)
                    anchor_to_dis = {
                        "ne": (-0.5, -0.5),
                        "n": (0, -0.5),
                        "nw": (0.5, -0.5),
                        "e": (-0.5, 0),
                        "center": (0, 0),
                        "c": (0, 0),
                        "w": (0.5, 0),
                        "se": (-0.5, 0.5),
                        "s": (0, 0.5),
                        "sw": (0.5, 0.5),
                    }
                    dx, dy = anchor_to_dis[anchor.lower()]
                    dx = dx * self.imwidth + offsetx
                    dy = dy * self.imheight + offsety
                    cosa = math.cos(math.radians(angle))
                    sina = math.sin(math.radians(angle))
                    ex = dx * cosa - dy * sina
                    ey = dx * sina + dy * cosa
                    imrwidth, imrheight = self._image.size

                    self.env._centerx = qx + ex
                    self.env._centery = qy + ey
                    self.env._dimx = width
                    self.env._dimy = height

                    self._image_x = qx + ex - imrwidth / 2
                    self._image_y = qy + ey - imrheight / 2

                elif self.type == "text":
                    # text contains self.text()
                    textcolor = self.env.colorspec_to_tuple(self.textcolor(t))
                    fontsize = self.fontsize(t)
                    angle = self.angle(t)
                    fontname = self.font(t)

                    if not self.screen_coordinates:
                        fontsize = fontsize * self.env._scale
                    text_anchor = self.text_anchor(t)

                    if self.attached_to:
                        text_offsetx = self.text_offsetx(t)
                        text_offsety = self.text_offsety(t)
                        if not self.screen_coordinates:
                            text_offsetx = text_offsetx * self.env._scale
                            text_offsety = text_offsety * self.env._scale
                        qx = self.env._centerx
                        qy = self.env._centery
                        anchor_to_dis = {
                            "ne": (0.5, 0.5),
                            "n": (0, 0.5),
                            "nw": (-0.5, 0.5),
                            "e": (0.5, 0),
                            "center": (0, 0),
                            "c": (0, 0),
                            "w": (-0.5, 0),
                            "se": (0.5, -0.5),
                            "s": (0, -0.5),
                            "sw": (-0.5, -0.5),
                        }
                        dis = anchor_to_dis[text_anchor.lower()]
                        offsetx += text_offsetx + dis[0] * self.env._dimx - dis[0] * 4  # 2 extra at east or west
                        offsety += text_offsety + dis[1] * self.env._dimy - (2 if dis[1] > 0 else 0)  # 2 extra at north
                    else:
                        if self.screen_coordinates:
                            qx = x
                            qy = y
                        else:
                            qx = (x - self.env._x0) * self.env._scale
                            qy = (y - self.env._y0) * self.env._scale
                    max_lines = self.max_lines(t)

                    self._image_ident = (text, fontname, fontsize, angle, textcolor, max_lines)
                    if self._image_ident != self._image_ident_prev:
                        font, heightA = getfont(fontname, fontsize)

                        lines = []
                        for item in deep_flatten(text):
                            for line in item.splitlines():
                                lines.append(line.rstrip())

                        if max_lines <= 0:  # 0 is all
                            lines = lines[max_lines:]
                        else:
                            lines = lines[:max_lines]

                        widths = [(font.getbbox(line)[2] if line else 0) for line in lines]
                        if widths:
                            totwidth = max(widths)
                        else:
                            totwidth = 0

                        number_of_lines = len(lines)
                        lineheight = font.getbbox("Ap")[3]
                        totheight = number_of_lines * lineheight
                        im = Image.new("RGBA", (int(totwidth + 0.1 * fontsize), int(totheight)), (0, 0, 0, 0))
                        imwidth, imheight = im.size
                        draw = ImageDraw.Draw(im)
                        pos = 0
                        for line, width in zip(lines, widths):
                            if line:
                                draw.text(xy=(0.1 * fontsize, pos), text=line, font=font, fill=textcolor)

                            pos += lineheight
                        # this code is to correct a bug in the rendering of text,
                        # leaving a kind of shadow around the text
                        del draw
                        if textcolor[:3] != (0, 0, 0):  # black is ok
                            if False and has_numpy():
                                arr = numpy.asarray(im).copy()
                                arr[:, :, 0] = textcolor[0]
                                arr[:, :, 1] = textcolor[1]
                                arr[:, :, 2] = textcolor[2]
                                im = Image.fromarray(numpy.uint8(arr))
                            else:
                                pix = im.load()
                                for y in range(imheight):
                                    for x in range(imwidth):
                                        pix[x, y] = (textcolor[0], textcolor[1], textcolor[2], pix[x, y][3])

                        # end of code to correct bug

                        self.imwidth, self.imheight = im.size
                        self.heightA = heightA

                        self._image = im.rotate(angle, expand=1)

                    anchor_to_dis = {
                        "ne": (-0.5, -0.5),
                        "n": (0, -0.5),
                        "nw": (0.5, -0.5),
                        "e": (-0.5, 0),
                        "center": (0, 0),
                        "c": (0, 0),
                        "w": (0.5, 0),
                        "se": (-0.5, 0.5),
                        "s": (0, 0.5),
                        "sw": (0.5, 0.5),
                    }
                    dx, dy = anchor_to_dis[text_anchor.lower()]
                    dx = dx * self.imwidth + offsetx - 0.1 * fontsize

                    dy = dy * self.imheight + offsety
                    cosa = math.cos(math.radians(angle))
                    sina = math.sin(math.radians(angle))
                    ex = dx * cosa - dy * sina
                    ey = dx * sina + dy * cosa
                    imrwidth, imrheight = self._image.size
                    self._image_x = qx + ex - imrwidth / 2
                    self._image_y = qy + ey - imrheight / 2
                else:
                    raise ValueError("Internal error: animate type" + self.type + "not recognized.")
                if self.over3d:
                    width = self.env._width3d
                    height = self.env._height3d
                else:
                    width = self.env._width
                    height = self.env._height

                self._image_visible = (
                    (self._image_x <= width)
                    and (self._image_y <= height)
                    and (self._image_x + self._image.size[0] >= 0)
                    and (self._image_y + self._image.size[1] >= 0)
                )
            else:
                self._image_visible = False
        except Exception as e:
            self.env._animate = False
            self.env.running = False
            traceback.print_exc()
            raise type(e)(str(e) + " [from " + self.type + " animation object created in line " + self.caller + "]") from e


class AnimateClassic(Animate2dBase):
    def __init__(self, master, locals_):
        super().__init__(locals_=locals_, type=master.type, argument_default={}, attach_text=False)
        self.master = master

    def text(self, t):
        return self.master.text(t)

    def x(self, t):
        return self.master.x(t)

    def y(self, t):
        return self.master.y(t)

    def layer(self, t):
        return self.master.layer(t)

    def visible(self, t):
        return self.master.visible(t)

    def flip_horizontal(self, t):
        return self.master.flip_horizontal(t)

    def flip_vertical(self, t):
        return self.master.flip_vertical(t)

    def animation_start(self, t):
        return self.master.animation_start(t)

    def animation_speed(self, t):
        return self.master.animation_speed(t)

    def animation_repeat(self, t):
        return self.master.animation_repeat(t)

    def animation_pingpong(self, t):
        return self.master.animation_pingpong(t)

    def animation_from(self, t):
        return self.master.animation_from(t)

    def animation_to(self, t):
        return self.master.animation_to(t)

    def keep(self, t):
        return self.master.keep(t)

    def xy_anchor(self, t):
        return self.master.xy_anchor(t)

    def offsetx(self, t):
        return self.master.offsetx(t)

    def offsety(self, t):
        return self.master.offsety(t)

    def angle(self, t):
        return self.master.angle(t)

    def textcolor(self, t):
        return self.master.textcolor(t)

    def text_anchor(self, t):
        return self.master.text_anchor(t)

    def fontsize(self, t):
        return self.master.fontsize(t)

    def font(self, t):
        return self.master.font0

    def max_lines(self, t):
        return self.master.max_lines(t)

    def image(self, t):
        return self.master.image(t)

    def width(self, t):
        return self.master.width(t)

    def height(self, t):
        return self.master.height(t)

    def anchor(self, t):
        return self.master.anchor(t)

    def alpha(self, t):
        return self.master.alpha(t)

    def linewidth(self, t):
        return self.master.linewidth(t)

    def linecolor(self, t) -> ColorType:
        return self.master.linecolor(t)

    def fillcolor(self, t):
        return self.master.fillcolor(t)

    def as_points(self, t):
        return self.master.as_points(t)

    def spec(self, t):
        if self.type == "line":
            return self.master.line(t)
        if self.type == "rectangle":
            return self.master.rectangle(t)
        if self.type == "polygon":
            return self.master.polygon(t)

    def radius(self, t):
        circle = self.master.circle(t)
        try:
            return circle[0]
        except TypeError:
            return circle

    def radius1(self, t):
        circle = self.master.circle(t)
        try:
            return circle[1]
        except (TypeError, IndexError):
            return circle

    def arc_angle0(self, t):
        circle = self.master.circle(t)
        try:
            return circle[2]
        except (TypeError, IndexError):
            return 0

    def arc_angle1(self, t):
        circle = self.master.circle(t)
        try:
            return circle[3]
        except (TypeError, IndexError):
            return 360

    def draw_arc(self, t):
        circle = self.master.circle(t)
        try:
            return circle[4]
        except (TypeError, IndexError):
            return False


class Animate:
    """
    defines an animation object

    Parameters
    ----------
    parent : Component
        component where this animation object belongs to (default None)

        if given, the animation object will be removed
        automatically when the parent component is no longer accessible

    layer : int
        layer value

        lower layer values are on top of higher layer values (default 0)

    keep : bool
        keep

        if False, animation object is hidden after t1, shown otherwise
        (default True)

    visible : bool
        visible

        if False, animation object is not shown, shown otherwise
        (default True)

    screen_coordinates : bool
        use screen_coordinates

        normally, the scale parameters are use for positioning and scaling
        objects.

        if True, screen_coordinates will be used instead.

    xy_anchor : str
        specifies where x and y (i.e. x0, y0, x1 and y1) are relative to

        possible values are (default: sw) ::

            nw    n    ne
            w     c     e
            sw    s    se

        If null string, the given coordimates are used untranslated

    t0 : float
        time of start of the animation (default: now)

    x0 : float
        x-coordinate of the origin at time t0 (default 0)

    y0 : float
        y-coordinate of the origin at time t0 (default 0)

    offsetx0 : float
        offsets the x-coordinate of the object at time t0 (default 0)

    offsety0 : float
        offsets the y-coordinate of the object at time t0 (default 0)

    circle0 : float or tuple/list
        the circle spec of the circle at time t0

        - radius

        - one item tuple/list containing the radius

        - five items tuple/list cntaining radius, radius1, arc_angle0, arc_angle1 and draw_arc

        (see class AnimateCircle for details)

    line0 : list or tuple
        the line(s) (xa,ya,xb,yb,xc,yc, ...) at time t0

    polygon0 : list or tuple
        the polygon (xa,ya,xb,yb,xc,yc, ...) at time t0

        the last point will be auto connected to the start

    rectangle0 : list or tuple
        the rectangle (xlowerleft,ylowerleft,xupperright,yupperright) at time t0

        optionally a fifth element can be used to specify the radius of a rounded rectangle

    image : str, pathlib.Path or PIL image
        the image to be displayed

        This may be either a filename or a PIL image

        if image is a string consisting of a zipfile-name, a bar (|) and a filename,
        the given filename will be read from the specified zip archive, e.g

        sim.AnimateImage(image="cars.zip|bmw.png")


    text : str, tuple or list
        the text to be displayed

        if text is str, the text may contain linefeeds, which are shown as individual lines

    max_lines : int
        the maximum of lines of text to be displayed

        if positive, it refers to the first max_lines lines

        if negative, it refers to the first -max_lines lines

        if zero (default), all lines will be displayed

    font : str or list/tuple
        font to be used for texts

        Either a string or a list/tuple of fontnames.
        If not found, uses calibri or arial

    anchor : str
        anchor position

        specifies where to put images or texts relative to the anchor
        point

        possible values are (default: c) ::

            nw    n    ne
            w     c     e
            sw    s    se

    as_points : bool
        if False (default), lines in line, rectangle and polygon are drawn

        if True, only the end points are shown in line, rectangle and polygon

    linewidth0 : float
        linewidth of the contour at time t0 (default 0 for polygon, rectangle and circle, 1 for line)

        if as_point is True, the default size is 3

    fillcolor0 : colorspec
        color of interior at time t0 (default foreground_color)

        if as_points is True, fillcolor0 defaults to transparent

    linecolor0 : colorspec
        color of the contour at time t0 (default foreground_color)

    textcolor0 : colorspec
        color of the text at time 0 (default foreground_color)

    angle0 : float
        angle of the polygon at time t0 (in degrees) (default 0)

    alpha0 : float
        alpha of the image at time t0 (0-255) (default 255)

    fontsize0 : float
        fontsize of text at time t0 (default 20)

    width0 : float
       width of the image to be displayed at time t0

       if omitted or None, no scaling

    height0 : float
       width of the image to be displayed at time t0

       if omitted or None, no scaling

    t1 : float
        time of end of the animation (default inf)

        if keep=True, the animation will continue (frozen) after t1

    x1 : float
        x-coordinate of the origin at time t1(default x0)

    y1 : float
        y-coordinate of the origin at time t1 (default y0)

    offsetx1 : float
        offsets the x-coordinate of the object at time t1 (default offsetx0)

    offsety1 : float
        offsets the y-coordinate of the object at time t1 (default offsety0)

    circle1 : float or tuple/list
        the circle spec of the circle at time t1 (default: circle0)

        - radius

        - one item tuple/list containing the radius

        - five items tuple/list cntaining radius, radius1, arc_angle0, arc_angle1 and draw_arc

        (see class AnimateCircle for details)

    line1 : tuple
        the line(s) at time t1 (xa,ya,xb,yb,xc,yc, ...) (default: line0)

        should have the same number of elements as line0

    polygon1 : tuple
        the polygon at time t1 (xa,ya,xb,yb,xc,yc, ...) (default: polygon0)

        should have the same number of elements as polygon0

    rectangle1 : tuple
        the rectangle (xlowerleft,ylowerleft,xupperright,yupperright) at time t1
        (default: rectangle0)

        optionally a fifth element can be used to specify the radius of a rounded rectangle

    linewidth1 : float
        linewidth of the contour at time t1 (default linewidth0)

    fillcolor1 : colorspec
        color of interior at time t1 (default fillcolor0)

    linecolor1 : colorspec
        color of the contour at time t1 (default linecolor0)

    textcolor1 : colorspec
        color of text at time t1 (default textcolor0)

    angle1 : float
        angle of the polygon at time t1 (in degrees) (default angle0)

    alpha1 : float
        alpha of the image at time t1 (0-255) (default alpha0)

    fontsize1 : float
        fontsize of text at time t1 (default: fontsize0)

    width1 : float
       width of the image to be displayed at time t1 (default: width0)

    height1 : float
       width of the image to be displayed at time t1 (default: height0)

    over3d : bool
        if True, this object will be rendered to the OpenGL window

        if False (default), the normal 2D plane will be used.

    Note
    ----
    one (and only one) of the following parameters is required:

         - circle0
         - image
         - line0
         - polygon0
         - rectangle0
         - text

    colors may be specified as a

        - valid colorname
        - hexname
        - tuple (R,G,B) or (R,G,B,A)
        - "fg" or "bg"

    colornames may contain an additional alpha, like ``red#7f``

    hexnames may be either 3 of 4 bytes long (``#rrggbb`` or ``#rrggbbaa``)

    both colornames and hexnames may be given as a tuple with an
    additional alpha between 0 and 255,
    e.g. ``(255,0,255,128)``, ("red",127)`` or ``("#ff00ff",128)``

    fg is the foreground color

    bg is the background color


    Permitted parameters

    ======================  ========= ========= ========= ========= ========= =========
    parameter               circle    image     line      polygon   rectangle text
    ======================  ========= ========= ========= ========= ========= =========
    parent                  -         -         -         -         -         -
    layer                   -         -         -         -         -         -
    keep                    -         -         -         -         -         -
    screen_coordinates      -         -         -         -         -         -
    xy_anchor               -         -         -         -         -         -
    t0,t1                   -         -         -         -         -         -
    x0,x1                   -         -         -         -         -         -
    y0,y1                   -         -         -         -         -         -
    offsetx0,offsetx1       -         -         -         -         -         -
    offsety0,offsety1       -         -         -         -         -         -
    circle0,circle1         -
    image                             -
    line0,line1                                 -
    polygon0,polygon1                                     -
    rectangle0,rectangle1                                           -
    text                                                                      -
    anchor                            -                                       -
    linewidth0,linewidth1    -                  -         -         -
    fillcolor0,fillcolor1    -                            -         -
    linecolor0,linecolor1    -                  -         -         -
    textcolor0,textcolor1                                                     -
    angle0,angle1                     -         -         -         -         -
    alpha0,alpha1                     -
    as_points                                   -         -         -
    font                                                                      -
    fontsize0,fontsize1                                                       -
    width0,width1                     -
    height0,height1                   -
    ======================  ========= ========= ========= ========= ========= =========
    """

    def __init__(
        self,
        parent: "Component" = None,
        layer: float = 0,
        keep: bool = True,
        visible: bool = True,
        screen_coordinates: bool = None,
        t0: float = None,
        x0: float = 0,
        y0: float = 0,
        offsetx0: float = 0,
        offsety0: float = 0,
        circle0: Union[float, Iterable] = None,
        line0: Iterable[float] = None,
        polygon0: Iterable[float] = None,
        rectangle0: Iterable[float] = None,
        points0: Iterable[float] = None,
        image: Any = None,
        text: str = None,
        font: Union[str, Iterable[str]] = "",
        anchor: str = "c",
        as_points: bool = False,
        max_lines: int = 0,
        text_anchor: str = None,
        linewidth0: float = None,
        fillcolor0: ColorType = None,
        linecolor0: ColorType = "fg",
        textcolor0: ColorType = "fg",
        angle0: float = 0,
        alpha0: float = 255,
        fontsize0: float = 20,
        width0: float = None,
        height0: float = None,
        t1: float = None,
        x1: float = None,
        y1: float = None,
        offsetx1: float = None,
        offsety1: float = None,
        circle1: Union[float, Iterable[float]] = None,
        line1: Iterable[float] = None,
        polygon1: Iterable[float] = None,
        rectangle1: Iterable[float] = None,
        points1: Iterable[float] = None,
        linewidth1: float = None,
        fillcolor1: ColorType = None,
        linecolor1: ColorType = None,
        textcolor1: ColorType = None,
        angle1: float = None,
        alpha1: float = None,
        fontsize1: float = None,
        width1: float = None,
        height1: float = None,
        xy_anchor: str = "",
        over3d: bool = None,
        flip_horizontal: bool = False,
        flip_vertical: bool = False,
        animation_start: float = None,
        animation_speed: float = 1,
        animation_repeat: bool = False,
        animation_pingpong: bool = False,
        animation_from: float = 0,
        animation_to: float = inf,
        env: "Environment" = None,
    ):
        self.env = _set_env(env)

        self._image_ident = None  # denotes no image yet
        self._image = None
        self._image_x = 0
        self._image_y = 0
        self.canvas_object = None
        self.over3d = _default_over3d if over3d is None else over3d
        self.screen_coordinates = screen_coordinates
        self.type = self.settype(circle0, line0, polygon0, rectangle0, points0, image, text)
        if self.type == "":
            raise ValueError("no object specified")
        type1 = self.settype(circle1, line1, polygon1, rectangle1, points1, None, None)
        if (type1 != "") and (type1 != self.type):
            raise TypeError("incompatible types: " + self.type + " and " + type1)

        self.layer0 = layer
        if parent is not None:
            if not isinstance(parent, Component):
                raise ValueError(repr(parent) + " is not a component")
            parent._animation_children.add(self)
        self.keep0 = keep
        self.visible0 = visible
        self.screen_coordinates = screen_coordinates
        self.sequence = self.env.serialize()

        self.circle0 = circle0
        self.line0 = de_none(line0)
        self.polygon0 = de_none(polygon0)
        self.rectangle0 = de_none(rectangle0)
        self.points0 = de_none(points0)
        self.text0 = text

        if image is None:
            self.width0 = 0  # just to be able to interpolat
            self.height0 = 0
        else:
            self.image0 = image
            self.width0 = width0  # None means original size
            self.height0 = height0

        self.as_points0 = as_points
        self.font0 = font
        self.max_lines0 = max_lines
        self.anchor0 = anchor
        if self.type == "text":
            if text_anchor is None:
                self.text_anchor0 = self.anchor0
            else:
                self.text_anchor0 = text_anchor
        self.anchor0 = anchor
        self.xy_anchor0 = xy_anchor

        self.x0 = x0
        self.y0 = y0
        self.offsetx0 = offsetx0
        self.offsety0 = offsety0

        if fillcolor0 is None:
            if self.as_points0:
                self.fillcolor0 = ""
            else:
                self.fillcolor0 = "fg"
        else:
            self.fillcolor0 = fillcolor0
        self.linecolor0 = linecolor0
        self.textcolor0 = textcolor0
        if linewidth0 is None:
            if self.as_points0:
                self.linewidth0 = 3
            else:
                if self.type == "line":
                    self.linewidth0 = 1
                else:
                    self.linewidth0 = 0
        else:
            self.linewidth0 = linewidth0
        self.angle0 = angle0
        self.alpha0 = alpha0
        self.fontsize0 = fontsize0

        self.t0 = self.env._now if t0 is None else t0

        self.circle1 = self.circle0 if circle1 is None else circle1
        self.line1 = self.line0 if line1 is None else de_none(line1)
        self.polygon1 = self.polygon0 if polygon1 is None else de_none(polygon1)
        self.rectangle1 = self.rectangle0 if rectangle1 is None else de_none(rectangle1)
        self.points1 = self.points0 if points1 is None else de_none(points1)

        self.x1 = self.x0 if x1 is None else x1
        self.y1 = self.y0 if y1 is None else y1
        self.offsetx1 = self.offsetx0 if offsetx1 is None else offsetx1
        self.offsety1 = self.offsety0 if offsety1 is None else offsety1
        self.fillcolor1 = self.fillcolor0 if fillcolor1 is None else fillcolor1
        self.linecolor1 = self.linecolor0 if linecolor1 is None else linecolor1
        self.textcolor1 = self.textcolor0 if textcolor1 is None else textcolor1
        self.linewidth1 = self.linewidth0 if linewidth1 is None else linewidth1
        self.angle1 = self.angle0 if angle1 is None else angle1
        self.alpha1 = self.alpha0 if alpha1 is None else alpha1
        self.fontsize1 = self.fontsize0 if fontsize1 is None else fontsize1
        self.width1 = self.width0 if width1 is None else width1
        self.height1 = self.height0 if height1 is None else height1
        self.t1 = inf if t1 is None else t1
        if self.env._animate_debug:
            self.caller = self.env._frame_to_lineno(_get_caller_frame(), add_filename=True)
        else:
            self.caller = "?. use env.animate_debug(True) to get the originating Animate location"
        """
        if over3d:
            self.env.an_objects_over3d.append(self)
        else:
            self.env.an_objects.append(self)
        """

        arg = None  # just to make Animate2dBase happy

        self.flip_horizontal0 = flip_horizontal
        self.flip_vertical0 = flip_vertical
        if animation_start is None:
            self.animation_start0 = self.env._now
        else:
            self.animation_start0 = animation_start
        self.animation_speed0 = animation_speed
        self.animation_repeat0 = animation_repeat
        self.animation_pingpong0 = animation_pingpong
        self.animation_from0 = animation_from
        self.animation_to0 = animation_to

        self.animation_object = AnimateClassic(master=self, locals_=locals())

    def update(
        self,
        layer=None,
        keep=None,
        visible=None,
        t0=None,
        x0=None,
        y0=None,
        offsetx0=None,
        offsety0=None,
        circle0=None,
        line0=None,
        polygon0=None,
        rectangle0=None,
        points0=None,
        image=None,
        text=None,
        font=None,
        anchor=None,
        xy_anchor0=None,
        max_lines=None,
        text_anchor=None,
        linewidth0=None,
        fillcolor0=None,
        linecolor0=None,
        textcolor0=None,
        angle0=None,
        alpha0=None,
        fontsize0=None,
        width0=None,
        height0=None,
        xy_anchor1=None,
        as_points=None,
        t1=None,
        x1=None,
        y1=None,
        offsetx1=None,
        offsety1=None,
        circle1=None,
        line1=None,
        polygon1=None,
        rectangle1=None,
        points1=None,
        linewidth1=None,
        fillcolor1=None,
        linecolor1=None,
        textcolor1=None,
        angle1=None,
        alpha1=None,
        fontsize1=None,
        width1=None,
        height1=None,
        flip_horizontal=None,
        flip_vertical=None,
        animation_start=None,
        animation_speed=None,
        animation_repeat=None,
        animation_pingpong=None,
        animation_from=None,
        animation_to=None,
    ):
        """
        updates an animation object

        Parameters
        ----------
        layer : int
            layer value

            lower layer values are on top of higher layer values (default see below)

        keep : bool
            keep

            if False, animation object is hidden after t1, shown otherwise
            (default see below)

        visible : bool
            visible

            if False, animation object is not shown, shown otherwise
            (default see below)

        xy_anchor : str
            specifies where x and y (i.e. x0, y0, x1 and y1) are relative to

            possible values are:

            ``nw    n    ne``

            ``w     c     e``

            ``sw    s    se``

            If null string, the given coordimates are used untranslated

            default see below

        t0 : float
            time of start of the animation (default: now)

        x0 : float
            x-coordinate of the origin at time t0 (default see below)

        y0 : float
            y-coordinate of the origin at time t0 (default see below)

        offsetx0 : float
            offsets the x-coordinate of the object at time t0 (default see below)

        offsety0 : float
            offsets the y-coordinate of the object at time t0 (default see below)

        circle0 : float or tuple/list
            the circle spec of the circle at time t0

            - radius

            - one item tuple/list containing the radius

            - five items tuple/list cntaining radius, radius1, arc_angle0, arc_angle1 and draw_arc
            (see class AnimateCircle for details)

        line0 : tuple
            the line(s) at time t0 (xa,ya,xb,yb,xc,yc, ...) (default see below)

        polygon0 : tuple
            the polygon at time t0 (xa,ya,xb,yb,xc,yc, ...)

            the last point will be auto connected to the start (default see below)

        rectangle0 : tuple
            the rectangle at time t0

            (xlowerleft,ylowerlef,xupperright,yupperright) (default see below)

            optionally a fifth element can be used to specify the radius of a rounded rectangle


        points0 : tuple
            the points(s) at time t0 (xa,ya,xb,yb,xc,yc, ...) (default see below)

        image : str or PIL image
            the image to be displayed

            This may be either a filename or a PIL image (default see below)

            if image is a string consisting of a zipfile-name, a bar (|) and a filename,
            the given filename will be read from the specified zip archive, e.g

        sim.AnimateImage(image="cars.zip|bmw.png")

        text : str
            the text to be displayed (default see below)

        font : str or list/tuple
            font to be used for texts

            Either a string or a list/tuple of fontnames. (default see below)
            If not found, uses calibri or arial

        max_lines : int
            the maximum of lines of text to be displayed

            if positive, it refers to the first max_lines lines

            if negative, it refers to the first -max_lines lines

            if zero (default), all lines will be displayed

        anchor : str
            anchor position

            specifies where to put images or texts relative to the anchor
            point (default see below)

            possible values are (default: c):

            ``nw    n    ne``

            ``w     c     e``

            ``sw    s    se``

        linewidth0 : float
            linewidth of the contour at time t0 (default see below)

        fillcolor0 : colorspec
            color of interior/text at time t0 (default see below)

        linecolor0 : colorspec
            color of the contour at time t0 (default see below)

        angle0 : float
            angle of the polygon at time t0 (in degrees) (default see below)

        fontsize0 : float
            fontsize of text at time t0 (default see below)

        width0 : float
            width of the image to be displayed at time t0 (default see below)

            if None, the original width of the image will be used

        height0 : float
            height of the image to be displayed at time t0 (default see below)

            if None, the original height of the image will be used

        t1 : float
            time of end of the animation (default: inf)

            if keep=True, the animation will continue (frozen) after t1

        x1 : float
            x-coordinate of the origin at time t1 (default x0)

        y1 : float
            y-coordinate of the origin at time t1 (default y0)

        offsetx1 : float
            offsets the x-coordinate of the object at time t1 (default offsetx0)

        offsety1 : float
            offsets the y-coordinate of the object at time t1 (default offset0)

        circle1 : float or tuple/ist
            the circle spec of the circle at time t1

            - radius

            - one item tuple/list containing the radius

            - five items tuple/list cntaining radius, radius1, arc_angle0, arc_angle1 and draw_arc
            (see class AnimateCircle for details)

        line1 : tuple
            the line(s) at time t1 (xa,ya,xb,yb,xc,yc, ...) (default: line0)

            should have the same number of elements as line0

        polygon1 : tuple
            the polygon at time t1 (xa,ya,xb,yb,xc,yc, ...) (default: polygon0)

            should have the same number of elements as polygon0

        rectangle1 : tuple
            the rectangle at time t (xlowerleft,ylowerleft,xupperright,yupperright)
            (default: rectangle0)

            optionally a fifth element can be used to specify the radius of a rounded rectangle

        points1 : tuple
            the points(s) at time t1 (xa,ya,xb,yb,xc,yc, ...) (default: points0)

            should have the same number of elements as points1

        linewidth1 : float
            linewidth of the contour at time t1 (default linewidth0)

        fillcolor1 : colorspec
            color of interior/text at time t1 (default fillcolor0)

        linecolor1 : colorspec
            color of the contour at time t1 (default linecolor0)

        angle1 : float
            angle of the polygon at time t1 (in degrees) (default angle0)

        fontsize1 : float
            fontsize of text at time t1 (default: fontsize0)

        width1 : float
           width of the image to be displayed at time t1 (default: width0)

        height1 : float
           height of the image to be displayed at time t1 (default: height0)

        Note
        ----
        The type of the animation cannot be changed with this method.

        The default value of most of the parameters is the current value (at time now)
        """

        t = self.env._now
        type0 = self.settype(circle0, line0, polygon0, rectangle0, points0, image, text)
        if (type0 != "") and (type0 != self.type):
            raise TypeError("incorrect type " + type0 + " (should be " + self.type)
        type1 = self.settype(circle1, line1, polygon1, rectangle1, points1, None, None)
        if (type1 != "") and (type1 != self.type):
            raise TypeError("incompatible types: " + self.type + " and " + type1)

        if layer is not None:
            self.layer0 = layer
        if keep is not None:
            self.keep0 = keep
        if visible is not None:
            self.visible0 = visible
        self.circle0 = self.circle() if circle0 is None else circle0
        self.line0 = self.line() if line0 is None else de_none(line0)
        self.polygon0 = self.polygon() if polygon0 is None else de_none(polygon0)
        self.rectangle0 = self.rectangle() if rectangle0 is None else de_none(rectangle0)
        self.points0 = self.points() if points0 is None else de_none(points0)
        if as_points is not None:
            self.as_points0 = as_points
        if text is not None:
            self.text0 = text
        if max_lines is not None:
            self.max_lines0 = max_lines

        self.width0 = self.width() if width0 is None else width0
        self.height0 = self.height() if height0 is None else height0

        if image is not None:
            self.image0 = image

        if font is not None:
            self.font0 = font
        if anchor is not None:
            self.anchor0 = anchor
            if self.type == "text":
                if text_anchor is not None:
                    self.text_anchor0 = text_anchor
        if text_anchor is not None:
            self.text_anchor0 = text_anchor

        self.x0 = self.x(t) if x0 is None else x0
        self.y0 = self.y(t) if y0 is None else y0
        self.offsetx0 = self.offsetx(t) if offsetx0 is None else offsetx0
        self.offsety0 = self.offsety(t) if offsety0 is None else offsety0

        self.fillcolor0 = self.fillcolor(t) if fillcolor0 is None else fillcolor0
        self.linecolor0 = self.linecolor(t) if linecolor0 is None else linecolor0
        self.textcolor0 = self.textcolor(t) if textcolor0 is None else textcolor0
        self.linewidth0 = self.linewidth(t) if linewidth0 is None else linewidth0
        self.angle0 = self.angle(t) if angle0 is None else angle0
        self.alpha0 = self.alpha(t) if alpha0 is None else alpha0
        self.fontsize0 = self.fontsize(t) if fontsize0 is None else fontsize0
        self.t0 = self.env._now if t0 is None else t0
        self.xy_anchor0 = self.xy_anchor(t) if xy_anchor0 is None else xy_anchor0

        self.circle1 = self.circle0 if circle1 is None else circle1
        self.line1 = self.line0 if line1 is None else de_none(line1)
        self.polygon1 = self.polygon0 if polygon1 is None else de_none(polygon1)
        self.rectangle1 = self.rectangle0 if rectangle1 is None else de_none(rectangle1)
        self.points1 = self.points0 if points1 is None else de_none(points1)

        self.x1 = self.x0 if x1 is None else x1
        self.y1 = self.y0 if y1 is None else y1
        self.offsetx1 = self.offsetx0 if offsetx1 is None else offsetx1
        self.offsety1 = self.offsety0 if offsety1 is None else offsety1
        self.fillcolor1 = self.fillcolor0 if fillcolor1 is None else fillcolor1
        self.linecolor1 = self.linecolor0 if linecolor1 is None else linecolor1
        self.textcolor1 = self.textcolor0 if textcolor1 is None else textcolor1
        self.linewidth1 = self.linewidth0 if linewidth1 is None else linewidth1
        self.angle1 = self.angle0 if angle1 is None else angle1
        self.alpha1 = self.alpha0 if alpha1 is None else alpha1
        self.fontsize1 = self.fontsize0 if fontsize1 is None else fontsize1
        self.width1 = self.width0 if width1 is None else width1
        self.height1 = self.height0 if height1 is None else height1
        self.xy_anchor1 = self.xy_anchor0 if xy_anchor1 is None else xy_anchor1

        self.t1 = inf if t1 is None else t1
        if flip_horizontal is not None:
            self.flip_horizontal0 = flip_horizontal
        if flip_vertical is not None:
            self.flip_vertical0 = flip_vertical
        if animation_start is not None:
            self.animation_start0 = animation_start
        if animation_speed is not None:
            self.animation_speed0 = animation_speed
        if animation_repeat is not None:
            self.animation_repeat0 = animation_repeat
        if animation_pingpong is not None:
            self.animation_pingpong0 = animation_pingpong
        if animation_from is not None:
            self.animation_from0 = animation_from
        if animation_to is not None:
            self.animation_to0 = animation_to

    def show(self):
        self.animation_object.show()

    def remove(self):
        """
        removes the animation object from the animation queue,
        so effectively ending this animation.

        Note
        ----
        The animation object might be still updated, if required
        """
        self.animation_object.remove()

    def is_removed(self):
        return self.animation_object.is_removed()

    def x(self, t=None):
        """
        x-position of an animate object. May be overridden.

        Parameters
        ----------
        t : float
            current time

        Returns
        -------
        x : float
            default behaviour: linear interpolation between self.x0 and self.x1
        """
        return interpolate((self.env._now if t is None else t), self.t0, self.t1, self.x0, self.x1)

    def y(self, t=None):
        """
        y-position of an animate object. May be overridden.

        Parameters
        ----------
        t : float
            current time

        Returns
        -------
        y : float
            default behaviour: linear interpolation between self.y0 and self.y1
        """
        return interpolate((self.env._now if t is None else t), self.t0, self.t1, self.y0, self.y1)

    def offsetx(self, t=None):
        """
        offsetx of an animate object. May be overridden.

        Parameters
        ----------
        t : float
            current time

        Returns
        -------
        offsetx : float
            default behaviour: linear interpolation between self.offsetx0 and self.offsetx1
        """
        return interpolate((self.env._now if t is None else t), self.t0, self.t1, self.offsetx0, self.offsetx1)

    def offsety(self, t=None):
        """
        offsety of an animate object. May be overridden.

        Parameters
        ----------
        t : float
            current time

        Returns
        -------
        offsety : float
            default behaviour: linear interpolation between self.offsety0 and self.offsety1
        """
        return interpolate((self.env._now if t is None else t), self.t0, self.t1, self.offsety0, self.offsety1)

    def angle(self, t=None):
        """
        angle of an animate object. May be overridden.

        Parameters
        ----------
        t : float
            current time

        Returns
        -------
        angle : float
            default behaviour: linear interpolation between self.angle0 and self.angle1
        """
        return interpolate((self.env._now if t is None else t), self.t0, self.t1, self.angle0, self.angle1)

    def alpha(self, t=None):
        """
        alpha of an animate object. May be overridden.

        Parameters
        ----------
        t : float
            current time

        Returns
        -------
        alpha : float
            default behaviour: linear interpolation between self.alpha0 and self.alpha1
        """
        return interpolate((self.env._now if t is None else t), self.t0, self.t1, self.alpha0, self.alpha1)

    def linewidth(self, t=None):
        """
        linewidth of an animate object. May be overridden.

        Parameters
        ----------
        t : float
            current time

        Returns
        -------
        linewidth : float
            default behaviour: linear interpolation between self.linewidth0 and self.linewidth1
        """
        return interpolate((self.env._now if t is None else t), self.t0, self.t1, self.linewidth0, self.linewidth1)

    def linecolor(self, t=None):
        """
        linecolor of an animate object. May be overridden.

        Parameters
        ----------
        t : float
            current time

        Returns
        -------
        linecolor : colorspec
            default behaviour: linear interpolation between self.linecolor0 and self.linecolor1
        """
        return self.env.colorinterpolate((self.env._now if t is None else t), self.t0, self.t1, self.linecolor0, self.linecolor1)

    def fillcolor(self, t=None):
        """
        fillcolor of an animate object. May be overridden.

        Parameters
        ----------
        t : float
            current time

        Returns
        -------
        fillcolor : colorspec
            default behaviour: linear interpolation between self.fillcolor0 and self.fillcolor1
        """
        return self.env.colorinterpolate((self.env._now if t is None else t), self.t0, self.t1, self.fillcolor0, self.fillcolor1)

    def circle(self, t=None):
        """
        circle of an animate object. May be overridden.

        Parameters
        ----------
        t : float
            current time

        Returns
        -------
        circle : float or tuple/list
            either

            - radius

            - one item tuple/list containing the radius

            - five items tuple/list cntaining radius, radius1, arc_angle0, arc_angle1 and draw_arc

            (see class AnimateCircle for details)

            default behaviour: linear interpolation between self.circle0 and self.circle1
        """
        return interpolate((self.env._now if t is None else t), self.t0, self.t1, self.circle0, self.circle1)

    def textcolor(self, t=None):
        """
        textcolor of an animate object. May be overridden.

        Parameters
        ----------
        t : float
            current time

        Returns
        -------
        textcolor : colorspec
            default behaviour: linear interpolation between self.textcolor0 and self.textcolor1
        """
        return self.env.colorinterpolate((self.env._now if t is None else t), self.t0, self.t1, self.textcolor0, self.textcolor1)

    def line(self, t=None):
        """
        line of an animate object. May be overridden.

        Parameters
        ----------
        t : float
            current time

        Returns
        -------
        line : tuple
            series of x- and y-coordinates (xa,ya,xb,yb,xc,yc, ...)

            default behaviour: linear interpolation between self.line0 and self.line1
        """
        return interpolate((self.env._now if t is None else t), self.t0, self.t1, self.line0, self.line1)

    def polygon(self, t=None):
        """
        polygon of an animate object. May be overridden.

        Parameters
        ----------
        t : float
            current time

        Returns
        -------
        polygon: tuple
            series of x- and y-coordinates describing the polygon (xa,ya,xb,yb,xc,yc, ...)

            default behaviour: linear interpolation between self.polygon0 and self.polygon1
        """
        return interpolate((self.env._now if t is None else t), self.t0, self.t1, self.polygon0, self.polygon1)

    def rectangle(self, t=None):
        """
        rectangle of an animate object. May be overridden.

        Parameters
        ----------
        t : float
            current time

        Returns
        -------
        rectangle: tuple
            (xlowerleft,ylowerlef,xupperright,yupperright)

            default behaviour: linear interpolation between self.rectangle0 and self.rectangle1
        """
        return interpolate((self.env._now if t is None else t), self.t0, self.t1, self.rectangle0, self.rectangle1)

    def points(self, t=None):
        """
        points of an animate object. May be overridden.

        Parameters
        ----------
        t : float
            current time

        Returns
        -------
        points : tuple
            series of x- and y-coordinates (xa,ya,xb,yb,xc,yc, ...)

            default behaviour: linear interpolation between self.points0 and self.points1
        """
        return interpolate((self.env._now if t is None else t), self.t0, self.t1, self.points0, self.points1)

    def width(self, t=None):
        """
        width of an animated image object. May be overridden.

        Parameters
        ----------
        t : float
            current time

        Returns
        -------
        width : float
            default behaviour: linear interpolation between self.width0 and self.width1

            if None, the original width of the image will be used
        """
        width0 = self.width0
        width1 = self.width1
        if width0 is None and width1 is None:
            return None
        if width0 is None:
            width0 = ImageContainer(self.image0).images[0].size[0]
        if width1 is None:
            width1 = ImageContainer(self.image1).images[0].size[0]

        return interpolate((self.env._now if t is None else t), self.t0, self.t1, width0, width1)

    def height(self, t=None):
        """
        height of an animated image object. May be overridden.

        Parameters
        ----------
        t : float
            current time

        Returns
        -------
        height : float
            default behaviour: linear interpolation between self.height0 and self.height1

            if None, the original height of the image will be used
        """
        height0 = self.height0
        height1 = self.height1
        if height0 is None and height1 is None:
            return None
        if height0 is None:
            height0 = ImageContainer(self.image0).images[0].size[0]
        if height1 is None:
            height1 = ImageContainer(self.image1).images[0].size[0]

        return interpolate((self.env._now if t is None else t), self.t0, self.t1, height0, height1)

    def fontsize(self, t=None):
        """
        fontsize of an animate object. May be overridden.

        Parameters
        ----------
        t : float
            current time

        Returns
        -------
        fontsize : float
            default behaviour: linear interpolation between self.fontsize0 and self.fontsize1
        """
        return interpolate((self.env._now if t is None else t), self.t0, self.t1, self.fontsize0, self.fontsize1)

    def as_points(self, t=None):
        """
        as_points of an animate object. May be overridden.

        Parameters
        ----------
        t : float
            current time

        Returns
        -------
        as_points : bool
            default behaviour: self.as_points (text given at creation or update)
        """
        return self.as_points0

    def text(self, t=None):
        """
        text of an animate object. May be overridden.

        Parameters
        ----------
        t : float
            current time

        Returns
        -------
        text : str
            default behaviour: self.text0 (text given at creation or update)
        """
        return self.text0

    def max_lines(self, t=None):
        """
        maximum number of lines to be displayed of text. May be overridden.

        Parameters
        ----------
        t : float
            current time

        Returns
        -------
        max_lines : int
            default behaviour: self.max_lines0 (max_lines given at creation or update)
        """
        return self.max_lines0

    def anchor(self, t=None):
        """
        anchor of an animate object. May be overridden.

        Parameters
        ----------
        t : float
            current time

        Returns
        -------
        anchor : str
            default behaviour: self.anchor0 (anchor given at creation or update)
        """

        return self.anchor0

    def text_anchor(self, t=None):
        """
        text_anchor of an animate object. May be overridden.

        Parameters
        ----------
        t : float
            current time

        Returns
        -------
        text_anchor : str
            default behaviour: self.text_anchor0 (text_anchor given at creation or update)
        """

        return self.text_anchor0

    def layer(self, t=None):
        """
        layer of an animate object. May be overridden.

        Parameters
        ----------
        t : float
            current time

        Returns
        -------
        layer : int or float
            default behaviour: self.layer0 (layer given at creation or update)
        """
        return self.layer0

    def font(self, t=None):
        """
        font of an animate object. May be overridden.

        Parameters
        ----------
        t : float
            current time

        Returns
        -------
        font : str
            default behaviour: self.font0 (font given at creation or update)
        """
        return self.font0

    def xy_anchor(self, t=None):
        """
        xy_anchor attribute of an animate object. May be overridden.

        Parameters
        ----------
        t : float
            current time

        Returns
        -------
        xy_anchor : str
            default behaviour: self.xy_anchor0 (xy_anchor given at creation or update)
        """
        return self.xy_anchor0

    def visible(self, t=None):
        """
        visible attribute of an animate object. May be overridden.

        Parameters
        ----------
        t : float
            current time

        Returns
        -------
        visible : bool
            default behaviour: self.visible0 and t >= self.t0 (visible given at creation or update)
        """
        return self.visible0 and t >= self.t0

    def flip_horizontal(self, t=None):
        return self.flip_horizontal0

    def flip_vertical(self, t=None):
        return self.flip_vertical0

    def animation_start(self, t=None):
        return self.env._now

    def animation_repeat(self, t=None):
        return False

    def animation_pingpong(self, t=None):
        return False

    def animation_speed(self, t=None):
        return 1

    def animation_from(self, t=None):
        return 0

    def animation_to(self, t=None):
        return inf

    def keep(self, t):
        """
        keep attribute of an animate object. May be overridden.

        Parameters
        ----------
        t : float
            current time

        Returns
        -------
        keep : bool
            default behaviour: self.keep0 or t <= self.t1 (visible given at creation or update)
        """
        return self.keep0 or t <= self.t1

    def image(self, t=None):
        """
        image of an animate object. May be overridden.

        Parameters
        ----------
        t : float
            current time

        Returns
        -------
        image : PIL.Image.Image
            default behaviour: self.image0 (image given at creation or update)
        """
        return self.image0

    def settype(self, circle, line, polygon, rectangle, points, image, text):
        n = 0
        t = ""
        if circle is not None:
            t = "circle"
            n += 1
        if line is not None:
            t = "line"
            n += 1
        if polygon is not None:
            t = "polygon"
            n += 1
        if rectangle is not None:
            t = "rectangle"
            n += 1
        if points is not None:
            t = "points"
            n += 1
        if image is not None:
            t = "image"
            n += 1
        if text is not None:
            t = "text"
            n += 1
        if n >= 2:
            raise ValueError("more than one object given")
        return t

    def remove_background(self, im):
        pixels = im.load()
        background = pixels[0, 0]
        imagewidth, imageheight = im.size
        for y in range(imageheight):
            for x in range(imagewidth):
                if abs(pixels[x, y][0] - background[0]) < 10:
                    if abs(pixels[x, y][1] - background[1]) < 10:
                        if abs(pixels[x, y][2] - background[2]) < 10:
                            pixels[x, y] = (255, 255, 255, 0)


class AnimateEntry:
    """
    defines a button

    Parameters
    ----------
    x : int
        x-coordinate of centre of the button in screen coordinates (default 0)

    y : int
        y-coordinate of centre of the button in screen coordinates (default 0)

    number_of_chars : int
        number of characters displayed in the entry field (default 20)

    fillcolor : colorspec
        color of the entry background (default foreground_color)

    color : colorspec
        color of the text (default background_color)

    value : str
        initial value of the text of the entry (default null string)


    action :  function
        action to take when the Enter-key is pressed

        the function should have no arguments


    xy_anchor : str
        specifies where x and y are relative to

        possible values are (default: sw):

        ``nw    n    ne``

        ``w     c     e``

        ``sw    s    se``

    env : Environment
        environment where the component is defined

        if omitted, default_env will be used

    Note
    ----
    All measures are in screen coordinates

    This class is not available under Pythonista.
    """

    def __init__(
        self,
        x: float = 0,
        y: float = 0,
        number_of_chars: int = 20,
        value: str = "",
        fillcolor: ColorType = "fg",
        color: ColorType = "bg",
        action: Callable = None,
        env: "Environment" = None,
        xy_anchor: str = "sw",
    ):
        self.env = _set_env(env)

        self.env.ui_objects.append(self)
        self.type = "entry"
        self.value = value
        self.sequence = self.env.serialize()
        self.x = x
        self.y = y
        self.number_of_chars = number_of_chars
        self.fillcolor = self.env.colorspec_to_tuple(fillcolor)
        self.color = self.env.colorspec_to_tuple(color)
        self.action = action
        self.xy_anchor = xy_anchor
        self.installed = False

    def install(self):
        x = self.x + self.env.xy_anchor_to_x(self.xy_anchor, screen_coordinates=True)
        y = self.y + self.env.xy_anchor_to_y(self.xy_anchor, screen_coordinates=True)

        self.entry = tkinter.Entry(self.env.root)
        self.entry.configure(
            width=self.number_of_chars,
            foreground=self.env.colorspec_to_hex(self.color, False),
            background=self.env.colorspec_to_hex(self.fillcolor, False),
            relief=tkinter.FLAT,
        )
        self.entry.bind("<Return>", self.on_enter)
        self.entry_window = g.canvas.create_window(x, self.env._height - y, anchor=tkinter.SW, window=self.entry)
        self.entry.insert(0, self.value)
        self.installed = True

    def on_enter(self, ev):
        if self.action is not None:
            self.action()

    def get(self):
        """
        get the current value of the entry

        Returns
        -------
        Current value of the entry : str
        """
        return self.entry.get()

    def remove(self):
        """
        removes the entry object.

        the ui object is removed from the ui queue,
        so effectively ending this ui
        """
        if self in self.env.ui_objects:
            self.env.ui_objects.remove(self)
        if self.installed:
            self.entry.destroy()
            self.installed = False


class AnimateButton:
    """
    defines a button

    Parameters
    ----------
    x : int
        x-coordinate of centre of the button in screen coordinates (default 0)

    y : int
        y-coordinate of centre of the button in screen coordinates (default 0)

    width : int
        width of button in screen coordinates (default 80)

    height : int
        height of button in screen coordinates (default 30)

    linewidth : int
        width of contour in screen coordinates (default 0=no contour)

    fillcolor : colorspec
        color of the interior (foreground_color)

    linecolor : colorspec
        color of contour (default foreground_color)

    color : colorspec
        color of the text (default background_color)

    text : str or function
        text of the button (default null string)

        if text is an argumentless function, this will be called each time;
        the button is shown/updated

    font : str
        font of the text (default Calibri)

    fontsize : int
        fontsize of the text (default 15)

    action :  function
        action to take when button is pressed

        executed when the button is pressed (default None)
        the function should have no arguments


    xy_anchor : str
        specifies where x and y are relative to

        possible values are (default: sw):

        ``nw    n    ne``

        ``w     c     e``

        ``sw    s    se``

    env : Environment
        environment where the component is defined

        if omitted, default_env will be used

    Note
    ----
    All measures are in screen coordinates

    On Pythonista, this functionality is emulated by salabim
    On other platforms, the tkinter functionality is used.
    """

    def __init__(
        self,
        x: float = 0,
        y: float = 0,
        width: int = 80,
        fillcolor: ColorType = "fg",
        color: ColorType = "bg",
        text: str = "",
        font: str = "",
        fontsize: int = 15,
        action: Callable = None,
        env: "Environment" = None,
        xy_anchor: str = "sw",
    ):
        self.env = _set_env(env)

        self.type = "button"
        self.t0 = -inf
        self.t1 = inf
        self.x0 = 0
        self.y0 = 0
        self.x1 = 0
        self.y1 = 0
        self.sequence = self.env.serialize()
        self.height = 30
        self.x = x - width / 2
        self.y = y - self.height / 2
        self.width = width
        self.fillcolor = self.env.colorspec_to_tuple(fillcolor)
        self.linecolor = self.env.colorspec_to_tuple("fg")
        self.color = self.env.colorspec_to_tuple(color)
        self.linewidth = 0
        self.font = font
        self.fontsize = fontsize
        self.text0 = text
        self.lasttext = "*"
        self.action = action
        self.xy_anchor = xy_anchor

        self.env.ui_objects.append(self)
        self.installed = False

    in_topleft: bool

    def text(self):
        return self.text0

    def install(self):
        if not Pythonista:
            x = self.x + self.env.xy_anchor_to_x(self.xy_anchor, screen_coordinates=True)
            y = self.y + self.env.xy_anchor_to_y(self.xy_anchor, screen_coordinates=True)
            if Chromebook:  # the Chromebook settings are not accurate for anything else than the menu buttons
                my_font = tkinter.font.Font(size=int(self.fontsize * 0.45))
                my_width = int(0.6 * self.width / self.fontsize)
                y = y + 8
            else:
                my_font = tkinter.font.Font(size=int(self.fontsize * 0.7))
                my_width = int(1.85 * self.width / self.fontsize)

            self.button = tkinter.Button(self.env.root, text=self.lasttext, command=self.action, anchor=tkinter.CENTER)
            self.button.configure(
                font=my_font,
                width=my_width,
                foreground=self.env.colorspec_to_hex(self.color, False),
                background=self.env.colorspec_to_hex(self.fillcolor, False),
                relief=tkinter.FLAT,
            )
            self.button_window = g.canvas.create_window(x + self.width, self.env._height - y - self.height, anchor=tkinter.NE, window=self.button)
        self.installed = True

    def remove(self):
        """
        removes the button object.

        the ui object is removed from the ui queue,
        so effectively ending this ui
        """
        if self in self.env.ui_objects:
            self.env.ui_objects.remove(self)
        if self.installed:
            if not Pythonista:
                self.button.destroy()
            self.installed = False


class AnimateSlider:
    """
    defines a slider

    Parameters
    ----------
    x : int
        x-coordinate of centre of the slider in screen coordinates (default 0)

    y : int
        y-coordinate of centre of the slider in screen coordinates (default 0)

    vmin : float
        minimum value of the slider (default 0)

    vmax : float
        maximum value of the slider (default 0)

    v : float
        initial value of the slider (default 0)

        should be between vmin and vmax

    resolution : float
        step size of value (default 1)

    width : float
        width of slider in screen coordinates (default 100)

    height : float
        height of slider in screen coordinates (default 20)

    foreground_color : colorspec
        color of the foreground (default "fg")

    background_color : colorspec
        color of the backgroundground (default "bg")

    trough_color : colorspec
        color of the trough (default "lightgrey")

    show_value : boolean
        if True (default), show values; if False don't show values

    label : str
        label if the slider (default null string)


    font : str
         font of the text (default Helvetica)

    fontsize : int
         fontsize of the text (default 12)

    action : function
         function executed when the slider value is changed (default None)

         the function should have one argument, being the new value

         if None (default), no action

    xy_anchor : str
        specifies where x and y are relative to

        possible values are (default: sw):

        ``nw    n    ne``

        ``w     c     e``

        ``sw    s    se``

    env : Environment
        environment where the component is defined

        if omitted, default_env will be used

    Note
    ----
    The current value of the slider is the v attibute of the slider.

    All measures are in screen coordinates

    On Pythonista, this functionality is emulated by salabim
    On other platforms, the tkinter functionality is used.
    """

    def __init__(
        self,
        x: float = 0,
        y: float = 0,
        width: int = 100,
        height: int = 20,
        vmin: float = 0,
        vmax: float = 10,
        v: float = None,
        resolution: float = 1,
        background_color: ColorType = "bg",
        foreground_color: ColorType = "fg",
        trough_color: ColorType = "lightgray",
        show_value: bool = True,
        label: str = "",
        font: str = "",
        fontsize: int = 12,
        action: Callable = None,
        xy_anchor: str = "sw",
        env: "Environment" = None,
        linecolor: ColorType = None,  # only for backward compatibility
        labelcolor: ColorType = None,  # only for backward compatibility
        layer: float = None,  # only for backward compatibility
    ):
        self.env = _set_env(env)
        n = round((vmax - vmin) / resolution) + 1
        self.vmin = vmin
        self.vmax = vmin + (n - 1) * resolution
        self._v = vmin if v is None else v
        self.xdelta = width / n
        self.resolution = resolution

        self.type = "slider"
        self.t0 = -inf
        self.t1 = inf
        self.x0 = 0
        self.y0 = 0
        self.x1 = 0
        self.y1 = 0
        self.sequence = self.env.serialize()
        self.x = x
        self.y = y - fontsize
        self.width = width
        self.height = height
        self.background_color = background_color
        self.foreground_color = foreground_color
        self.trough_color = trough_color
        self.show_value = show_value
        self.font = font
        self.fontsize = fontsize
        self._label = label
        self.action = action
        self.installed = False
        self.xy_anchor = xy_anchor

        if Pythonista:
            self.y = self.y - height * 1.5

        self.env.ui_objects.append(self)

    def v(self, value=None):
        """
        value

        Parameters
        ----------
        value: float
            new value

            if omitted, no change

        Returns
        -------
        Current value of the slider : float
        """
        if value is not None:
            if self.installed:
                if Pythonista:
                    self._v = value
                    if self.action is not None:
                        self.action(str(value))
                else:
                    self.slider.set(value)
            else:
                self._v = value

        if Pythonista:
            return repr(self._v)
        else:
            if self.installed:
                return self.slider.get()
            else:
                return self._v

    def label(self, text=None):
        if text is not None:
            self._label = text
            if hasattr(self, "slider"):
                self.slider.config(label=self._label)
        return self._label

    def install(self):
        if not Pythonista:
            x = self.x + self.env.xy_anchor_to_x(self.xy_anchor, screen_coordinates=True)
            y = self.y + self.env.xy_anchor_to_y(self.xy_anchor, screen_coordinates=True)
            self.slider = tkinter.Scale(
                self.env.root,
                from_=self.vmin,
                to=self.vmax,
                orient=tkinter.HORIZONTAL,
                resolution=self.resolution,
                command=self.action,
                length=self.width,
                width=self.height,
            )
            self.slider.window = g.canvas.create_window(x, self.env._height - y, anchor=tkinter.NW, window=self.slider)
            self.slider.config(
                font=(self.font, int(self.fontsize * 0.8)),
                foreground=self.env.colorspec_to_hex(self.env.colorspec_to_tuple(self.foreground_color), False),
                background=self.env.colorspec_to_hex(self.env.colorspec_to_tuple(self.background_color), False),
                highlightbackground=self.env.colorspec_to_hex(self.env.colorspec_to_tuple(self.background_color), False),
                troughcolor=self.env.colorspec_to_hex(self.env.colorspec_to_tuple(self.trough_color), False),
                showvalue=self.show_value,
                label=self._label,
            )

        self.installed = True
        self.v(self._v)

    def remove(self):
        """
        removes the slider object

        The ui object is removed from the ui queue,
        so effectively ending this ui
        """
        if self in self.env.ui_objects:
            self.env.ui_objects.remove(self)
        if self.installed:
            self.slider.destroy()
            if not Pythonista:
                self.slider.quit()
            self.installed = False


class AnimateQueue(DynamicClass):
    """
    Animates the component in a queue.

    Parameters
    ----------
    queue : Queue
        queue it concerns

    x : float
        x-position of the first component in the queue

        default: 50

    y : float
        y-position of the first component in the queue

        default: 50

    direction : str
        if "w", waiting line runs westwards (i.e. from right to left)

        if "n", waiting line runs northeards (i.e. from bottom to top)

        if "e", waiting line runs eastwards (i.e. from left to right) (default)

        if "s", waiting line runs southwards (i.e. from top to bottom)

    trajectory : Trajectory
        trajectory to be followed. Overrides any given directory

    reverse : bool
        if False (default), display in normal order. If True, reversed.

    max_length : int
        maximum number of components to be displayed

    xy_anchor : str
        specifies where x and y are relative to

        possible values are (default: sw):

        ``nw    n    ne``

        ``w     c     e``

        ``sw    s    se``

    titlecolor : colorspec
        color of the title (default foreground color)

    titlefont : font
        font of the title (default null string)

    titlefontsize : int
        size of the font of the title (default 15)

    title : str
        title to be shown above queue

        default: name of the queue

    titleoffsetx : float
        x-offset of the title relative to the start of the queue

        default: 25 if direction is w, -25 otherwise

    titleoffsety : float
        y-offset of the title relative to the start of the queue

        default: -25 if direction is s, -25 otherwise

    id : any
        the animation works by calling the animation_objects method of each component, optionally
        with id. By default, this is self, but can be overriden, particularly with the queue

    arg : any
        this is used when a parameter is a function with two parameters, as the first argument or
        if a parameter is a method as the instance

        default: self (instance itself)

    visible : bool
        if False, nothing will be shown

        (default True)

    keep : bool
        if False, animation object will be taken from the animation objects. With show(), the animation can be reshown.
        (default True)

    parent : Component
        component where this animation object belongs to (default None)

        if given, the animation object will be removed
        automatically when the parent component is no longer accessible

    Note
    ----
    All measures are in screen coordinates


    All parameters, apart from queue, id, arg and parent can be specified as:

    - a scalar, like 10

    - a function with zero arguments, like lambda: title

    - a function with one argument, being the time t, like lambda t: t + 10

    - a function with two parameters, being arg (as given) and the time, like lambda comp, t: comp.state

    - a method instance arg for time t, like self.state, actually leading to arg.state(t) to be called
    """

    def __init__(
        self,
        queue,
        x=50,
        y=50,
        direction="w",
        trajectory=None,
        max_length=None,
        xy_anchor="sw",
        reverse=False,
        title=None,
        titlecolor="fg",
        titlefontsize=15,
        titlefont="",
        titleoffsetx=None,
        titleoffsety=None,
        layer=0,
        id=None,
        arg=None,
        parent=None,
        over3d=None,
        keep=True,
        visible=True,
        screen_coordinates=True,
    ):
        super().__init__()
        _checkisqueue(queue)
        self._queue = queue
        self.xy_anchor = xy_anchor
        self.x = x
        self.y = y
        self.id = self if id is None else id
        self.arg = self if arg is None else arg
        self.max_length = max_length
        self.direction = direction
        self.reverse = reverse
        self.current_aos = {}
        if parent is not None:
            if not isinstance(parent, Component):
                raise ValueError(repr(parent) + " is not a component")
            parent._animation_children.add(self)
        self.env = queue.env

        self.titleoffsetx = titleoffsetx
        self.titleoffsety = titleoffsety
        self.titlefont = titlefont
        self.titlefontsize = titlefontsize
        self.titlecolor = titlecolor
        self.title = queue.name() if title is None else title
        self.layer = layer
        self.visible = visible
        self.keep = keep
        self.over3d = _default_over3d if over3d is None else over3d
        self.trajectory = trajectory
        self.screen_coordinates = screen_coordinates
        self.register_dynamic_attributes(
            "xy_anchor x y id max_length direction reverse titleoffsetx titleoffsety titlefont titlefontsize titlecolor title layer visible keep trajectory"
        )

        self.ao_title = AnimateText(
            text=lambda t: self.title(t),
            textcolor=lambda t: self.titlecolor(t),
            x=lambda: self.x_t,
            y=lambda: self.y_t,
            text_anchor=lambda: self.text_anchor_t,
            angle=lambda: self.angle_t,
            screen_coordinates=self.screen_coordinates,
            fontsize=lambda t: self.titlefontsize(t),
            font=lambda t: self.titlefont(t),
            layer=lambda t: self.layer(t),
            over3d=self.over3d,
            visible=lambda: self.visible_t,
        )
        self.show()

    def update(self, t):
        if not self.keep(t):
            self.remove()
            return
        prev_aos = self.current_aos
        self.current_aos = {}
        xy_anchor = self.xy_anchor(t)
        max_length = self.max_length(t)
        direction = self.direction(t).lower()
        if self.trajectory(t) is None:
            x = self.x(t)
            y = self.y(t)
        else:
            direction = "t"
            x = 0
            y = 0
            xt = self.x(t)
            yt = self.y(t)
            trajectory = self.trajectory(t)

        reverse = self.reverse(t)
        self.visible_t = self.visible(t)
        titleoffsetx = self.titleoffsetx(t)
        titleoffsety = self.titleoffsety(t)

        x += self._queue.env.xy_anchor_to_x(xy_anchor, screen_coordinates=True, over3d=self.over3d)
        y += self._queue.env.xy_anchor_to_y(xy_anchor, screen_coordinates=True, over3d=self.over3d)

        if direction == "e":
            self.x_t = x + (-25 if titleoffsetx is None else titleoffsetx)
            self.y_t = y + (25 if titleoffsety is None else titleoffsety)
            self.text_anchor_t = "sw"
            self.angle_t = 0
        elif direction == "w":
            self.x_t = x + (25 if titleoffsetx is None else titleoffsetx)
            self.y_t = y + (25 if titleoffsety is None else titleoffsety)
            self.text_anchor_t = "se"
            self.angle_t = 0
        elif direction == "n":
            self.x_t = x + (-25 if titleoffsetx is None else titleoffsetx)
            self.y_t = y + (-25 if titleoffsety is None else titleoffsety)
            self.text_anchor_t = "sw"
            self.angle_t = 0
        elif direction == "s":
            self.x_t = x + (-25 if titleoffsetx is None else titleoffsetx)
            self.y_t = y + (25 if titleoffsety is None else titleoffsety)
            self.text_anchor_t = "sw"
            self.angle_t = 0
        elif direction == "t":
            self.x_t = xt + (-25 if titleoffsetx is None else titleoffsetx)
            self.y_t = yt + (25 if titleoffsety is None else titleoffsety)
            self.text_anchor_t = "sw"
            self.angle_t = 0
        n = 0
        for c in reversed(self._queue) if reverse else self._queue:
            if ((max_length is not None) and n >= max_length) or not self.visible_t:
                break

            if c not in prev_aos:
                nargs = c.animation_objects.__code__.co_argcount
                if nargs == 1:
                    animation_objects = self.current_aos[c] = c.animation_objects()
                else:
                    animation_objects = self.current_aos[c] = c.animation_objects(self.id(t))
            else:
                animation_objects = self.current_aos[c] = prev_aos[c]
                del prev_aos[c]

            dimx = _call(animation_objects[0], t, c)
            dimy = _call(animation_objects[1], t, c)
            for ao in animation_objects[2:]:
                ao.screen_coordinates = self.screen_coordinates
                if isinstance(ao, AnimateClassic):
                    if direction == "t":
                        ao.x0 = xt + trajectory.x(t=x * 1.00, _t0=0)
                        ao.y0 = yt + trajectory.y(t=x * 1.00, _t0=0)
                    else:
                        ao.x0 = x
                        ao.y0 = y
                else:
                    if direction == "t":
                        ao.x = xt + trajectory.x(t=x * 1.00, _t0=0)
                        ao.y = yt + trajectory.y(t=x * 1.00, _t0=0)
                        ao.angle = trajectory.angle(t=x * 1.00, _t0=0)
                    else:
                        ao.x = x
                        ao.y = y

            if direction == "w":
                x -= dimx
            if direction == "s":
                y -= dimy
            if direction == "e":
                x += dimx
            if direction == "n":
                y += dimy
            if direction == "t":
                x += dimx * 1

            n += 1

        for animation_objects in prev_aos.values():
            for ao in animation_objects[2:]:
                ao.remove()

    def show(self):
        """
        show (unremove)

        It is possible to use this method if already shown
        """
        self.ao_title.show()
        self.env.sys_objects.add(self)
        self.current_aos = {}

    def remove(self):
        self.env.sys_objects.discard(self)
        self.ao_title.remove()
        for animation_objects in self.current_aos.values():
            for ao in animation_objects[2:]:
                ao.remove()

    def is_removed(self):
        return self not in self.env.sys_objects


class Animate3dQueue(DynamicClass):
    """
    Animates the component in a queue.

    Parameters
    ----------
    queue : Queue

    x : float
        x-position of the first component in the queue

        default: 0

    y : float
        y-position of the first component in the queue

        default: 0

    z : float
        z-position of the first component in the queue

        default: 0

    direction : str
        if "x+", waiting line runs in positive x direction (default)

        if "x-", waiting line runs in negative x direction

        if "y+", waiting line runs in positive y direction

        if "y-", waiting line runs in negative y direction

        if "z+", waiting line runs in positive z direction

        if "z-", waiting line runs in negative z direction


    reverse : bool
        if False (default), display in normal order. If True, reversed.

    max_length : int
        maximum number of components to be displayed

    layer : int
        layer (default 0)

    id : any
        the animation works by calling the animation_objects method of each component, optionally
        with id. By default, this is self, but can be overriden, particularly with the queue

    arg : any
        this is used when a parameter is a function with two parameters, as the first argument or
        if a parameter is a method as the instance

        default: self (instance itself)

    visible : bool
        if False, nothing will be shown

        (default True)

    keep : bool
        if False, animation object will be taken from the animation objects. With show(), the animation can be reshown.
        (default True)

    parent : Component
        component where this animation object belongs to (default None)

        if given, the animation object will be removed
        automatically when the parent component is no longer accessible

    Note
    ----
    All parameters, apart from queue, id, arg and parent can be specified as:

    - a scalar, like 10

    - a function with zero arguments, like lambda: title

    - a function with one argument, being the time t, like lambda t: t + 10

    - a function with two parameters, being arg (as given) and the time, like lambda comp, t: comp.state

    - a method instance arg for time t, like self.state, actually leading to arg.state(t) to be called
    """

    def __init__(
        self,
        queue: "Queue",
        x: Union[float, Callable] = 0,
        y: Union[float, Callable] = 0,
        z: Union[float, Callable] = 0,
        direction: Union[str, Callable] = "x+",
        max_length: Union[int, Callable] = None,
        reverse: Union[bool, Callable] = False,
        layer: Union[int, Callable] = 0,
        id: Union[Any, Callable] = None,
        arg: Any = None,
        parent: "Component" = None,
        visible: Union[bool, Callable] = True,
        keep: Union[bool, Callable] = True,
    ):
        super().__init__()
        _checkisqueue(queue)
        self._queue = queue
        self.x = x
        self.y = y
        self.z = z
        self.id = self if id is None else id
        self.arg = self if arg is None else arg
        self.max_length = max_length
        self.direction = direction
        self.visible = visible
        self.keep = keep
        self.reverse = reverse
        self.current_aos = {}
        if parent is not None:
            if not isinstance(parent, Component):
                raise ValueError(repr(parent) + " is not a component")
            parent._animation_children.add(self)
        self.env = queue.env
        self.layer = layer
        self.register_dynamic_attributes("x y z id max_length direction reverse layer visible keep")
        self.show()

    def update(self, t):
        if not self.keep(t):
            self.remove()
            return

        prev_aos = self.current_aos
        self.current_aos = {}
        max_length = self.max_length(t)
        x = self.x(t)
        y = self.y(t)
        z = self.z(t)

        direction = self.direction(t).lower()
        if direction not in ("x+ x- y+ y- z+ z-").split():
            raise ValueError(f"direction {direction} not recognized")

        reverse = self.reverse(t)

        n = 0
        for c in reversed(self._queue) if reverse else self._queue:
            if (max_length is not None) and n >= max_length:
                break
            if c not in prev_aos:
                nargs = c.animation3d_objects.__code__.co_argcount
                if nargs == 1:
                    animation_objects = self.current_aos[c] = c.animation3d_objects()
                else:
                    animation_objects = self.current_aos[c] = c.animation3d_objects(self.id(t))
            else:
                animation_objects = self.current_aos[c] = prev_aos[c]
                del prev_aos[c]
            dimx = _call(animation_objects[0], t, c)
            dimy = _call(animation_objects[1], t, c)
            dimz = _call(animation_objects[2], t, c)

            for ao in animation_objects[3:]:
                ao.x_offset = x
                ao.y_offset = y
                ao.z_offset = z

            if direction == "x+":
                x += dimx
            if direction == "x-":
                x -= dimx

            if direction == "y+":
                y += dimy
            if direction == "y-":
                y -= dimy

            if direction == "z+":
                z += dimz
            if direction == "z-":
                z -= dimz
            n += 1

        for animation_objects in prev_aos.values():
            for ao in animation_objects[3:]:
                ao.remove()

    def queue(self):
        """
        Returns
        -------
        the queue this object refers to. Can be useful in Component.animation3d_objects: queue
        """
        return self._queue

    def show(self):
        """
        show (unremove)

        It is possible to use this method if already shown
        """
        self.env.sys_objects.add(self)

    def remove(self):
        for animation_objects in self.current_aos.values():
            for ao in animation_objects[3:]:
                ao.remove()
        self.env.sys_objects.discard(self)

    def is_removed(self):
        return self not in self.env.sys_objects


class AnimateCombined:
    """
    Combines several Animate? objects

    Parameters
    ----------
    animation_objects : iterable
        iterable of Animate2dBase, Animate3dBase or AnimateCombined objects

    **kwargs : dict
        attributes to be set for objects in animation_objects

    Notes
    -----
    When an attribute of an AnimateCombined is assigned, it will propagate to all members,
    provided it has already that attribute.

    When an attribute of an AnimateCombined is queried, the value of the attribute
    of the first animation_object of the list that has such an attribute will be returned.

    If the attribute does not exist in any animation_object of the list, an AttributeError will be raised.

    It is possible to use animation_objects with ::

        an = sim.AnimationCombined(car.animation_objects[2:])
        an = sim.AnimationCombined(car.animation3d_objects[3:])
    """

    def __init__(self, animation_objects: Iterable, **kwargs):
        self.animation_objects = list(animation_objects)

        self.update(**kwargs)

    def update(self, **kwargs):
        """
        Updated one or more attributes

        Parameters
        ----------
        **kwargs : dict
            attributes to be set
        """

        for k, v in kwargs.items():
            for item in self.animation_objects:
                setattr(item, k, v)

    def __setattr__(self, key, value):
        if key == "animation_objects":
            super().__setattr__(key, value)
        else:
            for item in self.animation_objects:
                if hasattr(item, key):
                    setattr(item, key, value)

    def __getattr__(self, key):
        for item in self.animation_objects:
            if hasattr(item, key):
                return getattr(item, key)

        raise AttributeError(f"None of the AnimateCombined animation objects has an attribute {key!r}")

    def append(self, item):
        """
        Add Animate2dBase, Animate3dBase or AnimateCombined object

        Parameters
        ----------
        item : Animate2dBase, Animate3dBase or AnimateCombined
            to be added
        """
        if not isinstance(item, (AnimateCombined, Animate2dBase, Animate3dBase)):
            return NotImplemented
        self.animation_objects.append(item)

    def remove(self):
        """
        remove all members from the animation
        """
        for item in self.animation_objects:
            item.remove()

    def show(self):
        """
        show all members in the animation
        """
        for item in self.animation_objects:
            item.show()

    def is_removed(self):
        return all(item.is_removed() for item in self.animation_objects)

    def __repr__(self):
        return f"{self.__class__.__name__} ({','.join(repr(item) for item in self.animation_objects)})"


class AnimateText(Animate2dBase):
    """
    Displays a text

    Parameters
    ----------
    text : str, tuple or list
        the text to be displayed

        if text is str, the text may contain linefeeds, which are shown as individual lines
        if text is tuple or list, each item is displayed on a separate line

    x : float
        position of anchor point (default 0)

    y : float
        position of anchor point (default 0)

    xy_anchor : str
        specifies where x and y are relative to

        possible values are (default: sw) :

        ``nw    n    ne``

        ``w     c     e``

        ``sw    s    se``

        If null string, the given coordimates are used untranslated

    offsetx : float
        offsets the x-coordinate of the rectangle (default 0)

    offsety : float
        offsets the y-coordinate of the rectangle (default 0)

    angle : float
        angle of the text (in degrees)

        default: 0

    max_lines : int
        the maximum of lines of text to be displayed

        if positive, it refers to the first max_lines lines

        if negative, it refers to the last -max_lines lines

        if zero (default), all lines will be displayed

    font : str or list/tuple
        font to be used for texts

        Either a string or a list/tuple of fontnames.
        If not found, uses calibri or arial

    text_anchor : str
        anchor position of text

        specifies where to texts relative to the rectangle
        point

        possible values are (default: c):

        ``nw    n    ne``

        ``w     c     e``

        ``sw    s    se``

    textcolor : colorspec
        color of the text (default foreground_color)

    fontsize : float
        fontsize of text (default 15)

    arg : any
        this is used when a parameter is a function with two parameters, as the first argument or
        if a parameter is a method as the instance

        default: self (instance itself)

    parent : Component
        component where this animation object belongs to (default None)

        if given, the animation object will be removed
        automatically when the parent component is no longer accessible

    screen_coordinates : bool
        use screen_coordinates

        normally, the scale parameters are use for positioning and scaling
        objects.

        if True, screen_coordinates will be used instead.

    layer : float
        default: 0

        lower layer numbers are placed on top of higher layer numbers

    over3d : bool
        if True, this object will be rendered to the OpenGL window

        if False (default), the normal 2D plane will be used.

    Note
    ----
    All measures are in screen coordinates


    All parameters, apart from parent, arg and env can be specified as:

    - a scalar, like 10

    - a function with zero arguments, like lambda: title

    - a function with one argument, being the time t, like lambda t: t + 10

    - a function with two parameters, being arg (as given) and the time, like lambda comp, t: comp.state

    - a method instance arg for time t, like self.state, actually leading to arg.state(t) to be called

    """

    def __init__(
        self,
        text: Union[str, Iterable[str], Callable] = None,
        x: Union[float, Callable] = None,
        y: Union[float, Callable] = None,
        font: Union[str, Callable] = None,
        fontsize: Union[float, Callable] = None,
        textcolor: Union[ColorType, Callable] = None,
        text_anchor: Union[str, Callable] = None,
        angle: Union[float, Callable] = None,
        xy_anchor: Union[str, Callable] = None,
        layer: Union[float, Callable] = None,
        max_lines: Union[int, Callable] = None,
        offsetx: Union[float, Callable] = None,
        offsety: Union[float, Callable] = None,
        arg: Any = None,
        visible: Union[bool, Callable] = None,
        keep: Union[bool, Callable] = None,
        parent: "Component" = None,
        env: "Environment" = None,
        screen_coordinates: bool = False,
        over3d: bool = None,
    ):
        super().__init__(
            locals_=locals(),
            type="text",
            argument_default=dict(
                text="",
                x=0,
                y=0,
                fontsize=15,
                textcolor="fg",
                font="mono",
                text_anchor="sw",
                angle=0,
                visible=True,
                keep=True,
                xy_anchor="",
                layer=0,
                offsetx=0,
                offsety=0,
                max_lines=0,
            ),
            attach_text=False,
        )


class AnimateRectangle(Animate2dBase):
    """
    Displays a rectangle, optionally with a text

    Parameters
    ----------
    spec : four item tuple or list
        should specify xlowerleft, ylowerleft, xupperright, yupperright

        optionally a fifth element can be used to specify the radius of a rounded rectangle

    x : float
        position of anchor point (default 0)

    y : float
        position of anchor point (default 0)

    xy_anchor : str
        specifies where x and y are relative to

        possible values are (default: sw) :

        ``nw    n    ne``

        ``w     c     e``

        ``sw    s    se``

        If null string, the given coordimates are used untranslated

    offsetx : float
        offsets the x-coordinate of the rectangle (default 0)

    offsety : float
        offsets the y-coordinate of the rectangle (default 0)

    linewidth : float
        linewidth of the contour

        default 1

    fillcolor : colorspec
        color of interior (default foreground_color)

        default transparent

    linecolor : colorspec
        color of the contour (default transparent)

    angle : float
        angle of the rectangle (in degrees)

        default: 0

    as_points : bool
         if False (default), the contour lines are drawn

         if True, only the corner points are shown

    text : str, tuple or list
        the text to be displayed

        if text is str, the text may contain linefeeds, which are shown as individual lines

    max_lines : int
        the maximum of lines of text to be displayed

        if positive, it refers to the first max_lines lines

        if negative, it refers to the last -max_lines lines

        if zero (default), all lines will be displayed

    font : str or list/tuple
        font to be used for texts

        Either a string or a list/tuple of fontnames.
        If not found, uses calibri or arial

    text_anchor : str
        anchor position of text

        specifies where to texts relative to the rectangle
        point

        possible values are (default: c):

        ``nw    n    ne``

        ``w     c     e``

        ``sw    s    se``

    textcolor : colorspec
        color of the text (default foreground_color)

    text_offsetx : float
        extra x offset to the text_anchor point

    text_offsety : float
        extra y offset to the text_anchor point

    fontsize : float
        fontsize of text (default 15)

    arg : any
        this is used when a parameter is a function with two parameters, as the first argument or
        if a parameter is a method as the instance

        default: self (instance itself)

    layer : float
        default: 0

        lower layer numbers are placed on top of higher layer numbers

    parent : Component
        component where this animation object belongs to (default None)

        if given, the animation object will be removed
        automatically when the parent component is no longer accessible

    Note
    ----
    All measures are in screen coordinates


    All parameters, apart from parent, arg and env can be specified as:

    - a scalar, like 10

    - a function with zero arguments, like lambda: title

    - a function with one argument, being the time t, like lambda t: t + 10

    - a function with two parameters, being arg (as given) and the time, like lambda comp, t: comp.state

    - a method instance arg for time t, like self.state, actually leading to arg.state(t) to be called
    """

    def __init__(
        self,
        spec: Union[Iterable, Callable] = None,
        x: Union[float, Callable] = None,
        y: Union[float, Callable] = None,
        fillcolor: Union[ColorType, Callable] = None,
        linecolor: Union[ColorType, Callable] = None,
        linewidth: Union[float, Callable] = None,
        text: Union[str, Callable] = None,
        fontsize: Union[float, Callable] = None,
        textcolor: Union[ColorType, Callable] = None,
        font: Union[str, Callable] = None,
        angle: Union[float, Callable] = None,
        xy_anchor: Union[str, Callable] = None,
        layer: Union[float, Callable] = None,
        max_lines: Union[int, Callable] = None,
        offsetx: Union[float, Callable] = None,
        offsety: Union[float, Callable] = None,
        as_points: Union[bool, Callable] = None,
        text_anchor: Union[str, Callable] = None,
        text_offsetx: Union[float, Callable] = None,
        text_offsety: Union[float, Callable] = None,
        arg: Any = None,
        parent: "Component" = None,
        visible: Union[bool, Callable] = None,
        keep: Union[bool, Callable] = None,
        env: "Environment" = None,
        screen_coordinates: bool = False,
        over3d: bool = None,
    ):
        super().__init__(
            locals_=locals(),
            type="rectangle",
            argument_default=dict(
                spec=(0, 0, 0, 0),
                x=0,
                y=0,
                fillcolor="fg",
                linecolor="",
                linewidth=1,
                text="",
                fontsize=15,
                textcolor="bg",
                font="",
                angle=0,
                xy_anchor="",
                layer=0,
                max_lines=0,
                offsetx=0,
                offsety=0,
                as_points=False,
                text_anchor="c",
                text_offsetx=0,
                text_offsety=0,
                visible=True,
                keep=True,
                parent=None,
            ),
            attach_text=True,
        )


class AnimatePolygon(Animate2dBase):
    """
    Displays a polygon, optionally with a text

    Parameters
    ----------
    spec : tuple or list
        should specify x0, y0, x1, y1, ...

    x : float
        position of anchor point (default 0)

    y : float
        position of anchor point (default 0)

    xy_anchor : str
        specifies where x and y are relative to

        possible values are (default: sw) :

        ``nw    n    ne``

        ``w     c     e``

        ``sw    s    se``

        If null string, the given coordimates are used untranslated

    offsetx : float
        offsets the x-coordinate of the polygon (default 0)

    offsety : float
        offsets the y-coordinate of the polygon (default 0)

    linewidth : float
        linewidth of the contour

        default 1

    fillcolor : colorspec
        color of interior (default foreground_color)

        default transparent

    linecolor : colorspec
        color of the contour (default transparent)

    angle : float
        angle of the polygon (in degrees)

        default: 0

    as_points : bool
         if False (default), the contour lines are drawn

         if True, only the corner points are shown

    text : str, tuple or list
        the text to be displayed

        if text is str, the text may contain linefeeds, which are shown as individual lines

    max_lines : int
        the maximum of lines of text to be displayed

        if positive, it refers to the first max_lines lines

        if negative, it refers to the last -max_lines lines

        if zero (default), all lines will be displayed

    font : str or list/tuple
        font to be used for texts

        Either a string or a list/tuple of fontnames.
        If not found, uses calibri or arial

    text_anchor : str
        anchor position of text

        specifies where to texts relative to the polygon
        point

        possible values are (default: c):

        ``nw    n    ne``

        ``w     c     e``

        ``sw    s    se``

    textcolor : colorspec
        color of the text (default foreground_color)

    text_offsetx : float
        extra x offset to the text_anchor point

    text_offsety : float
        extra y offset to the text_anchor point

    fontsize : float
        fontsize of text (default 15)

    arg : any
        this is used when a parameter is a function with two parameters, as the first argument or
        if a parameter is a method as the instance

        default: self (instance itself)

    parent : Component
        component where this animation object belongs to (default None)

        if given, the animation object will be removed
        automatically when the parent component is no longer accessible

    screen_coordinates : bool
        use screen_coordinates

        normally, the scale parameters are use for positioning and scaling
        objects.

        if True, screen_coordinates will be used instead.

    layer : float
        default: 0

        lower layer numbers are placed on top of higher layer numbers

    over3d : bool
        if True, this object will be rendered to the OpenGL window

        if False (default), the normal 2D plane will be used.

    Note
    ----
    All measures are in screen coordinates


    All parameters, apart from parent, arg and env can be specified as:

    - a scalar, like 10

    - a function with zero arguments, like lambda: title

    - a function with one argument, being the time t, like lambda t: t + 10

    - a function with two parameters, being arg (as given) and the time, like lambda comp, t: comp.state

    - a method instance arg for time t, like self.state, actually leading to arg.state(t) to be called
    """

    def __init__(
        self,
        spec: Union[Iterable, Callable] = None,
        x: Union[float, Callable] = None,
        y: Union[float, Callable] = None,
        fillcolor: Union[ColorType, Callable] = None,
        linecolor: Union[ColorType, Callable] = None,
        linewidth: Union[float, Callable] = None,
        text: Union[str, Callable] = None,
        fontsize: Union[float, Callable] = None,
        textcolor: Union[ColorType, Callable] = None,
        font: Union[str, Callable] = None,
        angle: Union[float, Callable] = None,
        xy_anchor: Union[str, Callable] = None,
        layer: Union[float, Callable] = None,
        max_lines: Union[int, Callable] = None,
        offsetx: Union[float, Callable] = None,
        offsety: Union[float, Callable] = None,
        as_points: Union[bool, Callable] = None,
        text_anchor: Union[str, Callable] = None,
        text_offsetx: Union[float, Callable] = None,
        text_offsety: Union[float, Callable] = None,
        arg: Any = None,
        parent: "Component" = None,
        visible: Union[bool, Callable] = None,
        keep: Union[bool, Callable] = None,
        env: "Environment" = None,
        screen_coordinates: bool = False,
        over3d: bool = None,
    ):
        super().__init__(
            locals_=locals(),
            type="polygon",
            argument_default=dict(
                spec=(),
                x=0,
                y=0,
                linecolor="",
                linewidth=1,
                fillcolor="fg",
                text="",
                fontsize=15,
                textcolor="fg",
                font="",
                angle=0,
                xy_anchor="",
                layer=0,
                max_lines=0,
                offsetx=0,
                offsety=0,
                as_points=False,
                text_anchor="c",
                text_offsetx=0,
                text_offsety=0,
                visible=True,
                keep=True,
            ),
            attach_text=True,
        )


class AnimateLine(Animate2dBase):
    """
    Displays a line, optionally with a text

    Parameters
    ----------
    spec : tuple or list
        should specify x0, y0, x1, y1, ...

    x : float
        position of anchor point (default 0)

    y : float
        position of anchor point (default 0)

    xy_anchor : str
        specifies where x and y are relative to

        possible values are (default: sw) :

        ``nw    n    ne``

        ``w     c     e``

        ``sw    s    se``

        If null string, the given coordimates are used untranslated

    offsetx : float
        offsets the x-coordinate of the line (default 0)

    offsety : float
        offsets the y-coordinate of the line (default 0)

    linewidth : float
        linewidth of the contour

        default 1

    linecolor : colorspec
        color of the contour (default foreground_color)

    angle : float
        angle of the line (in degrees)

        default: 0

    as_points : bool
         if False (default), the contour lines are drawn

         if True, only the corner points are shown

    text : str, tuple or list
        the text to be displayed

        if text is str, the text may contain linefeeds, which are shown as individual lines

    max_lines : int
        the maximum of lines of text to be displayed

        if positive, it refers to the first max_lines lines

        if negative, it refers to the last -max_lines lines

        if zero (default), all lines will be displayed

    font : str or list/tuple
        font to be used for texts

        Either a string or a list/tuple of fontnames.
        If not found, uses calibri or arial

    text_anchor : str
        anchor position of text

        specifies where to texts relative to the polygon
        point

        possible values are (default: c):

        ``nw    n    ne``

        ``w     c     e``

        ``sw    s    se``

    textcolor : colorspec
        color of the text (default foreground_color)

    text_offsetx : float
        extra x offset to the text_anchor point

    text_offsety : float
        extra y offset to the text_anchor point

    fontsize : float
        fontsize of text (default 15)

    arg : any
        this is used when a parameter is a function with two parameters, as the first argument or
        if a parameter is a method as the instance

        default: self (instance itself)

    parent : Component
        component where this animation object belongs to (default None)

        if given, the animation object will be removed
        automatically when the parent component is no longer accessible

    layer : float
        default: 0

        lower layer numbers are placed on top of higher layer numbers

    screen_coordinates : bool
        use screen_coordinates

        normally, the scale parameters are use for positioning and scaling
        objects.

        if True, screen_coordinates will be used instead.

    over3d : bool
        if True, this object will be rendered to the OpenGL window

        if False (default), the normal 2D plane will be used.

    Note
    ----
    All measures are in screen coordinates


    All parameters, apart from parent, arg and env can be specified as:

    - a scalar, like 10

    - a function with zero arguments, like lambda: title

    - a function with one argument, being the time t, like lambda t: t + 10

    - a function with two parameters, being arg (as given) and the time, like lambda comp, t: comp.state

    - a method instance arg for time t, like self.state, actually leading to arg.state(t) to be called
    """

    def __init__(
        self,
        spec: Union[Iterable, Callable] = None,
        x: Union[float, Callable] = None,
        y: Union[float, Callable] = None,
        fillcolor: Union[ColorType, Callable] = None,
        linecolor: Union[ColorType, Callable] = None,
        linewidth: Union[float, Callable] = None,
        text: Union[str, Callable] = None,
        fontsize: Union[float, Callable] = None,
        textcolor: Union[ColorType, Callable] = None,
        font: Union[str, Callable] = None,
        angle: Union[float, Callable] = None,
        xy_anchor: Union[str, Callable] = None,
        layer: Union[float, Callable] = None,
        max_lines: Union[int, Callable] = None,
        offsetx: Union[float, Callable] = None,
        offsety: Union[float, Callable] = None,
        as_points: Union[bool, Callable] = None,
        text_anchor: Union[str, Callable] = None,
        text_offsetx: Union[float, Callable] = None,
        text_offsety: Union[float, Callable] = None,
        arg: Any = None,
        parent: "Component" = None,
        visible: Union[bool, Callable] = None,
        keep: Union[bool, Callable] = None,
        env: "Environment" = None,
        screen_coordinates: bool = False,
        over3d: bool = None,
    ):
        fillcolor = None  # required for make_pil_image

        super().__init__(
            locals_=locals(),
            type="line",
            argument_default=dict(
                spec=(),
                x=0,
                y=0,
                linecolor="fg",
                linewidth=1,
                fillcolor="",
                text="",
                fontsize=15,
                textcolor="fg",
                font="",
                angle=0,
                xy_anchor="",
                layer=0,
                max_lines=0,
                offsetx=0,
                offsety=0,
                as_points=False,
                text_anchor="c",
                text_offsetx=0,
                text_offsety=0,
                visible=True,
                keep=True,
            ),
            attach_text=True,
        )


class AnimatePoints(Animate2dBase):
    """
    Displays a series of points, optionally with a text

    Parameters
    ----------
    spec : tuple or list
        should specify x0, y0, x1, y1, ...

    x : float
        position of anchor point (default 0)

    y : float
        position of anchor point (default 0)

    xy_anchor : str
        specifies where x and y are relative to

        possible values are (default: sw) :

        ``nw    n    ne``

        ``w     c     e``

        ``sw    s    se``

        If null string, the given coordimates are used untranslated

    offsetx : float
        offsets the x-coordinate of the points (default 0)

    offsety : float
        offsets the y-coordinate of the points (default 0)

    linewidth : float
        width of the points

        default 1

    linecolor : colorspec
        color of the points (default foreground_color)

    angle : float
        angle of the points (in degrees)

        default: 0

    as_points : bool
         if False, the contour lines are drawn

         if True (default), only the corner points are shown

    text : str, tuple or list
        the text to be displayed

        if text is str, the text may contain linefeeds, which are shown as individual lines

    max_lines : int
        the maximum of lines of text to be displayed

        if positive, it refers to the first max_lines lines

        if negative, it refers to the last -max_lines lines

        if zero (default), all lines will be displayed

    font : str or list/tuple
        font to be used for texts

        Either a string or a list/tuple of fontnames.
        If not found, uses calibri or arial

    text_anchor : str
        anchor position of text

        specifies where to texts relative to the polygon
        point

        possible values are (default: c):

        ``nw    n    ne``

        ``w     c     e``

        ``sw    s    se``

    textcolor : colorspec
        color of the text (default foreground_color)

    text_offsetx : float
        extra x offset to the text_anchor point

    text_offsety : float
        extra y offset to the text_anchor point

    fontsize : float
        fontsize of text (default 15)

    arg : any
        this is used when a parameter is a function with two parameters, as the first argument or
        if a parameter is a method as the instance

        default: self (instance itself)

    parent : Component
        component where this animation object belongs to (default None)

        if given, the animation object will be removed
        automatically when the parent component is no longer accessible

    screen_coordinates : bool
        use screen_coordinates

        normally, the scale parameters are use for positioning and scaling
        objects.

        if True, screen_coordinates will be used instead.

    layer : float
        default: 0

        lower layer numbers are placed on top of higher layer numbers

    over3d : bool
        if True, this object will be rendered to the OpenGL window

        if False (default), the normal 2D plane will be used.

    Note
    ----
    All measures are in screen coordinates


    All parameters, apart from parent, arg and env can be specified as:

    - a scalar, like 10

    - a function with zero arguments, like lambda: title

    - a function with one argument, being the time t, like lambda t: t + 10

    - a function with two parameters, being arg (as given) and the time, like lambda comp, t: comp.state

    - a method instance arg for time t, like self.state, actually leading to arg.state(t) to be called
    """

    def __init__(
        self,
        spec: Union[Iterable, Callable] = None,
        x: Union[float, Callable] = None,
        y: Union[float, Callable] = None,
        fillcolor: Union[ColorType, Callable] = None,
        linecolor: Union[ColorType, Callable] = None,
        linewidth: Union[float, Callable] = None,
        text: Union[str, Callable] = None,
        fontsize: Union[float, Callable] = None,
        textcolor: Union[ColorType, Callable] = None,
        font: Union[str, Callable] = None,
        angle: Union[float, Callable] = None,
        xy_anchor: Union[str, Callable] = None,
        layer: Union[float, Callable] = None,
        max_lines: Union[int, Callable] = None,
        offsetx: Union[float, Callable] = None,
        offsety: Union[float, Callable] = None,
        as_points: Union[bool, Callable] = None,
        text_anchor: Union[str, Callable] = None,
        text_offsetx: Union[float, Callable] = None,
        text_offsety: Union[float, Callable] = None,
        arg: Any = None,
        parent: "Component" = None,
        visible: Union[bool, Callable] = None,
        keep: Union[bool, Callable] = None,
        env: "Environment" = None,
        screen_coordinates: bool = False,
        over3d: bool = None,
    ):
        fillcolor = None  # required for make_pil_image

        super().__init__(
            locals_=locals(),
            type="line",
            argument_default=dict(
                spec=(),
                x=0,
                y=0,
                linecolor="fg",
                linewidth=1,
                fillcolor="",
                text="",
                fontsize=15,
                textcolor="fg",
                font="",
                angle=0,
                xy_anchor="",
                layer=0,
                max_lines=0,
                offsetx=0,
                offsety=0,
                as_points=True,
                text_anchor="c",
                text_offsetx=0,
                text_offsety=0,
                visible=True,
                keep=True,
            ),
            attach_text=True,
        )


class AnimateCircle(Animate2dBase):
    """
    Displays a (partial) circle or (partial) ellipse , optionally with a text

    Parameters
    ----------
    radius : float
        radius of the circle

    radius1 : float
        the 'height' of the ellipse. If None (default), a circle will be drawn

    arc_angle0 : float
        start angle of the circle (default 0)

    arc_angle1 : float
        end angle of the circle (default 360)

        when arc_angle1 > arc_angle0 + 360, only 360 degrees will be shown

    draw_arc : bool
        if False (default), no arcs will be drawn
        if True, the arcs from and to the center will be drawn

    x : float
        position of anchor point (default 0)

    y : float
        position of anchor point (default 0)

    xy_anchor : str
        specifies where x and y are relative to

        possible values are (default: sw) :

        ``nw    n    ne``

        ``w     c     e``

        ``sw    s    se``

        If null string, the given coordimates are used untranslated

        The positions corresponds to a full circle even if arc_angle0 and/or arc_angle1 are specified.

    offsetx : float
        offsets the x-coordinate of the circle (default 0)

    offsety : float
        offsets the y-coordinate of the circle (default 0)

    linewidth : float
        linewidth of the contour

        default 1

    fillcolor : colorspec
        color of interior (default foreground_color)

    linecolor : colorspec
        color of the contour (default transparent)

    angle : float
        angle of the circle/ellipse and/or text (in degrees)

        default: 0

    text : str, tuple or list
        the text to be displayed

        if text is str, the text may contain linefeeds, which are shown as individual lines

    max_lines : int
        the maximum of lines of text to be displayed

        if positive, it refers to the first max_lines lines

        if negative, it refers to the last -max_lines lines

        if zero (default), all lines will be displayed

    font : str or list/tuple
        font to be used for texts

        Either a string or a list/tuple of fontnames.
        If not found, uses calibri or arial

    text_anchor : str
        anchor position of text

        specifies where to texts relative to the polygon
        point

        possible values are (default: c):

        ``nw    n    ne``

        ``w     c     e``

        ``sw    s    se``

    textcolor : colorspec
        color of the text (default foreground_color)

    text_offsetx : float
        extra x offset to the text_anchor point

    text_offsety : float
        extra y offset to the text_anchor point

    fontsize : float
        fontsize of text (default 15)

    arg : any
        this is used when a parameter is a function with two parameters, as the first argument or
        if a parameter is a method as the instance

        default: self (instance itself)

    parent : Component
        component where this animation object belongs to (default None)

        if given, the animation object will be removed
        automatically when the parent component is no longer accessible

    layer : float
        default: 0

        lower layer numbers are placed on top of higher layer numbers

    screen_coordinates : bool
        use screen_coordinates

        normally, the scale parameters are use for positioning and scaling
        objects.

        if True, screen_coordinates will be used instead.

    over3d : bool
        if True, this object will be rendered to the OpenGL window

        if False (default), the normal 2D plane will be used.

    Note
    ----
    All measures are in screen coordinates


    All parameters, apart from parent, arg and env can be specified as:

    - a scalar, like 10

    - a function with zero arguments, like lambda: title

    - a function with one argument, being the time t, like lambda t: t + 10

    - a function with two parameters, being arg (as given) and the time, like lambda comp, t: comp.state

    - a method instance arg for time t, like self.state, actually leading to arg.state(t) to be called
    """

    def __init__(
        self,
        radius: Union[float, Callable] = None,
        radius1: Union[float, Callable] = None,
        arc_angle0: Union[float, Callable] = None,
        arc_angle1: Union[float, Callable] = None,
        draw_arc: Union[float, Callable] = None,
        x: Union[float, Callable] = None,
        y: Union[float, Callable] = None,
        fillcolor: Union[ColorType, Callable] = None,
        linecolor: Union[ColorType, Callable] = None,
        linewidth: Union[float, Callable] = None,
        text: Union[str, Callable] = None,
        fontsize: Union[float, Callable] = None,
        textcolor: Union[ColorType, Callable] = None,
        font: Union[str, Callable] = None,
        angle: Union[float, Callable] = None,
        xy_anchor: Union[str, Callable] = None,
        layer: Union[float, Callable] = None,
        max_lines: Union[int, Callable] = None,
        offsetx: Union[float, Callable] = None,
        offsety: Union[float, Callable] = None,
        as_points: Union[bool, Callable] = None,
        text_anchor: Union[str, Callable] = None,
        text_offsetx: Union[float, Callable] = None,
        text_offsety: Union[float, Callable] = None,
        arg: Any = None,
        parent: "Component" = None,
        visible: Union[bool, Callable] = None,
        keep: Union[bool, Callable] = None,
        env: "Environment" = None,
        screen_coordinates: bool = False,
        over3d: bool = None,
    ):
        super().__init__(
            locals_=locals(),
            type="circle",
            argument_default=dict(
                radius=100,
                radius1=None,
                arc_angle0=0,
                arc_angle1=360,
                draw_arc=False,
                x=0,
                y=0,
                fillcolor="fg",
                linecolor="",
                linewidth=1,
                text="",
                fontsize=15,
                textcolor="bg",
                font="",
                angle=0,
                xy_anchor="",
                layer=0,
                max_lines=0,
                offsetx=0,
                offsety=0,
                text_anchor="c",
                text_offsetx=0,
                text_offsety=0,
                visible=True,
                keep=True,
            ),
            attach_text=True,
        )


class AnimateImage(Animate2dBase):
    """
    Displays an image, optionally with a text

    Parameters
    ----------
    image : str, pathlib.Path or PIL Image
        image to be displayed

        if used as function or method or in direct assigmnent,
        the image should be a file containing an image or a PIL image

        if image is a string consisting of a zipfile-name, a bar (|) and a filename,
        the given filename will be read from the specified zip archive, e.g

        sim.AnimateImage(image="cars.zip|bmw.png")

    x : float
        position of anchor point (default 0)

    y : float
        position of anchor point (default 0)

    xy_anchor : str
        specifies where x and y are relative to

        possible values are (default: sw) :

        ``nw    n    ne``

        ``w     c     e``

        ``sw    s    se``

        If null string, the given coordimates are used untranslated

    anchor : str
        specifies where the x and refer to

        possible values are (default: sw) :

        ``nw    n    ne``

        ``w     c     e``

        ``sw    s    se``


    offsetx : float
        offsets the x-coordinate of the circle (default 0)

    offsety : float
        offsets the y-coordinate of the circle (default 0)

    angle : float
        angle of the image (in degrees) (default 0)

    alpha : float
        alpha of the image (0-255) (default 255)

    width : float
       width of the image (default: None = no scaling)

    heighth : float
       height of the image (default: None = no scaling)

    text : str, tuple or list
        the text to be displayed

        if text is str, the text may contain linefeeds, which are shown as individual lines

    max_lines : int
        the maximum of lines of text to be displayed

        if positive, it refers to the first max_lines lines

        if negative, it refers to the last -max_lines lines

        if zero (default), all lines will be displayed

    font : str or list/tuple
        font to be used for texts

        Either a string or a list/tuple of fontnames.
        If not found, uses calibri or arial

    text_anchor : str
        anchor position of text

        specifies where to texts relative to the polygon
        point

        possible values are (default: c):

        ``nw    n    ne``

        ``w     c     e``

        ``sw    s    se``

    textcolor : colorspec
        color of the text (default foreground_color)

    text_offsetx : float
        extra x offset to the text_anchor point

    text_offsety : float
        extra y offset to the text_anchor point

    fontsize : float
        fontsize of text (default 15)

    animation_start : float
        (simulation)time to start the animation

        default: env.t()

        When the image is not an animated GIF, no effect

    animation_repeat : float
        if False (default), the animation will be shown only once

        if True, the animation will be repeated

        When the image is not an animated GIF, no effect

    animation_speed : float
        time scale (relative to current speed) (default: 1)

        When the image is not an animated GIF, no effect

    animation_pingpong : bool
        if False (default), the animation will play forward only

        if True, the animation will first play forward, then backward.
        Note that the backward loop might run slowly.

    animation_from : float
        animate from this time (measured in seconds in the actual gif/webp video)

        default: 0

    animation_to : float
        animate to this time (measured in seconds in the actual gif/webp video)

        default: inf (=end of video)

    arg : any
        this is used when a parameter is a function with two parameters, as the first argument or
        if a parameter is a method as the instance

        default: self (instance itself)

    parent : Component
        component where this animation object belongs to (default None)

        if given, the animation object will be removed
        automatically when the parent component is no longer accessible

    layer : float
        default: 0

        lower layer numbers are placed on top of higher layer numbers

    screen_coordinates : bool
        use screen_coordinates

        normally, the scale parameters are used for positioning and scaling
        objects.

        if True, screen_coordinates will be used instead.

    over3d : bool
        if True, this object will be rendered to the OpenGL window

        if False (default), the normal 2D plane will be used.

    Note
    ----
    All measures are in screen coordinates

    All parameters, apart from parent, arg and env can be specified as:

    - a scalar, like 10

    - a function with zero arguments, like lambda: title

    - a function with one argument, being the time t, like lambda t: t + 10

    - a function with two parameters, being arg (as given) and the time, like lambda comp, t: comp.state

    - a method instance arg for time t, like self.state, actually leading to arg.state(t) to be called
    """

    def __init__(
        self,
        image: Any = None,
        x: Union[float, Callable] = None,
        y: Union[float, Callable] = None,
        width: Union[float, Callable] = None,
        height: Union[float, Callable] = None,
        text: Union[str, Callable] = None,
        fontsize: Union[float, Callable] = None,
        textcolor: Union[ColorType, Callable] = None,
        font: Union[str, Callable] = None,
        angle: Union[float, Callable] = None,
        alpha: Union[float, Callable] = None,
        xy_anchor: Union[str, Callable] = None,
        layer: Union[float, Callable] = None,
        max_lines: Union[int, Callable] = None,
        offsetx: Union[float, Callable] = None,
        offsety: Union[float, Callable] = None,
        text_anchor: Union[str, Callable] = None,
        text_offsetx: Union[float, Callable] = None,
        text_offsety: Union[float, Callable] = None,
        anchor: Union[str, Callable] = None,
        animation_start: Union[float, Callable] = None,
        animation_repeat: Union[bool, Callable] = None,
        animation_pingpong: Union[bool, Callable] = None,
        animation_speed: Union[float, Callable] = None,
        animation_from: Union[float, Callable] = None,
        animation_to: Union[float, Callable] = None,
        flip_horizontal: Union[bool, Callable] = None,
        flip_vertical: Union[bool, Callable] = None,
        arg: Any = None,
        parent: "Component" = None,
        visible: Union[bool, Callable] = None,
        keep: Union[bool, Callable] = None,
        env: "Environment" = None,
        screen_coordinates: bool = False,
        over3d: bool = None,
    ):
        if env is None:  # this required here to get access to env.now()
            env = g.default_env

        super().__init__(
            locals_=locals(),
            type="image",
            argument_default=dict(
                image="",
                x=0,
                y=0,
                width=None,
                height=None,
                text="",
                fontsize=15,
                textcolor="bg",
                font="",
                angle=0,
                alpha=255,
                xy_anchor="",
                layer=0,
                max_lines=0,
                offsetx=0,
                offsety=0,
                text_anchor="c",
                text_offsetx=0,
                text_offsety=0,
                anchor="sw",
                animation_start=env._now,
                animation_repeat=False,
                animation_pingpong=False,
                animation_speed=1,
                animation_from=0,
                animation_to=inf,
                flip_horizontal=False,
                flip_vertical=False,
                visible=True,
                keep=True,
            ),
            attach_text=True,
        )

    def duration(self):
        """
        Returns
        -------
        duration of spec (in seconds) : float
            if image is not an animated gif, 0 will be returned

            does not take animation_pingpong, animation_from or animation_to into consideration
        """
        image_container = ImageContainer(self.image(self.env._t))
        return image_container._duration


def AnimateGrid(spacing: float = 100, env: "Environment" = None, **kwargs):
    """
    Draws a grid with text labels

    Parameters
    ----------
    spacing : float
        spacing of the grid lines in vertical and horizontal direction

    env : Environment
        environment where the component is defined

        if omitted, default_env will be used

    **kwargs : dict
        extra parameters to be given to AnimateLine, like linecolor, textcolor, font, visible
    """
    if env is None:
        env = g.default_env

    for y in arange(env.y0(), env.y1() + 1, spacing):
        AnimateLine(spec=(0, 0, env.x1() - env.x0(), 0), x=0, y=y, text=str(y), text_anchor="sw", env=env, **kwargs)

    for x in arange(env.x0(), env.x1() + 1, spacing):
        AnimateLine(spec=(0, 0, 0, env.y1() - env.y0()), x=x, y=0, text=str(x), text_anchor="se", env=env, **kwargs)


class ComponentGenerator(Component):
    """
    Component generator object

    A component generator can be used to genetate components

    There are two ways of generating components:

    - according to a given inter arrival time (iat) value or distribution
    - random spread over a given time interval

    Parameters
    ----------
    component_class : callable, usually a subclass of Component or Pdf/Pmf or Cdf distribution
        the type of components to be generated

        in case of a distribution, the Pdf/Pmf or Cdf should return a callable

    generator_name : str
        name of the component generator.

        if the name ends with a period (.),
        auto serializing will be applied

        if the name end with a comma,
        auto serializing starting at 1 will be applied

        if omitted, the name will be derived from the name of the component_class, padded with '.generator'

    at : float or distribution
        time where the generator starts time

        if omitted, now is used

        if distribution, the distribution is sampled

    delay : float or distribution
        delay where the generator starts (at = now + delay)

        if omitted, no delay

        if distribution, the distribution is sampled

    till : float or distribution
        time up to which components should be generated

        if omitted, no end

        if distribution, the distribution is sampled

    duration : float or distribution
        duration to which components should be generated (till = now + duration)

        if omitted, no end

        if distribution, the distribution is sampled

    number : int or distribution
        (maximum) number of components to be generated

        if distribution, the distribution is sampled

    iat : float or distribution
        inter arrival time (distribution).

        if None (default), a random spread over the interval (at, till) will be used


    force_at : bool
        for iat generation:

            if False (default), the first component will be generated at time = at + sample from the iat

            if True, the first component will be generated at time = at

        for random spread generation:

            if False (default), no force for time = at

            if True, force the first generation at time = at


    force_till : bool
        only possible for random spread generation:

        if False (default), no force for time = till

        if True, force the last generated component at time = till


    disturbance : callable (usually a distribution)
        for each component to be generated, the disturbance call (sampling) is added
        to the actual generation time.

        disturbance may only be used together with iat. The force_at parameter is not
        allowed in that case.

    suppress_trace : bool
        suppress_trace indicator

        if True, the component generator events will be excluded from the trace

        If False (default), the component generator will be traced

        Can be queried or set later with the suppress_trace method.

    suppress_pause_at_step : bool
        suppress_pause_at_step indicator

        if True, if this component generator becomes current, do not pause when stepping

        If False (default), the component generator will be paused when stepping

        Can be queried or set later with the suppress_pause_at_step method.

    equidistant : bool
        spread the arrival moments evenly over the defined duration

        in this case, iat may not be specified and number=1 is not allowed.

        force_at and force_till are ignored.

    at_end : callable
        function called upon termination of the generator.

        e.g. env.main().activate()

    moments : iterable
        specifies the moments when the components have to be generated. It is not required that these are sorted.

        note that the moments are specified in the current time unit

        cannot be used together with at, delay, till, duration, number, iat,force_at, force_till, disturbance or equidistant

    env : Environment
        environment where the component is defined

        if omitted, default_env will be used

    Note
    ----
    For iat distributions: if till/duration and number are specified, the generation stops whichever condition
    comes first.
    """

    def __init__(
        self,
        component_class: Type,
        generator_name: str = None,
        at: Union[float, Callable] = None,
        delay: Union[float, Callable] = None,
        till: Union[float, Callable] = None,
        duration: Union[float, Callable] = None,
        number: Union[int, Callable] = None,
        iat=None,
        force_at: bool = False,
        force_till: bool = False,
        suppress_trace: bool = False,
        suppress_pause_at_step: bool = False,
        disturbance: Callable = None,
        equidistant: bool = False,
        at_end: Callable = None,
        moments: Iterable = None,
        env: "Environment" = None,
        **kwargs,
    ):
        if generator_name is None:
            if inspect.isclass(component_class) and issubclass(component_class, Component):
                generator_name = str(component_class).split(".")[-1][:-2] + ".generator."
            elif isinstance(component_class, _Distribution):
                generator_name = str(component_class) + ".generator."
            else:
                generator_name = component_class.__name__ + ".generator."
        if env is None:
            env = g.default_env
        self.env = env
        self.overridden_lineno = env._frame_to_lineno(_get_caller_frame())

        if not callable(component_class):
            raise ValueError("component_class must be a callable")
        if moments is not None:
            if any(prop for prop in (at, delay, till, duration, number, iat, force_at, force_till, disturbance, equidistant)):
                raise ValueError(
                    "specifying at, delay, till,duration, number, iat,force_at, force_till, disturbance or equidistant is not allowed, if moments is specified"
                )
            if callable(moments):
                moments = moments()
            moments = sorted([env.spec_to_time(moment) for moment in moments])

        self.component_class = component_class
        self.iat = iat
        self.disturbance = disturbance
        self.force_at = force_at
        self.at_end = (lambda: None) if at_end is None else at_end

        if disturbance:  # falsy values are interpreted as no disturbance
            if iat is None and not equidistant:
                raise ValueError("disturbance can only be used with an iat")
            if not issubclass(component_class, Component):
                raise ValueError("component_class has to be a Component subclass if disturbance is specified.")
        at = env.spec_to_time(at)
        delay = env.spec_to_duration(delay)
        if delay is not None and at is not None:
            raise ValueError("delay and at specified.")
        if delay is None:
            delay = 0
        at = env._now + delay if at is None else at + env._offset
        till = env.spec_to_time(till)
        duration = env.spec_to_duration(duration)
        if till is None:
            if duration is None:
                self.till = inf
            else:
                self.till = at + duration
        else:
            if duration is None:
                self.till = till + env._offset
            else:
                raise ValueError("till and duration specified.")
        if callable(number):
            self.number = int(number())
        else:
            self.number = inf if number is None else int(number)
        if self.till < at:
            raise ValueError("at > till")
        if self.number < 0:
            raise ValueError("number < 0")
        if self.number < 1:
            at = None
            process = ""
        else:
            if (self.iat is None and not equidistant) or moments:
                if not moments:
                    if till == inf or self.number == inf:
                        raise ValueError("iat not specified --> till and number need to be specified")
                    if disturbance is not None:
                        raise ValueError("iat not specified --> disturbance not allowed")

                    moments = sorted([Uniform(at, till)() for _ in range(self.number)])
                    if force_at or force_till:
                        if number == 1:
                            if force_at and force_till:
                                raise ValueError("force_at and force_till does not allow number=1")
                            moments = [at] if force_at else [till]
                        else:
                            v_at = at if force_at else moments[0]
                            v_till = till if force_till else moments[-1]
                            min_moment = moments[0]
                            max_moment = moments[-1]
                            moments = [interpolate(moment, min_moment, max_moment, v_at, v_till) for moment in moments]
                self.intervals = [t1 - t0 for t0, t1 in zip([0] + moments, moments)]
                at = self.intervals[0]
                self.intervals[0] = 0
                process = "do_spread_yieldless" if env._yieldless else "do_spread"
            else:
                if equidistant:
                    force_till = False  # just to prevent errors further on
                    force_at = True  # just to prevent errors further on
                    duration = self.till - at
                    if duration < 0:
                        raise ValueError("at > till not allowed for equidistant")
                    if duration == inf:
                        raise ValueError("infinite duration not allowed for equidistant")
                    if self.number == 1:
                        raise ValueError("number=1 not allowed for equidistant")
                    if self.iat is not None:
                        raise ValueError("iat not allowed for equidistant")

                    self.iat = duration / (self.number - 1)
                    self.till = inf  # let numbers do the end

                if force_till:
                    raise ValueError("force_till is not allowed for iat generators")
                if not force_at:
                    if not self.disturbance:
                        if callable(self.iat):
                            at += self.iat()
                        else:
                            at += self.iat
                if at > self.till:
                    at = self.till
                    process = "do_finalize"
                else:
                    if self.disturbance:
                        process = "do_iat_disturbance_yieldless" if env._yieldless else "do_iat_disturbance"
                    else:
                        process = "do_iat_yieldless" if env._yieldless else "do_iat"
        self.kwargs = kwargs

        super().__init__(name=generator_name, env=env, process=process, at=at, suppress_trace=suppress_trace, suppress_pause_at_step=suppress_pause_at_step)

    def do_spread(self):
        for interval in self.intervals:
            yield self.hold(interval)
            save_default_env = g.default_env
            g.default_env = self.env
            if isinstance(self.component_class, _Distribution):
                self.component_class()(**self.kwargs)
            else:
                self.component_class(**self.kwargs)
            g.default_env = save_default_env

        self.env.print_trace("", "", "all components generated")
        self.at_end()

    def do_iat(self):
        n = 0
        while True:
            save_default_env = g.default_env
            g.default_env = self.env
            if isinstance(self.component_class, _Distribution):
                self.component_class()(**self.kwargs)
            else:
                self.component_class(**self.kwargs)
            g.default_env = save_default_env
            n += 1
            if n >= self.number:
                self.env.print_trace("", "", f"{n} components generated")
                self.at_end()
                return
            if callable(self.iat):
                t = self.env._now + self.iat()
            else:
                t = self.env._now + self.iat
            if t > self.till:
                yield self.activate(process="do_finalize", at=self.till)

            yield self.hold(till=t)

    def do_iat_disturbance(self):
        n = 0
        while True:
            save_default_env = g.default_env
            g.default_env = self.env
            if callable(self.iat):
                iat = self.iat()
            else:
                iat = self.iat
            g.default_env = save_default_env

            if callable(self.disturbance):
                disturbance = self.disturbance()
            else:
                disturbance = self.disturbance
            if self.force_at:
                at = self.env._now + disturbance
            else:
                at = self.env._now + iat + disturbance
            if at > self.till:
                yield self.activate(process="do_finalize", at=self.till)
            if isinstance(self.component_class, _Distribution):
                component_class = self.component_class()
            else:
                component_class = self.component_class
            component_class(at=at, **self.kwargs)
            n += 1
            if n >= self.number:
                self.env.print_trace("", "", str(n) + " components generated")
                return
            t = self.env._now + iat

            yield self.hold(till=t)

    def do_spread_yieldless(self):
        for interval in self.intervals:
            self.hold(interval)
            save_default_env = g.default_env
            g.default_env = self.env
            if isinstance(self.component_class, _Distribution):
                self.component_class()(**self.kwargs)
            else:
                self.component_class(**self.kwargs)
            g.default_env = save_default_env

        self.env.print_trace("", "", "all components generated")
        self.at_end()

    def do_iat_yieldless(self):
        n = 0
        while True:
            save_default_env = g.default_env
            g.default_env = self.env
            if isinstance(self.component_class, _Distribution):
                self.component_class()(**self.kwargs)
            else:
                self.component_class(**self.kwargs)
            g.default_env = save_default_env
            n += 1
            if n >= self.number:
                self.env.print_trace("", "", f"{n} components generated")
                self.at_end()
                return
            if callable(self.iat):
                t = self.env._now + self.iat()
            else:
                t = self.env._now + self.iat
            if t > self.till:
                self.activate(process="do_finalize", at=self.till)

            self.hold(till=t)

    def do_iat_disturbance_yieldless(self):
        n = 0
        while True:
            save_default_env = g.default_env
            g.default_env = self.env
            if callable(self.iat):
                iat = self.iat()
            else:
                iat = self.iat
            g.default_env = save_default_env

            if callable(self.disturbance):
                disturbance = self.disturbance()
            else:
                disturbance = self.disturbance
            if self.force_at:
                at = self.env._now + disturbance
            else:
                at = self.env._now + iat + disturbance
            if at > self.till:
                self.activate(process="do_finalize", at=self.till)
            if isinstance(self.component_class, _Distribution):
                component_class = self.component_class()
            else:
                component_class = self.component_class
            component_class(at=at, **self.kwargs)
            n += 1
            if n >= self.number:
                self.env.print_trace("", "", str(n) + " components generated")
                return
            t = self.env._now + iat

            self.hold(till=t)

    def do_finalize(self):
        self.env.print_trace("", "", "till reached")
        self.at_end()

    def print_info(self, as_str: bool = False, file: TextIO = None) -> str:
        """
        prints information about the component generator

        Parameters
        ----------
        as_str: bool
            if False (default), print the info
            if True, return a string containing the info

        file: file
            if None(default), all output is directed to stdout

            otherwise, the output is directed to the file

        Returns
        -------
        info (if as_str is True) : str
        """
        result = []
        result.append(object_to_str(self) + " " + hex(id(self)))
        result.append("  name=" + self.name())

        result.append("  class of components=" + str(self.component_class).split(".")[-1][:-2])
        result.append("  iat=" + repr(self.iat))
        result.append("  suppress_trace=" + str(self._suppress_trace))
        result.append("  suppress_pause_at_step=" + str(self._suppress_pause_at_step))
        result.append("  status=" + self.status.value)
        result.append("  mode=" + self._modetxt().strip())
        result.append("  mode_time=" + self.env.time_to_str(self.mode_time()))
        result.append("  creation_time=" + self.env.time_to_str(self.creation_time()))
        result.append("  scheduled_time=" + self.env.time_to_str(self.scheduled_time()))
        return return_or_print(result, as_str, file)


class _BlindVideoMaker(Component):
    def handle(self):
        self.env._t = self.env._now
        self.env.animation_pre_tick_sys(self.env.t())  # required to update sys objects, like AnimateQueue
        if self.env._animate3d:
            if not self.env._gl_initialized:
                self.env.animation3d_init()

            self.env._exclude_from_animation = "*"  # makes that both video and non video over2d animation objects are shown

            an_objects3d = sorted(self.env.an_objects3d, key=lambda obj: (obj.layer(self.env._t), obj.sequence))
            for an in an_objects3d:
                if an.keep(self.env._t):
                    if an.visible(self.env._t):
                        an.draw(self.env._t)
                else:
                    an.remove()
            self.env._exclude_from_animation = "only in video"

        self.env._save_frame()

    def process_yielded(self):
        while True:
            self.handle()
            if not self.env._event_list:
                break  # we've finished
            yield self.hold(self.env._speed / self.env._fps)

    def process_yieldless(self):
        while True:
            self.handle()
            if not self.env._event_list:
                break  # we've finished
            self.hold(self.env._speed / self.env._fps)


class Random(random.Random):
    """
    defines a randomstream, equivalent to random.Random()

    Parameters
    ----------
    seed : any hashable
        default: None
    """

    def __init__(self, seed: Hashable = None):
        random.Random.__init__(self, seed)


class _Distribution:
    _mean: float

    def bounded_sample(
        self,
        lowerbound: float = None,
        upperbound: float = None,
        fail_value: float = None,
        number_of_retries: int = None,
        include_lowerbound: bool = True,
        include_upperbound: bool = True,
    ) -> float:
        """
        Parameters
        ----------
        lowerbound : float
            sample values < lowerbound will be rejected (at most 100 retries)

            if omitted, no lowerbound check

        upperbound : float
            sample values > upperbound will be rejected (at most 100 retries)

            if omitted, no upperbound check

        fail_value : float
            value to be used if. after number_of_tries retries, sample is still not within bounds

            default: lowerbound, if specified, otherwise upperbound

        number_of_tries : int
            number of tries before fail_value is returned

            default: 100

        include_lowerbound : bool
            if True (default), the lowerbound may be included.
            if False, the lowerbound will be excluded.

        include_upperbound : bool
            if True (default), the upperbound may be included.
            if False, the upperbound will be excluded.

        Returns
        -------
        Bounded sample of a distribution : depending on distribution type (usually float)

        Note
        ----
        If, after number_of_tries retries, the sampled value is still not within the given bounds,
        fail_value  will be returned

        Samples that cannot be converted (only possible with /Pmf and CumPdf/CumPmf) to float
        are assumed to be within the bounds.
        """
        return Bounded(self, lowerbound, upperbound, fail_value, number_of_retries, include_lowerbound, include_upperbound).sample()

    def __call__(self, *args, **kwargs):
        return self.sample(*args, **kwargs)

    def __pos__(self):
        return _Expression(self, 0, operator.add)

    def __neg__(self):
        return _Expression(0, self, operator.sub)

    def __add__(self, other):
        return _Expression(self, other, operator.add)

    def __radd__(self, other):
        return _Expression(other, self, operator.add)

    def __sub__(self, other):
        return _Expression(self, other, operator.sub)

    def __rsub__(self, other):
        return _Expression(other, self, operator.sub)

    def __mul__(self, other):
        return _Expression(self, other, operator.mul)

    def __rmul__(self, other):
        return _Expression(other, self, operator.mul)

    def __truediv__(self, other):
        return _Expression(self, other, operator.truediv)

    def __rtruediv__(self, other):
        return _Expression(other, self, operator.truediv)

    def __floordiv__(self, other):
        return _Expression(self, other, operator.floordiv)

    def __rfloordiv__(self, other):
        return _Expression(other, self, operator.floordiv)

    def __pow__(self, other):
        return _Expression(self, other, operator.pow)

    def __rpow__(self, other):
        return _Expression(other, self, operator.pow)

    def register_time_unit(self, time_unit, env):
        self.time_unit = "" if time_unit is None else time_unit
        self.time_unit_factor = _time_unit_factor(time_unit, env)


class _Expression(_Distribution):
    """
    expression distribution

    This class is only created when using an expression with one ore more distributions.

    Note
    ----
    The randomstream of the distribution(s) in the expression are used.
    """

    def __init__(self, dis0, dis1, op):
        if isinstance(dis0, Constant):
            self.dis0 = dis0._mean
        else:
            self.dis0 = dis0
        if isinstance(dis1, Constant):
            self.dis1 = dis1._mean
        else:
            self.dis1 = dis1
        self.op = op

    def sample(self) -> Any:
        """
        Returns
        -------
        Sample of the expression of distribution(s) : float
        """
        if isinstance(self.dis0, _Distribution):
            v0 = self.dis0.sample()
        else:
            v0 = self.dis0
        if isinstance(self.dis1, _Distribution):
            v1 = self.dis1.sample()
        else:
            v1 = self.dis1
        return self.op(v0, v1)

    def mean(self) -> float:
        """
        Returns
        -------
        Mean of the expression of distribution(s) : float
            returns nan if mean can't be calculated
        """
        if isinstance(self.dis0, _Distribution):
            m0 = self.dis0.mean()
        else:
            m0 = self.dis0
        if isinstance(self.dis1, _Distribution):
            m1 = self.dis1.mean()
        else:
            m1 = self.dis1

        if self.op == operator.add:
            return m0 + m1

        if self.op == operator.sub:
            return m0 - m1

        if self.op == operator.mul:
            if isinstance(self.dis0, _Distribution) and isinstance(self.dis1, _Distribution):
                return nan
            else:
                return m0 * m1

        if self.op == operator.truediv:
            if isinstance(self.dis1, _Distribution):
                return nan
            else:
                return m0 / m1

        if self.op == operator.floordiv:
            return nan

        if self.op == operator.pow:
            return nan

    def __repr__(self):
        return "_Expression"

    def print_info(self, as_str: bool = False, file: TextIO = None) -> str:
        """
        prints information about the expression of distribution(s)

        Parameters
        ----------
        as_str: bool
            if False (default), print the info
            if True, return a string containing the info

        file: file
            if None(default), all output is directed to stdout

            otherwise, the output is directed to the file

        Returns
        -------
        info (if as_str is True) : str
        """
        result = []
        result.append("_Expression " + hex(id(self)))
        result.append("  mean=" + str(self.mean()))
        return return_or_print(result, as_str, file)


class Map(_Distribution):
    """
    Parameters
    ----------
    dis : distribution
        distribution to be mapped

    function : function
        function to be applied on each sampled value

    Examples
    --------
    d = sim.Map(sim.Normal(10,3), lambda x: x if x > 0 else 0)  # map negative samples to zero
    d = sim.Map(sim.Uniform(1,7), int)  # die simulator
    """

    def __init__(self, dis: "_Distribution", function: Callable):
        self.dis = dis
        self.function = function

    def sample(self) -> Any:
        sample = self.dis.sample()
        return self.function(sample)

    def mean(self) -> float:
        return nan

    def __repr__(self):
        return "Map " + self.dis.__repr__()


class Bounded(_Distribution):
    """
    Parameters
    ----------
    dis : distribution
        distribution to be bounded

    lowerbound : float
        sample values < lowerbound will be rejected (at most 100 retries)

        if omitted, no lowerbound check

    upperbound : float
        sample values > upperbound will be rejected (at most 100 retries)

        if omitted, no upperbound check

    fail_value : float
        value to be used if. after number_of_tries retries, sample is still not within bounds

        default: lowerbound, if specified, otherwise upperbound

    number_of_tries : int
        number of tries before fail_value is returned

        default: 100

    include_lowerbound : bool
        if True (default), the lowerbound may be included.
        if False, the lowerbound will be excluded.

    include_upperbound : bool
        if True (default), the upperbound may be included.
        if False, the upperbound will be excluded.

    time_unit : str
        specifies the time unit of the lowerbound or upperbound

        must be one of "years", "weeks", "days", "hours", "minutes", "seconds", "milliseconds", "microseconds"

        default : no conversion


    Note
    ----
    If, after number_of_tries retries, the sampled value is still not within the given bounds,
    fail_value  will be returned

    Samples that cannot be converted to float (only possible with Pdf/Pmf and CumPdf)
    are assumed to be within the bounds.
    """

    def __init__(
        self,
        dis,
        lowerbound: float = None,
        upperbound: float = None,
        fail_value: float = None,
        number_of_retries: int = None,
        include_lowerbound: bool = True,
        include_upperbound: bool = True,
        time_unit: str = None,
        env: "Environment" = None,
    ):
        self.register_time_unit(time_unit, env)
        self.lowerbound = -inf if lowerbound is None else lowerbound * self.time_unit_factor
        self.upperbound = inf if upperbound is None else upperbound * self.time_unit_factor

        if self.lowerbound > self.upperbound:
            raise ValueError("lowerbound > upperbound")

        if fail_value is None:
            self.fail_value = self.upperbound if self.lowerbound == -inf else self.lowerbound
        else:
            self.fail_value = fail_value

        self.dis = dis
        self.lowerbound_op = operator.ge if include_lowerbound else operator.gt
        self.upperbound_op = operator.le if include_upperbound else operator.lt
        self.number_of_retries = 100 if number_of_retries is None else number_of_retries

    def sample(self) -> float:
        if (self.lowerbound == -inf) and (self.upperbound == inf):
            return self.dis.sample()
        for _ in range(self.number_of_retries):
            sample = self.dis.sample()
            try:
                samplefloat = float(sample)
            except (ValueError, TypeError):
                return sample  # a value that cannot be converted to a float is sampled is assumed to be correct

            if self.lowerbound_op(samplefloat, self.lowerbound) and self.upperbound_op(samplefloat, self.upperbound):
                return sample

        return self.fail_value

    def mean(self) -> float:
        """
        Returns
        -------
        Mean of the expression of bounded distribution : float
            unless no bounds are specified, returns nan
        """
        if (self.lowerbound == -inf) and (self.upperbound == inf):
            return self.dis.mean()
        return nan

    def __repr__(self):
        return "Bounded " + self.dis.__repr__()

    def print_info(self, as_str: bool = False, file: TextIO = None) -> str:
        """
        prints information about the expression of distribution(s)

        Parameters
        ----------
        as_str: bool
            if False (default), print the info
            if True, return a string containing the info

        file: file
            if None(default), all output is directed to stdout

            otherwise, the output is directed to the file

        Returns
        -------
        info (if as_str is True) : str
        """
        result = []
        result.append("Bounded " + self.dis.__repr__() + " " + hex(id(self)))
        result.append("  mean=" + str(self.mean()))
        return return_or_print(result, as_str, file)


class Exponential(_Distribution):
    """
    exponential distribution

    Parameters
    ----------
    mean : float
        mean of the distribtion (beta)

        if omitted, the rate is used

        must be >0

    time_unit : str
        specifies the time unit

        must be one of "years", "weeks", "days", "hours", "minutes", "seconds", "milliseconds", "microseconds"

        default : no conversion


    rate : float
        rate of the distribution (lambda)

        if omitted, the mean is used

        must be >0

    randomstream: randomstream
        randomstream to be used

        if omitted, random will be used

        if used as random.Random(12299)
        it assigns a new stream with the specified seed

    env : Environment
        environment where the distribution is defined

        if omitted, default_env will be used

    Note
    ----
    Either mean or rate has to be specified, not both
    """

    def __init__(self, mean: float = None, time_unit: str = None, rate: float = None, randomstream: Any = None, env: "Environment" = None):
        self.register_time_unit(time_unit, env)
        if mean is None:
            if rate is None:
                raise TypeError("neither mean nor rate are specified")
            else:
                if rate <= 0:
                    raise ValueError("rate<=0")
                self._mean = 1 / rate
        else:
            if rate is None:
                if mean <= 0:
                    raise ValueError("mean<=0")
                self._mean = mean
            else:
                raise TypeError("both mean and rate are specified")

        if randomstream is None:
            self.randomstream = random
        else:
            _checkrandomstream(randomstream)
            self.randomstream = randomstream

    def __repr__(self):
        return "Exponential"

    def print_info(self, as_str: bool = False, file: TextIO = None) -> str:
        """
        prints information about the distribution

        Parameters
        ----------
        as_str: bool
            if False (default), print the info
            if True, return a string containing the info

        file: file
            if None(default), all output is directed to stdout

            otherwise, the output is directed to the file

        Returns
        -------
        info (if as_str is True) : str
        """
        result = []
        result.append("Exponential distribution " + hex(id(self)))
        result.append("  mean=" + str(self._mean) + " " + self.time_unit)
        result.append("  rate (lambda)=" + str(1 / self._mean) + (" " if self.time_unit == "" else " /" + self.time_unit))
        result.append("  randomstream=" + hex(id(self.randomstream)))
        return return_or_print(result, as_str, file)

    def sample(self) -> float:
        """
        Returns
        -------
        Sample of the distribution : float
        """
        return self.randomstream.expovariate(1 / (self._mean)) * self.time_unit_factor

    def mean(self) -> float:
        """
        Returns
        -------
        Mean of the distribution : float
        """
        return self._mean * self.time_unit_factor


class Normal(_Distribution):
    """
    normal distribution

    Parameters
    ----------
    mean : float
        mean of the distribution

    standard_deviation : float
        standard deviation of the distribution

        if omitted, coefficient_of_variation, is used to specify the variation
        if neither standard_devation nor coefficient_of_variation is given, 0 is used,
        thus effectively a contant distribution

        must be >=0

    coefficient_of_variation : float
        coefficient of variation of the distribution

        if omitted, standard_deviation is used to specify variation

        the resulting standard_deviation must be >=0

    use_gauss : bool
        if False (default), use the random.normalvariate method

        if True, use the random.gauss method

        the documentation for random states that the gauss method should be slightly faster,
        although that statement is doubtful.

    time_unit : str
        specifies the time unit

        must be one of "years", "weeks", "days", "hours", "minutes", "seconds", "milliseconds", "microseconds"

        default : no conversion


    randomstream: randomstream
        randomstream to be used

        if omitted, random will be used

        if used as random.Random(12299)
        it assigns a new stream with the specified seed

    env : Environment
        environment where the distribution is defined

        if omitted, default_env will be used
    """

    def __init__(
        self,
        mean: float,
        standard_deviation: float = None,
        time_unit: str = None,
        coefficient_of_variation: float = None,
        use_gauss: bool = False,
        randomstream: Any = None,
        env: "Environment" = None,
    ):
        self.register_time_unit(time_unit, env)
        self._use_gauss = use_gauss
        self._mean = mean
        if standard_deviation is None:
            if coefficient_of_variation is None:
                self._standard_deviation = 0.0
            else:
                if mean == 0:
                    raise ValueError("coefficient_of_variation not allowed with mean = 0")
                self._standard_deviation = coefficient_of_variation * mean
        else:
            if coefficient_of_variation is None:
                self._standard_deviation = standard_deviation
            else:
                raise TypeError("both standard_deviation and coefficient_of_variation specified")
        if self._standard_deviation < 0:
            raise ValueError("standard_deviation < 0")
        if randomstream is None:
            self.randomstream = random
        else:
            _checkrandomstream(randomstream)
            self.randomstream = randomstream

    def __repr__(self):
        return "Normal"

    def print_info(self, as_str: bool = False, file: TextIO = None) -> str:
        """
        prints information about the distribution

        Parameters
        ----------
        as_str: bool
            if False (default), print the info
            if True, return a string containing the info

        file: file
            if None(default), all output is directed to stdout

            otherwise, the output is directed to the file

        Returns
        -------
        info (if as_str is True) : str
        """
        result = []
        result.append("Normal distribution " + hex(id(self)))
        result.append("  mean=" + str(self._mean) + " " + self.time_unit)
        result.append("  standard_deviation=" + str(self._standard_deviation) + " " + self.time_unit)
        if self._mean == 0:
            result.append("  coefficient of variation= N/A")
        else:
            result.append("  coefficient_of_variation=" + str(self._standard_deviation / self._mean))
        if self._use_gauss:
            result.append("  use_gauss=True")
        result.append("  randomstream=" + hex(id(self.randomstream)))
        return return_or_print(result, as_str, file)

    def sample(self) -> float:
        """
        Returns
        -------
        Sample of the distribution : float
        """
        if self._use_gauss:
            return self.randomstream.gauss(self._mean, self._standard_deviation) * self.time_unit_factor
        else:
            return self.randomstream.normalvariate(self._mean, self._standard_deviation) * self.time_unit_factor

    def mean(self) -> float:
        """
        Returns
        -------
        Mean of the distribution : float
        """
        return self._mean * self.time_unit_factor


class IntUniform(_Distribution):
    """
    integer uniform distribution, i.e. sample integer values between lowerbound and upperbound (inclusive)

    Parameters
    ----------
    lowerbound : int
        lowerbound of the distribution

    upperbound : int
        upperbound of the distribution

        if omitted, lowerbound will be used

        must be >= lowerbound

    time_unit : str
        specifies the time unit. the sampled integer value will be multiplied by the appropriate factor

        must be one of "years", "weeks", "days", "hours", "minutes", "seconds", "milliseconds", "microseconds"

        default : no conversion


    randomstream: randomstream
        randomstream to be used

        if omitted, random will be used

        if used as random.Random(12299)
        it assigns a new stream with the specified seed

    env : Environment
        environment where the distribution is defined

        if omitted, default_env will be used

    Note
    ----
    In contrast to range, the upperbound is included.

    Example
    -------
    die = sim.IntUniform(1,6)
    for _ in range(10):
        print (die())

    This will print 10 throws of a die.
    """

    def __init__(self, lowerbound: int, upperbound: int = None, randomstream: Any = None, time_unit: str = None, env: "Environment" = None):
        self.register_time_unit(time_unit, env)
        self._lowerbound = lowerbound
        if upperbound is None:
            self._upperbound = lowerbound
        else:
            self._upperbound = upperbound
        if self._lowerbound > self._upperbound:
            raise ValueError("lowerbound>upperbound")
        if self._lowerbound != int(self._lowerbound):
            raise TypeError("lowerbound not integer")
        if self._upperbound != int(self._upperbound):
            raise TypeError("upperbound not integer")

        if randomstream is None:
            self.randomstream = random
        else:
            _checkrandomstream(randomstream)
            self.randomstream = randomstream
        self._mean = (self._lowerbound + self._upperbound) / 2

    def __repr__(self):
        return "IntUniform"

    def print_info(self, as_str: bool = False, file: TextIO = None) -> str:
        """
        prints information about the distribution

        Parameters
        ----------
        as_str: bool
            if False (default), print the info
            if True, return a string containing the info

        file: file
            if None(default), all output is directed to stdout

            otherwise, the output is directed to the file

        Returns
        -------
        info (if as_str is True) : str
        """
        result = []
        result.append("IntUniform distribution " + hex(id(self)))
        result.append("  lowerbound=" + str(self._lowerbound) + " " + self.time_unit)
        result.append("  upperbound=" + str(self._upperbound) + " " + self.time_unit)
        result.append("  randomstream=" + hex(id(self.randomstream)))
        return return_or_print(result, as_str, file)

    def sample(self) -> float:
        """
        Returns
        -------
        Sample of the distribution: int
        """
        return self.randomstream.randint(self._lowerbound, self._upperbound) * self.time_unit_factor

    def mean(self) -> float:
        """
        Returns
        -------
        Mean of the distribution : float
        """
        return self._mean * self.time_unit_factor


class Uniform(_Distribution):
    """
    uniform distribution

    Parameters
    ----------
    lowerbound : float
        lowerbound of the distribution

    upperbound : float
        upperbound of the distribution

        if omitted, lowerbound will be used

        must be >= lowerbound

    time_unit : str
        specifies the time unit

        must be one of "years", "weeks", "days", "hours", "minutes", "seconds", "milliseconds", "microseconds"

        default : no conversion


    randomstream: randomstream
        randomstream to be used

        if omitted, random will be used

        if used as random.Random(12299)
        it assigns a new stream with the specified seed

    env : Environment
        environment where the distribution is defined

        if omitted, default_env will be used
    """

    def __init__(self, lowerbound: float, upperbound: float = None, time_unit: str = None, randomstream: Any = None, env: "Environment" = None):
        self.register_time_unit(time_unit, env)
        self._lowerbound = lowerbound
        if upperbound is None:
            self._upperbound = lowerbound
        else:
            self._upperbound = upperbound
        if self._lowerbound > self._upperbound:
            raise ValueError("lowerbound>upperbound")
        if randomstream is None:
            self.randomstream = random
        else:
            _checkrandomstream(randomstream)
            self.randomstream = randomstream
        self._mean = (self._lowerbound + self._upperbound) / 2

    def __repr__(self):
        return "Uniform"

    def print_info(self, as_str: bool = False, file: TextIO = None) -> str:
        """
        prints information about the distribution

        Parameters
        ----------
        as_str: bool
            if False (default), print the info
            if True, return a string containing the info

        file: file
            if None(default), all output is directed to stdout

            otherwise, the output is directed to the file

        Returns
        -------
        info (if as_str is True) : str
        """
        result = []
        result.append("Uniform distribution " + hex(id(self)))
        result.append("  lowerbound=" + str(self._lowerbound) + " " + self.time_unit)
        result.append("  upperbound=" + str(self._upperbound) + " " + self.time_unit)
        result.append("  randomstream=" + hex(id(self.randomstream)))
        return return_or_print(result, as_str, file)

    def sample(self) -> float:
        """
        Returns
        -------
        Sample of the distribution: float
        """
        return self.randomstream.uniform(self._lowerbound, self._upperbound) * self.time_unit_factor

    def mean(self) -> float:
        """
        Returns
        -------
        Mean of the distribution : float
        """
        return self._mean * self.time_unit_factor


class Triangular(_Distribution):
    """
    triangular distribution

    Parameters
    ----------
    low : float
        lowerbound of the distribution

    high : float
        upperbound of the distribution

        if omitted, low will be used, thus effectively a constant distribution

        high must be >= low

    mode : float
        mode of the distribution

        if omitted, the average of low and high will be used, thus a symmetric triangular distribution

        mode must be between low and high

    time_unit : str
        specifies the time unit

        must be one of "years", "weeks", "days", "hours", "minutes", "seconds", "milliseconds", "microseconds"

        default : no conversion


    randomstream: randomstream
        randomstream to be used

        if omitted, random will be used

        if used as random.Random(12299)
        it assigns a new stream with the specified seed

    env : Environment
        environment where the distribution is defined

        if omitted, default_env will be used
    """

    def __init__(self, low: float, high: float = None, mode: float = None, time_unit: str = None, randomstream: Any = None, env: "Environment" = None):
        self.register_time_unit(time_unit, env)
        self._low = low
        if high is None:
            self._high = low
        else:
            self._high = high
        if mode is None:
            self._mode = (self._high + self._low) / 2
        else:
            self._mode = mode
        if self._low > self._high:
            raise ValueError("low>high")
        if self._low > self._mode:
            raise ValueError("low>mode")
        if self._high < self._mode:
            raise ValueError("high<mode")
        if randomstream is None:
            self.randomstream = random
        else:
            _checkrandomstream(randomstream)
            self.randomstream = randomstream
        self._mean = (self._low + self._mode + self._high) / 3

    def __repr__(self):
        return "Triangular"

    def print_info(self, as_str: bool = False, file: TextIO = None) -> str:
        """
        prints information about the distribution

        Parameters
        ----------
        as_str: bool
            if False (default), print the info
            if True, return a string containing the info

        file: file
            if None(default), all output is directed to stdout

            otherwise, the output is directed to the file

        Returns
        -------
        info (if as_str is True) : str
        """
        result = []
        result.append("Triangular distribution " + hex(id(self)))
        result.append("  low=" + str(self._low) + " " + self.time_unit)
        result.append("  high=" + str(self._high) + " " + self.time_unit)
        result.append("  mode=" + str(self._mode) + " " + self.time_unit)
        result.append("  randomstream=" + hex(id(self.randomstream)))
        return return_or_print(result, as_str, file)

    def sample(self) -> float:
        """
        Returns
        -------
        Sample of the distribtion : float
        """
        return self.randomstream.triangular(self._low, self._high, self._mode) * self.time_unit_factor

    def mean(self) -> float:
        """
        Returns
        -------
        Mean of the distribution : float
        """
        return self._mean * self.time_unit_factor


class Constant(_Distribution):
    """
    constant distribution

    Parameters
    ----------
    value : float
        value to be returned in sample

    time_unit : str
        specifies the time unit

        must be one of "years", "weeks", "days", "hours", "minutes", "seconds", "milliseconds", "microseconds"

        default : no conversion


    randomstream: randomstream
        randomstream to be used

        if omitted, random will be used

        if used as random.Random(12299)
        it assigns a new stream with the specified seed

        Note that this is only for compatibility with other distributions

    env : Environment
        environment where the distribution is defined

        if omitted, default_env will be used
    """

    def __init__(self, value: float, time_unit: str = None, randomstream: Any = None, env: "Environment" = None):
        self.register_time_unit(time_unit, env)
        self._value = value
        if randomstream is None:
            self.randomstream = random
        else:
            _checkrandomstream(randomstream)
            self.randomstream = randomstream
        self._mean = value
        self._mean *= self.time_unit_factor

    def __repr__(self):
        return "Constant"

    def print_info(self, as_str: bool = False, file: TextIO = None) -> str:
        """
        prints information about the distribution

        Parameters
        ----------
        as_str: bool
            if False (default), print the info
            if True, return a string containing the info

        file: file
            if None(default), all output is directed to stdout

            otherwise, the output is directed to the file

        Returns
        -------
        info (if as_str is True) : str
        """
        result = []
        result.append("Constant distribution " + hex(id(self)))
        result.append("  value=" + str(self._value) + " " + self.time_unit)
        result.append("  randomstream=" + hex(id(self.randomstream)))
        return return_or_print(result, as_str, file)

    def sample(self) -> float:
        """
        Returns
        -------
        sample of the distribution (= the specified constant) : float
        """
        return self._value * self.time_unit_factor

    def mean(self) -> float:
        """
        Returns
        -------
        mean of the distribution (= the specified constant) : float
        """
        return self._mean * self.time_unit_factor


class Poisson(_Distribution):
    """
    Poisson distribution

    Parameters
    ----------
    mean: float
        mean (lambda) of the distribution

    randomstream: randomstream
        randomstream to be used

        if omitted, random will be used

        if used as random.Random(12299)
        it assigns a new stream with the specified seed

    Note
    ----
    The run time of this function increases when mean (lambda) increases.

    It is not recommended to use mean (lambda) > 100
    """

    def __init__(self, mean: float, randomstream: Any = None, prefer_numpy: bool = False):
        if mean <= 0:
            raise ValueError("mean (lambda) <=0")

        self._mean = mean
        self._use_numpy = prefer_numpy and has_numpy()

        if randomstream is None:
            self.randomstream = random
        else:
            _checkrandomstream(randomstream)
            self.randomstream = randomstream

    def __repr__(self):
        return "Poisson"

    def print_info(self, as_str: bool = False, file: TextIO = None) -> str:
        """
        prints information about the distribution

        Parameters
        ----------
        as_str: bool
            if False (default), print the info
            if True, return a string containing the info

        file: file
            if None(default), all output is directed to stdout

            otherwise, the output is directed to the file

        Returns
        -------
        info (if as_str is True) : str
        """
        result = []
        result.append("Poisson distribution " + hex(id(self)))
        result.append("  mean (lambda)" + str(self._mean))
        return return_or_print(result, as_str, file)

    def sample_fallback(self):
        # from https://www.notion.so/940bd09c5be343888244beb21ed4a166?v=6bb0bd98d8fc47a081164069121ee396&p=1d3152108d7042bcbea3fa2b549fbe07&pm=s
        lam = self._mean
        t = 0
        n = 0
        while True:
            t += -math.log(self.randomstream.random()) / lam
            if t > 1:
                break
            n += 1
        return n

    def sample(self) -> int:
        """
        Returns
        -------
        Sample of the distribution : int
        """
        if self._use_numpy:
            return numpy.random.poisson(lam=self._mean)

        t = math.exp(-self._mean)
        s = t
        k = 0

        u = self.randomstream.random()
        last_s = inf
        while s < u:
            k += 1
            t *= self._mean / k
            s += t
            if last_s == s:  # avoid infinite loops
                return self.sample_fallback()  # uses a different (slower) algorithm
            last_s = s
        return k

    def mean(self) -> float:
        """
        Returns
        -------
        Mean of the distribution : float
        """
        return self._mean


class Weibull(_Distribution):
    """
    weibull distribution

    Parameters
    ----------
    scale: float
        scale of the distribution (alpha or k)

    shape: float
        shape of the distribution (beta or lambda)

        should be >0

    time_unit : str
        specifies the time unit

        must be one of "years", "weeks", "days", "hours", "minutes", "seconds", "milliseconds", "microseconds"

        default : no conversion


    randomstream: randomstream
        randomstream to be used

        if omitted, random will be used

        if used as random.Random(12299)
        it assigns a new stream with the specified seed

    env : Environment
        environment where the distribution is defined

        if omitted, default_env will be used
    """

    def __init__(self, scale: float, shape: float, time_unit: str = None, randomstream: Any = None, env: "Environment" = None):
        self.register_time_unit(time_unit, env)
        self._scale = scale
        if shape <= 0:
            raise ValueError("shape<=0")

        self._shape = shape
        if randomstream is None:
            self.randomstream = random
        else:
            _checkrandomstream(randomstream)
            self.randomstream = randomstream
        self._mean = self._scale * math.gamma((1 / self._shape) + 1)

    def __repr__(self):
        return "Weibull"

    def print_info(self, as_str: bool = False, file: TextIO = None) -> str:
        """
        prints information about the distribution

        Parameters
        ----------
        as_str: bool
            if False (default), print the info
            if True, return a string containing the info

        file: file
            if None(default), all output is directed to stdout

            otherwise, the output is directed to the file

        Returns
        -------
        info (if as_str is True) : str
        """
        result = []
        result.append("Weibull distribution " + hex(id(self)))
        result.append("  scale (alpha or k)=" + str(self._scale) + " " + self.time_unit)
        result.append("  shape (beta or lambda)=" + str(self._shape))
        result.append("  randomstream=" + hex(id(self.randomstream)))
        return return_or_print(result, as_str, file)

    def sample(self) -> float:
        """
        Returns
        -------
        Sample of the distribution : float
        """
        return self.randomstream.weibullvariate(self._scale, self._shape) * self.time_unit_factor

    def mean(self) -> float:
        """
        Returns
        -------
        Mean of the distribution : float
        """
        return self._mean * self.time_unit_factor


class Gamma(_Distribution):
    """
    gamma distribution

    Parameters
    ----------
    shape: float
        shape of the distribution (k)

        should be >0

    scale: float
        scale of the distribution (teta)

        should be >0

    time_unit : str
        specifies the time unit

        must be one of "years", "weeks", "days", "hours", "minutes", "seconds", "milliseconds", "microseconds"

        default : no conversion


    rate : float
        rate of the distribution (beta)

        should be >0

    randomstream: randomstream
        randomstream to be used

        if omitted, random will be used

        if used as random.Random(12299)
        it assigns a new stream with the specified seed


    env : Environment
        environment where the distribution is defined

        if omitted, default_env will be used

    Note
    ----
    Either scale or rate has to be specified, not both.
    """

    def __init__(self, shape: float, scale: float = None, time_unit: str = None, rate=None, randomstream: Any = None, env: "Environment" = None):
        self.register_time_unit(time_unit, env)
        if shape <= 0:
            raise ValueError("shape<=0")
        self._shape = shape
        if rate is None:
            if scale is None:
                raise TypeError("neither scale nor rate specified")
            else:
                if scale <= 0:
                    raise ValueError("scale<=0")
                self._scale = scale
        else:
            if scale is None:
                if rate <= 0:
                    raise ValueError("rate<=0")
                self._scale = 1 / rate
            else:
                raise TypeError("both scale and rate specified")

        if randomstream is None:
            self.randomstream = random
        else:
            _checkrandomstream(randomstream)
            self.randomstream = randomstream

        self._mean = self._shape * self._scale

    def __repr__(self):
        return "Gamma"

    def print_info(self, as_str: bool = False, file: TextIO = None) -> str:
        """
        prints information about the distribution

        Parameters
        ----------
        as_str: bool
            if False (default), print the info
            if True, return a string containing the info

        file: file
            if None(default), all output is directed to stdout

            otherwise, the output is directed to the file

        Returns
        -------
        info (if as_str is True) : str
        """
        result = []
        result.append("Gamma distribution " + hex(id(self)))
        result.append("  shape (k)=" + str(self._shape))
        result.append("  scale (teta)=" + str(self._scale) + " " + self.time_unit)
        result.append("  rate (beta)=" + str(1 / self._scale) + ("" if self.time_unit == "" else " /" + self.time_unit))
        result.append("  randomstream=" + hex(id(self.randomstream)))
        return return_or_print(result, as_str, file)

    def sample(self) -> float:
        """
        Returns
        -------
        Sample of the distribution : float
        """
        return self.randomstream.gammavariate(self._shape, self._scale) * self.time_unit_factor

    def mean(self) -> float:
        """
        Returns
        -------
        Mean of the distribution : float
        """
        return self._mean * self.time_unit_factor


class Beta(_Distribution):
    """
    beta distribution

    Parameters
    ----------
    alpha: float
        alpha shape of the distribution

        should be >0

    beta: float
        beta shape of the distribution

        should be >0

    randomstream: randomstream
        randomstream to be used

        if omitted, random will be used

        if used as random.Random(12299)
        it assigns a new stream with the specified seed
    """

    def __init__(self, alpha: float, beta: float, randomstream: Any = None):
        if alpha <= 0:
            raise ValueError("alpha<=0")
        self._alpha = alpha
        if beta <= 0:
            raise ValueError("beta<>=0")
        self._beta = beta

        if randomstream is None:
            self.randomstream = random
        else:
            _checkrandomstream(randomstream)
            self.randomstream = randomstream

        self._mean = self._alpha / (self._alpha + self._beta)

    def __repr__(self):
        return "Beta"

    def print_info(self, as_str: bool = False, file: TextIO = None) -> str:
        """
        prints information about the distribution

        Parameters
        ----------
        as_str: bool
            if False (default), print the info
            if True, return a string containing the info

        file: file
            if None(default), all output is directed to stdout

            otherwise, the output is directed to the file

        Returns
        -------
        info (if as_str is True) : str
        """
        result = []
        result.append("Beta distribution " + hex(id(self)))
        result.append("  alpha=" + str(self._alpha))
        result.append("  beta=" + str(self._beta))
        result.append("  randomstream=" + hex(id(self.randomstream)))
        return return_or_print(result, as_str, file)

    def sample(self) -> float:
        """
        Returns
        -------
        Sample of the distribution : float
        """
        return self.randomstream.betavariate(self._alpha, self._beta)

    def mean(self) -> float:
        """
        Returns
        -------
        Mean of the distribution : float
        """
        return self._mean


class Erlang(_Distribution):
    """
    erlang distribution

    Parameters
    ----------
    shape: int
        shape of the distribution (k)

        should be >0

    rate: float
        rate parameter (lambda)

        if omitted, the scale is used

        should be >0

    time_unit : str
        specifies the time unit

        must be one of "years", "weeks", "days", "hours", "minutes", "seconds", "milliseconds", "microseconds"

        default : no conversion


    scale: float
        scale of the distribution (mu)

        if omitted, the rate is used

        should be >0

    randomstream: randomstream
        randomstream to be used

        if omitted, random will be used

        if used as random.Random(12299)
        it assigns a new stream with the specified seed

    env : Environment
        environment where the distribution is defined

        if omitted, default_env will be used

    Note
    ----
    Either rate or scale has to be specified, not both.
    """

    def __init__(self, shape: float, rate: float = None, time_unit: str = None, scale: float = None, randomstream: Any = None, env: "Environment" = None):
        self.register_time_unit(time_unit, env)
        if int(shape) != shape:
            raise TypeError("shape not integer")
        if shape <= 0:
            raise ValueError("shape <=0")
        self._shape = shape
        if rate is None:
            if scale is None:
                raise TypeError("neither rate nor scale specified")
            else:
                if scale <= 0:
                    raise ValueError("scale<=0")
                self._rate = 1 / scale
        else:
            if scale is None:
                if rate <= 0:
                    raise ValueError("rate<=0")
                self._rate = rate
            else:
                raise ValueError("both rate and scale specified")

        if randomstream is None:
            self.randomstream = random
        else:
            _checkrandomstream(randomstream)
            self.randomstream = randomstream

        self._mean = self._shape / self._rate

    def __repr__(self):
        return "Erlang"

    def print_info(self, as_str: bool = False, file: TextIO = None) -> str:
        """
        prints information about the distribution

        Parameters
        ----------
        as_str: bool
            if False (default), print the info
            if True, return a string containing the info

        file: file
            if None(default), all output is directed to stdout

            otherwise, the output is directed to the file

        Returns
        -------
        info (if as_str is True) : str
        """
        result = []
        result.append("Erlang distribution " + hex(id(self)))
        result.append("  shape (k)=" + str(self._shape))
        result.append("  rate (lambda)=" + str(self._rate) + ("" if self.time_unit == "" else " /" + self.time_unit))
        result.append("  scale (mu)=" + str(1 / self._rate))
        result.append("  randomstream=" + hex(id(self.randomstream)))
        return return_or_print(result, as_str, file)

    def sample(self) -> float:
        """
        Returns
        -------
        Sample of the distribution : float
        """
        return self.randomstream.gammavariate(self._shape, 1 / self._rate) / self.time_unit_factor

    def mean(self) -> float:
        """
        Returns
        -------
        Mean of the distribution : float
        """
        return self._mean / self.time_unit_factor


class Cdf(_Distribution):
    """
    Cumulative distribution function

    Parameters
    ----------
    spec : list or tuple
        list with x-values and corresponding cumulative density
        (x1,c1,x2,c2, ...xn,cn)

        Requirements:

            x1<=x2<= ...<=xn

            c1<=c2<=cn

            c1=0

            cn>0

            all cumulative densities are auto scaled according to cn,
            so no need to set cn to 1 or 100.

    time_unit : str
        specifies the time unit

        must be one of "years", "weeks", "days", "hours", "minutes", "seconds", "milliseconds", "microseconds"

        default : no conversion


    randomstream: randomstream
        if omitted, random will be used

        if used as random.Random(12299)
        it defines a new stream with the specified seed

    env : Environment
        environment where the distribution is defined

        if omitted, default_env will be used
    """

    def __init__(self, spec: Iterable, time_unit: str = None, randomstream: Any = None, env: "Environment" = None):
        self.register_time_unit(time_unit, env)
        self._x = []
        self._cum = []
        if randomstream is None:
            self.randomstream = random
        else:
            _checkrandomstream(randomstream)
            self.randomstream = randomstream

        lastcum = 0
        lastx = -inf
        spec = list(spec)
        if not spec:
            raise TypeError("no arguments specified")
        if spec[1] != 0:
            raise ValueError("first cumulative value should be 0")
        while len(spec) > 0:
            x = spec.pop(0) * self.time_unit_factor
            if not spec:
                raise ValueError("uneven number of parameters specified")
            if x < lastx:
                raise ValueError(f"x value {x} is smaller than previous value {lastx}")
            cum = spec.pop(0)
            if cum < lastcum:
                raise ValueError(f"cumulative value {cum} is smaller than previous value {lastcum}")
            self._x.append(x)
            self._cum.append(cum)
            lastx = x
            lastcum = cum
        if lastcum == 0:
            raise ValueError("last cumulative value should be > 0")
        self._cum = [x / lastcum for x in self._cum]
        self._mean = 0
        for i in range(len(self._cum) - 1):
            self._mean += ((self._x[i] + self._x[i + 1]) / 2) * (self._cum[i + 1] - self._cum[i])

    def __repr__(self):
        return "Cdf"

    def print_info(self, as_str: bool = False, file: TextIO = None) -> str:
        """
        prints information about the distribution

        Parameters
        ----------
        as_str: bool
            if False (default), print the info
            if True, return a string containing the info

        file: file
            if None(default), all output is directed to stdout

            otherwise, the output is directed to the file

        Returns
        -------
        info (if as_str is True) : str
        """
        result = []
        result.append("Cdf distribution " + hex(id(self)))
        result.append("  randomstream=" + hex(id(self.randomstream)))
        return return_or_print(result, as_str, file)

    def sample(self) -> float:
        """
        Returns
        -------
        Sample of the distribution : float
        """
        r = self.randomstream.random()
        for i in range(len(self._cum)):
            if r < self._cum[i]:
                return interpolate(r, self._cum[i - 1], self._cum[i], self._x[i - 1], self._x[i])
        return self._x[i]

    def mean(self) -> float:
        """
        Returns
        -------
        Mean of the distribution : float
        """
        return self._mean


class Pdf(_Distribution):
    """
    Probability distribution function

    Parameters
    ----------
    spec : list, tuple or dict
        either

        -   if no probabilities specified:

            list/tuple with x-values and corresponding probability
            dict where the keys are re x-values and the values are probabilities
            (x0, p0, x1, p1, ...xn,pn)

        -   if probabilities is specified:

            list with x-values

    probabilities : iterable or float
        if omitted, spec contains the probabilities

        the iterable (p0, p1, ...pn) contains the probabilities of the corresponding
        x-values from spec.

        alternatively, if a float is given (e.g. 1), all x-values
        have equal probability. The value is not important.

    time_unit : str
        specifies the time unit

        must be one of "years", "weeks", "days", "hours", "minutes", "seconds", "milliseconds", "microseconds"

        default : no conversion


    randomstream : randomstream
        if omitted, random will be used

        if used as random.Random(12299)
        it assigns a new stream with the specified seed

    env : Environment
        environment where the distribution is defined

        if omitted, default_env will be used

    Note
    ----
    p0+p1=...+pn>0

    all densities are auto scaled according to the sum of p0 to pn,
    so no need to have p0 to pn add up to 1 or 100.

    The x-values can be any type.

    If it is a salabim distribution, not the distribution,
    but a sample will be returned when calling sample.


    This method is also available under the name Pmf
    """

    def __init__(self, spec: Union[Iterable, Dict], probabilities=None, time_unit: str = None, randomstream: Any = None, env: "Environment" = None):
        self.register_time_unit(time_unit, env)
        self._x = []
        self._cum = []
        if randomstream is None:
            self.randomstream = random
        else:
            _checkrandomstream(randomstream)
            self.randomstream = randomstream

        sump = 0
        sumxp = 0
        hasmean = True
        if probabilities is None:
            if not spec:
                raise TypeError("no arguments specified")
            if isinstance(spec, dict):
                xs = list(spec.keys())
                probabilities = list(spec.values())
            else:
                xs = spec[::2]
                probabilities = spec[1::2]
                if len(xs) != len(probabilities):
                    raise ValueError("uneven number of parameters specified")
        else:
            xs = list(spec)
            if hasattr(probabilities, "__iter__") and not isinstance(probabilities, str):
                probabilities = list(probabilities)
                if len(xs) != len(probabilities):
                    raise ValueError("length of x-values does not match length of probabilities")
            else:
                probabilities = len(spec) * [1]

        self.supports_n = probabilities[1:] == probabilities[:-1]

        for x, p in zip(xs, probabilities):
            if time_unit is not None:
                if isinstance(x, _Distribution):
                    raise TypeError("time_unit can't be combined with distribution value")
                try:
                    x = float(x) * self.time_unit_factor
                except (ValueError, TypeError):
                    raise TypeError("time_unit can't be combined with non numeric value")

            self._x.append(x)
            sump += p
            self._cum.append(sump)
            if isinstance(x, _Distribution):
                x = x._mean
            try:
                sumxp += float(x) * p
            except (ValueError, TypeError):
                hasmean = False

        if sump == 0:
            raise ValueError("at least one probability should be >0")

        self._cum = [x / sump for x in self._cum]
        if hasmean:
            self._mean = sumxp / sump
        else:
            self._mean = nan

    def __repr__(self):
        return "Pdf"

    def print_info(self, as_str: bool = False, file: TextIO = None) -> str:
        """
        prints information about the distribution

        Parameters
        ----------
        as_str: bool
            if False (default), print the info
            if True, return a string containing the info

        file: file
            if None(default), all output is directed to stdout

            otherwise, the output is directed to the file

        Returns
        -------
        info (if as_str is True) : str
        """
        result = []
        result.append("Pdf distribution " + hex(id(self)))
        result.append("  randomstream=" + hex(id(self.randomstream)))
        return return_or_print(result, as_str, file)

    def sample(self, n: int = None) -> Any:
        """
        Parameters
        ----------
        n : number of samples : int
            if not specified, specifies just return one sample, as usual

            if specified, return a list of n sampled values from the distribution without replacement.
            This requires that all probabilities are equal.

            If n > number of values in the Pdf distribution, n is assumed to be the number of values
            in the distribution.

            If a sampled value is a distribution, a sample from that distribution will be returned.

        Returns
        -------
        Sample of the distribution : any (usually float) or list
            In case n is specified, returns a list of n values

        """
        if self.supports_n:
            if n is None:
                return self.randomstream.sample(self._x, 1)[0]
            else:
                if n < 0:
                    raise ValueError("n < 0")
                n = min(n, len(self._x))
                xs = self.randomstream.sample(self._x, n)
                return [x.sample() if isinstance(x, _Distribution) else x for x in xs]
        else:
            if n is None:
                r = self.randomstream.random()
                for cum, x in zip([0] + self._cum, [0] + self._x):
                    if r <= cum:
                        if isinstance(x, _Distribution):
                            return x.sample()
                        return x
            else:
                raise ValueError("not all probabilities are the same")

    def mean(self) -> float:
        """
        Returns
        -------
        mean of the distribution : float
            if the mean can't be calculated (if not all x-values are scalars or distributions),
            nan will be returned.
        """
        return self._mean


class Pmf(Pdf):
    """
    Probability mass function

    Parameters
    ----------
    spec : list, tuple or dict
        either

        -   if no probabilities specified:

            list/tuple with x-values and corresponding probability
            dict where the keys are re x-values and the values are probabilities
            (x0, p0, x1, p1, ...xn,pn)

        -   if probabilities is specified:

            list with x-values

    probabilities : iterable or float
        if omitted, spec contains the probabilities

        the iterable (p0, p1, ...pn) contains the probabilities of the corresponding
        x-values from spec.

        alternatively, if a float is given (e.g. 1), all x-values
        have equal probability. The value is not important.

    time_unit : str
        specifies the time unit

        must be one of "years", "weeks", "days", "hours", "minutes", "seconds", "milliseconds", "microseconds"

        default : no conversion


    randomstream : randomstream
        if omitted, random will be used

        if used as random.Random(12299)
        it assigns a new stream with the specified seed

    env : Environment
        environment where the distribution is defined

        if omitted, default_env will be used

    Note
    ----
    p0+p1=...+pn>0

    all densities are auto scaled according to the sum of p0 to pn,
    so no need to have p0 to pn add up to 1 or 100.

    The x-values can be any type.

    If it is a salabim distribution, not the distribution,
    but a sample will be returned when calling sample.

    This method is also available under the name Pdf

    """

    def __repr__(self):
        return "Pmf"

    def print_info(self, as_str: bool = False, file: TextIO = None) -> str:
        """
        prints information about the distribution

        Parameters
        ----------
        as_str: bool
            if False (default), print the info
            if True, return a string containing the info

        file: file
            if None(default), all output is directed to stdout

            otherwise, the output is directed to the file

        Returns
        -------
        info (if as_str is True) : str
        """
        result = []
        result.append("Pmf distribution " + hex(id(self)))
        result.append("  randomstream=" + hex(id(self.randomstream)))
        return return_or_print(result, as_str, file)

    def sample(self, n: int = None) -> Any:
        """
        Parameters
        ----------
        n : number of samples : int
            if not specified, specifies just return one sample, as usual

            if specified, return a list of n sampled values from the distribution without replacement.
            This requires that all probabilities are equal.

            If n > number of values in the Pmf distribution, n is assumed to be the number of values
            in the distribution.

            If a sampled value is a distribution, a sample from that distribution will be returned.

        Returns
        -------
        Sample of the distribution : any (usually float) or list
            In case n is specified, returns a list of n values

        """
        if self.supports_n:
            if n is None:
                return self.randomstream.sample(self._x, 1)[0]
            else:
                if n < 0:
                    raise ValueError("n < 0")
                n = min(n, len(self._x))
                xs = self.randomstream.sample(self._x, n)
                return [x.sample() if isinstance(x, _Distribution) else x for x in xs]
        else:
            if n is None:
                r = self.randomstream.random()
                for cum, x in zip([0] + self._cum, [0] + self._x):
                    if r <= cum:
                        if isinstance(x, _Distribution):
                            return x.sample()
                        return x
            else:
                raise ValueError("not all probabilities are the same")

    def mean(self) -> float:
        """
        Returns
        -------
        mean of the distribution : float
            if the mean can't be calculated (if not all x-values are scalars or distributions),
            nan will be returned.
        """
        return self._mean


class CumPdf(_Distribution):
    """
    Cumulative Probability mass function

    Parameters
    ----------
    spec : list or tuple
        either

        -   if no cumprobabilities specified:

            list with x-values and corresponding cumulative probability
            (x0, p0, x1, p1, ...xn,pn)

        -   if cumprobabilities is specified:

            list with x-values

    cumprobabilities : list, tuple or float
        if omitted, spec contains the probabilities

        the list (p0, p1, ...pn) contains the cumulative probabilities of the corresponding
        x-values from spec.


    time_unit : str
        specifies the time unit

        must be one of "years", "weeks", "days", "hours", "minutes", "seconds", "milliseconds", "microseconds"

        default : no conversion


    randomstream : randomstream
        if omitted, random will be used

        if used as random.Random(12299)
        it assigns a new stream with the specified seed

    env : Environment
        environment where the distribution is defined

        if omitted, default_env will be used

    Note
    ----
    p0<=p1<=..pn>0

    all densities are auto scaled according to pn,
    so no need to have pn be 1 or 100.

    The x-values can be any type.

    If it is a salabim distribution, not the distribution,
    but a sample will be returned when calling sample.

    This method is also available under the name CumPmf
    """

    def __init__(
        self, spec: Iterable, cumprobabilities: Union[float, Iterable] = None, time_unit: str = None, randomstream: Any = None, env: "Environment" = None
    ):
        self.register_time_unit(time_unit, env)
        self._x = []
        self._cum = []
        if randomstream is None:
            self.randomstream = random
        else:
            _checkrandomstream(randomstream)
            self.randomstream = randomstream

        sump = 0
        sumxp = 0
        hasmean = True
        if not spec:
            raise TypeError("no arguments specified")
        if cumprobabilities is None:
            xs = spec[::2]
            cumprobabilities = spec[1::2]
            if len(xs) != len(cumprobabilities):
                raise ValueError("uneven number of parameters specified")
        else:
            if isinstance(cumprobabilities, (list, tuple)):
                cumprobabilities = list(cumprobabilities)
            else:
                raise TypeError("wrong type for cumulative probabilities")
            xs = list(spec)

            if len(xs) != len(cumprobabilities):
                raise ValueError("length of x-values does not match length of cumulative probabilities")

        for x, p in zip(xs, cumprobabilities):
            if time_unit is not None:
                if isinstance(x, _Distribution):
                    raise TypeError("time_unit can't be combined with distribution value")
                try:
                    x = float(x) * self.time_unit_factor
                except (ValueError, TypeError):
                    raise TypeError("time_unit can't be combined with non numeric value")
            self._x.append(x)
            p = p - sump
            if p < 0:
                raise ValueError("non increasing cumulative probabilities")
            sump += p
            self._cum.append(sump)
            if isinstance(x, _Distribution):
                x = x._mean
            try:
                sumxp += float(x) * p
            except (ValueError, TypeError):
                hasmean = False

        if sump == 0:
            raise ValueError("last cumulative probability should be >0")

        self._cum = [p / sump for p in self._cum]
        if hasmean:
            self._mean = sumxp / sump
        else:
            self._mean = nan

    def __repr__(self):
        return "CumPdf"

    def print_info(self, as_str: bool = False, file: TextIO = None) -> str:
        """
        prints information about the distribution

        Parameters
        ----------
        as_str: bool
            if False (default), print the info
            if True, return a string containing the info

        file: file
            if None(default), all output is directed to stdout

            otherwise, the output is directed to the file

        Returns
        -------
        info (if as_str is True) : str
        """
        result = []
        result.append("CumPdf distribution " + hex(id(self)))
        result.append("  randomstream=" + hex(id(self.randomstream)))
        return return_or_print(result, as_str, file)

    def sample(self) -> Any:
        """
        Returns
        -------
        Sample of the distribution : any (usually float)
        """
        r = self.randomstream.random()
        for cum, x in zip([0] + self._cum, [0] + self._x):
            if r <= cum:
                if isinstance(x, _Distribution):
                    return x.sample()
                return x

    def mean(self) -> float:
        """
        Returns
        -------
        mean of the distribution : float
            if the mean can't be calculated (if not all x-values are scalars or distributions),
            nan will be returned.
        """
        return self._mean


class CumPmf(CumPdf):
    """
    Cumulative Probability mass function

    Parameters
    ----------
    spec : list or tuple
        either

        -   if no cumprobabilities specified:

            list with x-values and corresponding cumulative probability
            (x0, p0, x1, p1, ...xn,pn)

        -   if cumprobabilities is specified:

            list with x-values

    cumprobabilities : list, tuple or float
        if omitted, spec contains the probabilities

        the list (p0, p1, ...pn) contains the cumulative probabilities of the corresponding
        x-values from spec.


    time_unit : str
        specifies the time unit

        must be one of "years", "weeks", "days", "hours", "minutes", "seconds", "milliseconds", "microseconds"

        default : no conversion


    randomstream : randomstream
        if omitted, random will be used

        if used as random.Random(12299)
        it assigns a new stream with the specified seed

    env : Environment
        environment where the distribution is defined

        if omitted, default_env will be used

    Note
    ----
    p0<=p1<=..pn>0

    all densities are auto scaled according to pn,
    so no need to have pn be 1 or 100.

    The x-values can be any type.

    If it is a salabim distribution, not the distribution,
    but a sample will be returned when calling sample.

    This method is also available under the name CumPdf
    """

    def __repr__(self):
        return "CumPmf"

    def print_info(self, as_str: bool = False, file: TextIO = None) -> str:
        """
        prints information about the distribution

        Parameters
        ----------
        as_str: bool
            if False (default), print the info
            if True, return a string containing the info

        file: file
            if None(default), all output is directed to stdout

            otherwise, the output is directed to the file

        Returns
        -------
        info (if as_str is True) : str
        """
        result = []
        result.append("CumPmf distribution " + hex(id(self)))
        result.append("  randomstream=" + hex(id(self.randomstream)))
        return return_or_print(result, as_str, file)

    def sample(self) -> Any:
        """
        Returns
        -------
        Sample of the distribution : any (usually float)
        """
        r = self.randomstream.random()
        for cum, x in zip([0] + self._cum, [0] + self._x):
            if r <= cum:
                if isinstance(x, _Distribution):
                    return x.sample()
                return x

    def mean(self) -> float:
        """
        Returns
        -------
        mean of the distribution : float
            if the mean can't be calculated (if not all x-values are scalars or distributions),
            nan will be returned.
        """
        return self._mean


class External(_Distribution):
    """
    External distribution function

    This distribution allows distributions from other modules, notably random, numpy.random and scipy.stats
    to be used as were they salabim distributions.

    Parameters
    ----------
    dis : external distribution
        either

        -   random.xxx

        -   numpy.random.xxx

        -   scipy.stats.xxx

    *args : any
        positional arguments to be passed to the dis distribution

    **kwargs : any
        keyword arguments to be passed to the dis distribution

    time_unit : str
        specifies the time unit

        must be one of "years", "weeks", "days", "hours", "minutes", "seconds", "milliseconds", "microseconds"

        default : no conversion


    env : Environment
        environment where the distribution is defined

        if omitted, default_env will be used
    """

    def __init__(self, dis: Any, *args, **kwargs):
        self.dis_is_scipy = False
        if "scipy" in sys.modules:
            import scipy

            self.dis_is_scipy = isinstance(dis, (scipy.stats.rv_continuous, scipy.stats.rv_discrete))
        self.dis = dis
        self.time_unit = None
        time_unit = None
        env = None
        for kwarg in list(kwargs.keys()):
            if kwarg == "time_unit":
                time_unit = kwargs[kwarg]
                del kwargs[kwarg]
            if kwarg == "env":
                env = kwargs[kwarg]
                del kwargs[kwarg]
        self.args = args
        self.kwargs = kwargs
        self.register_time_unit(time_unit, env)
        self.samples = []
        if self.dis_is_scipy:
            self._mean = self.dis.mean(**{k: v for k, v in self.kwargs.items() if k not in ("size", "random_state")})
        else:
            self._mean = nan

    def sample(self) -> Any:
        """
        Returns
        -------
        Sample of the distribution via external distribution method : any (usually float)
        """
        if not self.samples:
            if self.dis_is_scipy:
                samples = self.dis.rvs(*self.args, **self.kwargs)
            else:
                samples = self.dis(*self.args, **self.kwargs)
            if has_numpy() and isinstance(samples, numpy.ndarray):
                self.samples = samples.tolist()
            else:
                self.samples = [samples]
        return self.samples.pop() * self.time_unit_factor

    def mean(self) -> float:
        """
        Returns
        -------
        mean of the distribution : float
            only available for scipy.stats distribution. Otherwise nan will be returned.
        """
        return self._mean * self.time_unit_factor

    def __repr__(self):
        try:
            descr = self.dis.__name__
        except AttributeError:
            descr = self.dis.name  # for scipy.stats distributions
        return "External(" + descr + ")"

    def print_info(self, as_str: bool = False, file: TextIO = None) -> str:
        """
        prints information about the distribution

        Parameters
        ----------
        as_str: bool
            if False (default), print the info
            if True, return a string containing the info

        file: file
            if None(default), all output is directed to stdout

            otherwise, the output is directed to the file

        Returns
        -------
        info (if as_str is True) : str
        """
        result = []
        try:
            descr = [self.dis.__name__]
        except AttributeError:
            descr = [self.dis.name]  # for scipy.stats distributions
        for arg in self.args:
            descr.append(repr(arg))
        for kwarg in self.kwargs:
            descr.append(kwarg + "=" + repr(self.kwargs[kwarg]))
        if self.time_unit != "":
            descr.append("time_unit=" + repr(self.time_unit))
        result.append("External(" + ", ".join(descr) + ") distribution " + hex(id(self)))
        return return_or_print(result, as_str, file)


class Distribution(_Distribution):
    """
    Generate a distribution from a string

    Parameters
    ----------
    spec : str
        - string containing a valid salabim distribution, where only the first
          letters are relevant and casing is not important. Note that Erlang,
          Cdf, CumPdf and Poisson require at least two letters
          (Er, Cd, Cu and Po)
        - string containing one float (c1), resulting in Constant(c1)
        - string containing two floats seperated by a comma (c1,c2),
          resulting in a Uniform(c1,c2)
        - string containing three floats, separated by commas (c1,c2,c3),
          resulting in a Triangular(c1,c2,c3)

    time_unit : str
        Supported time_units:

        "years", "weeks", "days", "hours", "minutes", "seconds", "milliseconds", "microseconds"

        if spec has a time_unit as well, this parameter is ignored

    randomstream : randomstream
        if omitted, random will be used

        if used as random.Random(12299)
        it assigns a new stream with the specified seed


    Note
    ----
    The randomstream in the specifying string is ignored.

    It is possible to use expressions in the specification, as long these
    are valid within the context of the salabim module, which usually implies
    a global variable of the salabim package.

    Examples
    --------
    Uniform(13)  ==> Uniform(13)

    Uni(12,15)   ==> Uniform(12,15)

    UNIF(12,15)  ==> Uniform(12,15)

    N(12,3)      ==> Normal(12,3)

    Tri(10,20).  ==> Triangular(10,20,15)

    10.          ==> Constant(10)

    12,15        ==> Uniform(12,15)

    (12,15)      ==> Uniform(12,15)

    Exp(a)       ==> Exponential(100), provided sim.a=100

    E(2)         ==> Exponential(2)
    Er(2,3)      ==> Erlang(2,3)
    """

    def __init__(self, spec: str, randomstream: Any = None, time_unit: str = None):
        spec_orig = spec

        sp = spec.split("(")
        pre = sp[0].upper().strip()

        # here we have either a string starting with a ( of no ( at all
        if (pre == "") or not ("(" in spec):
            spec = spec.replace(")", "")  # get rid of closing parenthesis
            spec = spec.replace("(", "")  # get rid of starting parenthesis
            sp = spec.split(",")
            if len(sp) == 1:
                c1 = sp[0]
                spec = f"Constant({c1})"
            elif len(sp) == 2:
                c1 = sp[0]
                c2 = sp[1]
                spec = f"Uniform({c1}, {c2})"
            elif len(sp) == 3:
                c1 = sp[0]
                c2 = sp[1]
                c3 = sp[2]
                spec = f"Triangular({c1}, {c2}, {c3})"
            else:
                raise ValueError("incorrect specifier", spec_orig)

        else:
            for distype in (
                "Uniform",
                "Constant",
                "Triangular",
                "Exponential",
                "Normal",
                "Cdf",
                "Pdf",
                "CumPdf",
                "Weibull",
                "Gamma",
                "Erlang",
                "Beta",
                "IntUniform",
                "Poisson",
                "External",
            ):
                if pre == distype.upper()[: len(pre)]:
                    sp[0] = distype
                    spec = "(".join(sp)
                    break
        if time_unit is None:
            d = eval(spec)
        else:
            try:
                # try and add the time_unit=... parameter at the end
                d = eval(spec.strip()[:-1] + ", time_unit=" + repr(time_unit) + ")")
            except SyntaxError as e:
                if str(e).startswith("keyword argument repeated"):
                    d = eval(spec)
                else:
                    raise
            except TypeError as e:
                if "got multiple values" in str(e):
                    d = eval(spec)
                else:
                    raise

        if randomstream is None:
            self.randomstream = random
        else:
            _checkrandomstream(randomstream)
            self.randomstream = randomstream
        self._distribution = d
        try:
            self._mean = d._mean
        except AttributeError:
            self._mean = nan

    def __repr__(self):
        return self._distribution.__repr__()

    def print_info(self, as_str: bool = False, file: TextIO = None) -> str:
        """
        prints information about the distribution

        Parameters
        ----------
        as_str: bool
            if False (default), print the info
            if True, return a string containing the info

        file: file
            if None(default), all output is directed to stdout

            otherwise, the output is directed to the file

        Returns
        -------
        info (if as_str is True) : str
        """
        return self._distribution.print_info(as_str=as_str, file=file)

    def sample(self) -> Any:
        """
        Returns
        -------
        Sample of the  distribution : any (usually float)
        """
        self._distribution.randomstream = self.randomstream
        return self._distribution.sample()

    def mean(self) -> float:
        """
        Returns
        -------
        Mean of the distribution : float
        """
        return self._mean


class State:
    """
    State

    Parameters
    ----------
    name : str
        name of the state

        if the name ends with a period (.),
        auto serializing will be applied

        if the name end with a comma,
        auto serializing starting at 1 will be applied

        if omitted, the name will be derived from the class
        it is defined in (lowercased)

    value : any, preferably printable
        initial value of the state

        if omitted, False

    monitor : bool
        if True (default) , the waiters queue and the value are monitored

        if False, monitoring is disabled.

    type : str
        specifies how the state values are monitored. Using a
        int, uint of float type results in less memory usage and better
        performance. Note that you should avoid the number not to use
        as this is used to indicate 'off'

        -  "any" (default) stores values in a list. This allows for
           non numeric values. In calculations the values are
           forced to a numeric value (0 if not possible) do not use -inf
        -  "bool" bool (False, True). Actually integer >= 0 <= 254 1 byte do not use 255
        -  "int8" integer >= -127 <= 127 1 byte do not use -128
        -  "uint8" integer >= 0 <= 254 1 byte do not use 255
        -  "int16" integer >= -32767 <= 32767 2 bytes do not use -32768
        -  "uint16" integer >= 0 <= 65534 2 bytes do not use 65535
        -  "int32" integer >= -2147483647 <= 2147483647 4 bytes do not use -2147483648
        -  "uint32" integer >= 0 <= 4294967294 4 bytes do not use 4294967295
        -  "int64" integer >= -9223372036854775807 <= 9223372036854775807 8 bytes do not use -9223372036854775808
        -  "uint64" integer >= 0 <= 18446744073709551614 8 bytes do not use 18446744073709551615
        -  "float" float 8 bytes do not use -inf

    env : Environment
        environment to be used

        if omitted, default_env is used
    """

    def __init__(self, name: str = None, value: Any = False, type: str = "any", monitor: bool = True, env: "Environment" = None, *args, **kwargs):
        self.env = _set_env(env)
        _set_name(name, self.env._nameserializeState, self)
        self._value = value
        with self.env.suppress_trace():
            self._waiters = Queue(name="waiters of " + self.name(), monitor=monitor, env=self.env)
            self._waiters._isinternal = True
        self.value = _StateMonitor(parent=self, name="Value of " + self.name(), level=True, initial_tally=value, monitor=monitor, type=type, env=self.env)
        if self.env._trace:
            self.env.print_trace("", "", self.name() + " create", "value = " + repr(self._value))
        self.setup(*args, **kwargs)

    def setup(self) -> None:
        """
        called immediately after initialization of a state.

        by default this is a dummy method, but it can be overridden.

        only keyword arguments will be passed
        """
        pass

    def register(self, registry: List) -> "State":
        """
        registers the state in the registry

        Parameters
        ----------
        registry : list
            list of (to be) registered objetcs

        Returns
        -------
        state (self) : State

        Note
        ----
        Use State.deregister if state does not longer need to be registered.
        """
        if not isinstance(registry, list):
            raise TypeError("registry not list")
        if self in registry:
            raise ValueError(self.name() + " already in registry")
        registry.append(self)
        return self

    def deregister(self, registry: List) -> "State":
        """
        deregisters the state in the registry

        Parameters
        ----------
        registry : list
            list of registered states

        Returns
        -------
        state (self) : State
        """
        if not isinstance(registry, list):
            raise TypeError("registry not list")
        if self not in registry:
            raise ValueError(self.name() + " not in registry")
        registry.remove(self)
        return self

    def __repr__(self):
        return object_to_str(self) + " (" + self.name() + ")"

    def print_histograms(self, exclude: Iterable = (), as_str: bool = False, file: TextIO = None, graph_scale: float = None) -> str:
        """
        print histograms of the waiters queue and the value monitor

        Parameters
        ----------
        exclude : tuple or list
            specifies which queues or monitors to exclude

            default: ()

        as_str: bool
            if False (default), print the histograms
            if True, return a string containing the histograms

        file: file
            if None(default), all output is directed to stdout

            otherwise, the output is directed to the file

        graph_scale : float
            Scale in the graphical representation of the % and cum% (default=80)

        Returns
        -------
        histograms (if as_str is True) : str
        """
        result = []
        if self.waiters() not in exclude:
            result.append(self.waiters().print_histograms(exclude=exclude, as_str=True, graph_scale=graph_scale))
        if self.value not in exclude:
            result.append(self.value.print_histogram(as_str=True, graph_scale=graph_scale))
        return return_or_print(result, as_str, file)

    def print_info(self, as_str: bool = False, file: TextIO = None) -> str:
        """
        prints info about the state

        Parameters
        ----------
        as_str: bool
            if False (default), print the info
            if True, return a string containing the info

        file: file
            if None(default), all output is directed to stdout

            otherwise, the output is directed to the file

        Returns
        -------
        info (if as_str is True) : str
        """
        result = []
        result.append(object_to_str(self) + " " + hex(id(self)))
        result.append("  name=" + self.name())
        result.append("  value=" + str(self._value))
        if self._waiters:
            result.append("  waiting component(s):")
            mx = self._waiters._head.successor
            while mx != self._waiters._tail:
                c = mx.component
                mx = mx.successor
                values = ""
                for s, value, valuetype in c._waits:
                    if s == self:
                        if values != "":
                            values = values + ", "
                        values = values + str(value)

                result.append("    " + pad(c.name(), 20) + " value(s): " + values)
        else:
            result.append("  no waiting components")
        return return_or_print(result, as_str, file)

    def __call__(self):
        return self._value

    def get(self) -> Any:
        """
        get value of the state

        Returns
        -------
        value of the state : any
            Instead of this method, the state can also be called directly, like


            level = sim.State("level")

            ...

            print(level())

            print(level.get())  # identical

        """
        return self._value

    def set(self, value: Any = True):
        """
        set the value of the state

        Parameters
        ----------
        value : any (preferably printable)
            if omitted, True

            if there is a change, the waiters queue will be checked
            to see whether there are waiting components to be honored

        Note
        ----
        This method is identical to reset, except the default value is True.
        """
        if self.env._trace:
            self.env.print_trace("", "", self.name() + " set", "value = " + repr(value))
        if self._value != value:
            self._value = value
            self.value.tally(value)
            self._trywait()

    def reset(self, value: Any = False):
        """
        reset the value of the state

        Parameters
        ----------
        value : any (preferably printable)
            if omitted, False

            if there is a change, the waiters queue will be checked
            to see whether there are waiting components to be honored

        Note
        ----
        This method is identical to set, except the default value is False.
        """
        if self.env._trace:
            self.env.print_trace("", "", self.name() + " reset", "value = " + repr(value))
        if self._value != value:
            self._value = value
            self.value.tally(value)
            self._trywait()

    def trigger(self, value: Any = True, value_after: Any = None, max: Union[float, int] = inf):
        """
        triggers the value of the state

        Parameters
        ----------
        value : any (preferably printable)
            if omitted, True


        value_after : any (preferably printable)
            after the trigger, this will be the new value.

            if omitted, return to the the before the trigger.

        max : int
            maximum number of components to be honored for the trigger value

            default: inf

        Note
        ----
            The value of the state will be set to value, then at most
            max waiting components for this state  will be honored and next
            the value will be set to value_after and again checked for possible
            honors.
        """
        if value_after is None:
            value_after = self._value
        if self.env._trace:
            self.env.print_trace("", "", self.name() + " trigger", " value = " + str(value) + " --> " + str(value_after) + " allow " + str(max) + " components")
        self._value = value
        self.value.tally(value)  # strictly speaking, not required
        self._trywait(max)
        self._value = value_after
        self.value.tally(value_after)
        self._trywait()

    def _trywait(self, max=inf): # this _trywait of a state
        mx = self._waiters._head.successor
        while mx != self._waiters._tail:
            c = mx.component
            mx = mx.successor
            if c._trywait():
                max -= 1
                if max == 0:
                    return

    def monitor(self, value: bool = None) -> None:
        """
        enables/disables the state monitors and value monitor

        Parameters
        ----------
        value : bool
            if True, monitoring will be on.

            if False, monitoring is disabled

            if not specified, no change

        Note
        ----
        it is possible to individually control requesters().monitor(),
            value.monitor()
        """
        self.waiters().monitor(value)
        self.value.monitor(value)

    def all_monitors(self) -> Tuple["Monitor"]:
        """
        returns all monitors belonging to the state

        Returns
        -------
        all monitors : tuple of monitors
        """
        return (self.waiters().length, self.waiters().length_of_stay, self.value)

    def reset_monitors(self, monitor: bool = None, stats_only: bool = None) -> None:
        """
        resets the monitor for the state's value and the monitors of the waiters queue

        Parameters
        ----------
        monitor : bool
            if True, monitoring will be on.

            if False, monitoring is disabled

            if omitted, no change of monitoring state

        stats_only : bool
            if True, only statistics will be collected (using less memory, but also less functionality)

            if False, full functionality

            if omittted, no change of stats_only
        """
        self._waiters.reset_monitors(monitor=monitor, stats_only=stats_only)
        self.value.reset(monitor=monitor, stats_only=stats_only)

    def _get_value(self):
        return self._value

    def name(self, value: str = None) -> str:
        """
        Parameters
        ----------
        value : str
            new name of the state
            if omitted, no change

        Returns
        -------
        Name of the state : str

        Note
        ----
        base_name and sequence_number are not affected if the name is changed

        All derived named are updated as well.
        """
        if value is not None:
            self._name = value
            self._waiters.name("waiters of " + value)
            self.value.name("Value of " + value)

        return self._name

    def base_name(self) -> str:
        """
        Returns
        -------
        base name of the state (the name used at initialization): str
        """
        return getattr(self, "_base_name", self._name)

    def sequence_number(self) -> int:
        """
        Returns
        -------
        sequence_number of the state : int
            (the sequence number at initialization)

            normally this will be the integer value of a serialized name.

            Non serialized names (without a dot or a comma at the end)
            will return 1)
        """
        return getattr(self, "_sequence_number", 1)

    def print_statistics(self, as_str: bool = False, file: TextIO = None) -> str:
        """
        prints a summary of statistics of the state

        Parameters
        ----------
        as_str: bool
            if False (default), print the statistics
            if True, return a string containing the statistics

        file: file
            if None(default), all output is directed to stdout

            otherwise, the output is directed to the file

        Returns
        -------
        statistics (if as_str is True) : str
        """
        result = []
        result.append(f"Statistics of {self.name()} at {fn(self.env._now - self.env._offset, 13, 3)}")
        result.append(self.waiters().length.print_statistics(show_header=False, show_legend=True, do_indent=True, as_str=True))
        result.append("")
        result.append(self.waiters().length_of_stay.print_statistics(show_header=False, show_legend=False, do_indent=True, as_str=True))
        result.append("")
        result.append(self.value.print_statistics(show_header=False, show_legend=False, do_indent=True, as_str=True))
        return return_or_print(result, as_str, file)

    def waiters(self) -> Queue:
        """
        Returns
        -------
        queue containing all components waiting for this state : Queue
        """
        return self._waiters


class Resource:
    """
    Resource

    Parameters
    ----------
    name : str
        name of the resource

        if the name ends with a period (.),
        auto serializing will be applied

        if the name end with a comma,
        auto serializing starting at 1 will be applied

        if omitted, the name will be derived from the class
        it is defined in (lowercased)

    capacity : float
        capacity of the resource

        if omitted, 1

    initial_claimed_quantity : float
        initial claimed quantity. Only allowed to be non zero for anonymous resources

        if omitted, 0

    anonymous : bool
        anonymous specifier

        if True, claims are not related to any component. This is useful
        if the resource is actually just a level.

        if False, claims belong to a component.

    prememptive : bool
        if True, components with a lower priority will be bumped out of the claimers queue if possible
        if False (default), no bumping

    honor_only_first : bool
        if True, only the first component of requesters will be honoured (default: False)

    honor_only_highest_priority : bool
        if True, only component with the priority of the first requester will be honoured (default: False)
        Note: only respected if honor_only_first is False

    monitor : bool
        if True (default), the requesters queue, the claimers queue,
        the capacity, the available_quantity and the claimed_quantity are monitored

        if False, monitoring is disabled.

    env : Environment
        environment to be used

        if omitted, default_env is used
    """

    def __init__(
        self,
        name: str = None,
        capacity: float = 1,
        initial_claimed_quantity: float = 0,
        anonymous: bool = False,
        preemptive: bool = False,
        honor_only_first: bool = False,
        honor_only_highest_priority: bool = False,
        monitor: bool = True,
        env: "Environment" = None,
        *args,
        **kwargs,
    ):
        self.env = _set_env(env)
        if initial_claimed_quantity != 0:
            if not anonymous:
                raise ValueError("initial_claimed_quantity != 0 only allowed for anonymous resources")

        self._capacity = capacity
        self._honor_only_first = honor_only_first
        self._honor_only_highest_priority = honor_only_highest_priority

        _set_name(name, self.env._nameserializeResource, self)
        with self.env.suppress_trace():
            self._requesters = Queue(name="requesters of " + self.name(), monitor=monitor, env=self.env)
            self._requesters._isinternal = True
            self._claimers = Queue(name="claimers of " + self.name(), monitor=monitor, env=self.env)
            self._claimers._isinternal = True
            self._claimers._isclaimers = True  # used by Component.isbumped()

        self._claimed_quantity = initial_claimed_quantity
        self._anonymous = anonymous
        self._preemptive = preemptive
        self._minq = inf
        self._trying = False

        self.capacity = _CapacityMonitor("Capacity of " + self.name(), level=True, initial_tally=capacity, monitor=monitor, type="float", env=self.env)
        self.capacity.parent = self
        self.claimed_quantity = _SystemMonitor(
            "Claimed quantity of " + self.name(), level=True, initial_tally=initial_claimed_quantity, monitor=monitor, type="float", env=self.env
        )
        self.available_quantity = _SystemMonitor(
            "Available quantity of " + self.name(), level=True, initial_tally=capacity - initial_claimed_quantity, monitor=monitor, type="float", env=self.env
        )

        self.occupancy = _SystemMonitor("Occupancy of " + self.name(), level=True, initial_tally=0, monitor=monitor, type="float", env=self.env)
        if self.env._trace:
            self.env.print_trace("", "", self.name() + " create", "capacity=" + str(self._capacity) + (" anonymous" if self._anonymous else ""))
        self.setup(*args, **kwargs)

    def ispreemptive(self) -> bool:
        """
        Returns
        -------
        True if preemptive, False otherwise : bool
        """

        return self._preemptive

    def setup(self):
        """
        called immediately after initialization of a resource.

        by default this is a dummy method, but it can be overridden.

        only keyword arguments are passed
        """
        pass

    def all_monitors(self) -> Tuple["Monitor"]:
        """
        returns all mononitors belonging to the resource

        Returns
        -------
        all monitors : tuple of monitors
        """
        return (
            self.requesters().length,
            self.requesters().length_of_stay,
            self.claimers().length,
            self.claimers().length_of_stay,
            self.capacity,
            self.available_quantity,
            self.claimed_quantity,
            self.occupancy,
        )

    def reset_monitors(self, monitor: bool = None, stats_only: bool = None) -> None:
        """
        resets the resource monitors

        Parameters
        ----------
        monitor : bool
            if True, monitoring will be on.

            if False, monitoring is disabled

            if omitted, no change of monitoring state

        stats_only : bool
            if True, only statistics will be collected (using less memory, but also less functionality)

            if False, full functionality

            if omittted, no change of stats_only

        Note
        ----
            it is possible to reset individual monitoring with
            claimers().reset_monitors(),
            requesters().reset_monitors,
            capacity.reset(),
            available_quantity.reset() or
            claimed_quantity.reset() or
            occupancy.reset()
        """

        self.requesters().reset_monitors(monitor=monitor, stats_only=stats_only)
        self.claimers().reset_monitors(monitor=monitor, stats_only=stats_only)
        for m in (self.capacity, self.available_quantity, self.claimed_quantity, self.occupancy):
            m.reset(monitor=monitor, stats_only=stats_only)

    def print_statistics(self, as_str: bool = False, file: TextIO = None) -> str:
        """
        prints a summary of statistics of a resource

        Parameters
        ----------
        as_str: bool
            if False (default), print the statistics
            if True, return a string containing the statistics

        file: file
            if None(default), all output is directed to stdout

            otherwise, the output is directed to the file

        Returns
        -------
        statistics (if as_str is True) : str
        """
        result = []
        result.append(f"Statistics of {self.name()} at {(self.env._now - self.env._offset):13.3f}")
        show_legend = True
        for q in [self.requesters(), self.claimers()]:
            result.append(q.length.print_statistics(show_header=False, show_legend=show_legend, do_indent=True, as_str=True))
            show_legend = False
            result.append("")
            result.append(q.length_of_stay.print_statistics(show_header=False, show_legend=show_legend, do_indent=True, as_str=True))
            result.append("")

        for m in (self.capacity, self.available_quantity, self.claimed_quantity, self.occupancy):
            result.append(m.print_statistics(show_header=False, show_legend=show_legend, do_indent=True, as_str=True))
            result.append("")
        return return_or_print(result, as_str, file)

    def print_histograms(self, exclude=(), as_str: bool = False, file: TextIO = None, graph_scale: float = None) -> str:
        """
        prints histograms of the requesters and claimers queue as well as
        the capacity, available_quantity and claimed_quantity timstamped monitors of the resource

        Parameters
        ----------
        exclude : tuple or list
            specifies which queues or monitors to exclude

            default: ()

        as_str: bool
            if False (default), print the histograms
            if True, return a string containing the histograms

        file: file
            if None(default), all output is directed to stdout

            otherwise, the output is directed to the file

        graph_scale : float
            Scale in the graphical representation of the % and cum% (default=80)

        Returns
        -------
        histograms (if as_str is True) : str
        """
        result = []
        for q in (self.requesters(), self.claimers()):
            if q not in exclude:
                result.append(q.print_histograms(exclude=exclude, as_str=True, graph_scale=graph_scale))
        for m in (self.capacity, self.available_quantity, self.claimed_quantity, self.occupancy):
            if m not in exclude:
                result.append(m.print_histogram(as_str=True, graph_scale=graph_scale))
        return return_or_print(result, as_str, file)

    def monitor(self, value: bool) -> None:
        """
        enables/disables the resource monitors

        Parameters
        ----------
        value : bool
            if True, monitoring is enabled

            if False, monitoring is disabled


        Note
        ----
        it is possible to individually control monitoring with claimers().monitor()
        and requesters().monitor(), capacity.monitor(), available_quantity.monitor),
        claimed_quantity.monitor() or occupancy.monitor()
        """
        self.requesters().monitor(value)
        self.claimers().monitor(value)
        for m in (self.capacity, self.available_quantity, self.claimed_quantity, self.occupancy):
            m.monitor(value)

    def register(self, registry: List) -> "Resource":
        """
        registers the resource in the registry

        Parameters
        ----------
        registry : list
            list of (to be) registered objects

        Returns
        -------
        resource (self) : Resource

        Note
        ----
        Use Resource.deregister if resource does not longer need to be registered.
        """
        if not isinstance(registry, list):
            raise TypeError("registry not list")
        if self in registry:
            raise ValueError(self.name() + " already in registry")
        registry.append(self)
        return self

    def deregister(self, registry: List) -> "Resource":
        """
        deregisters the resource in the registry

        Parameters
        ----------
        registry : list
            list of registered components

        Returns
        -------
        resource (self) : Resource
        """
        if not isinstance(registry, list):
            raise TypeError("registry not list")
        if self not in registry:
            raise ValueError(self.name() + " not in registry")
        registry.remove(self)
        return self

    def __repr__(self):
        return object_to_str(self) + " (" + self.name() + ")"

    def print_info(self, as_str: bool = False, file: TextIO = None) -> str:
        """
        prints info about the resource

        Parameters
        ----------
        as_str: bool
            if False (default), print the info
            if True, return a string containing the info

        file: file
            if None(default), all output is directed to stdout

            otherwise, the output is directed to the file

        Returns
        -------
        info (if as_str is True) : str
        """
        result = []
        result.append(object_to_str(self) + " " + hex(id(self)))
        result.append("  name=" + self.name())
        result.append("  capacity=" + str(self._capacity))
        if self._requesters:
            result.append("  requesting component(s):")
            mx = self._requesters._head.successor
            while mx != self._requesters._tail:
                c = mx.component
                mx = mx.successor
                result.append("    " + pad(c.name(), 20) + " quantity=" + str(c._requests[self]))
        else:
            result.append("  no requesting components")

        result.append("  claimed_quantity=" + str(self._claimed_quantity))
        if self._claimed_quantity >= 0:
            if self._anonymous:
                result.append("  not claimed by any components," + " because the resource is anonymous")
            else:
                result.append("  claimed by:")
                mx = self._claimers._head.successor
                while mx != self._claimers._tail:
                    c = mx.component
                    mx = mx.successor
                    result.append("    " + pad(c.name(), 20) + " quantity=" + str(c._claims[self]))
        return return_or_print(result, as_str, file)

    def _tryrequest(self):
        # this is Resource._tryrequest
        if self._anonymous:
            if not self._trying:
                self._trying = True
                mx = mx_first = self._requesters._head.successor
                mx_first_priority = mx_first.priority
                while mx != self._requesters._tail:
                    if self._honor_only_first and mx != mx_first:
                        break
                    if self._honor_only_highest_priority and mx.priority != mx_first_priority:
                        break
                    c = mx.component
                    mx = mx.successor
                    c._tryrequest()
                    if c not in self._requesters:
                        mx = self._requesters._head.successor  # start again

                self._trying = False
        else:
            mx = mx_first = self._requesters._head.successor
            mx_first_priority = mx_first.priority

            while mx != self._requesters._tail:
                if self._honor_only_first and mx != mx_first:
                    break
                if self._honor_only_highest_priority and mx.priority != mx_first_priority:
                    break
                if self._minq > (self._capacity - self._claimed_quantity + 1e-8):
                    break  # inpossible to honor any more requests
                c = mx.component
                mx = mx.successor
                c._tryrequest()

    def release(self, quantity: float = None) -> None:
        """
        releases all claims or a specified quantity

        Parameters
        ----------
        quantity : float
            quantity to be released

            if not specified, the resource will be emptied completely

            for non-anonymous resources, all components claiming from this resource
            will be released.

        Note
        ----
        quantity may not be specified for a non-anonymous resoure
        """

        if self._anonymous:
            if quantity is None:
                q = self._claimed_quantity
            else:
                q = quantity

            self._claimed_quantity -= q
            if self._claimed_quantity < 1e-8:
                self._claimed_quantity = 0
            self.claimed_quantity.tally(self._claimed_quantity)
            self.occupancy.tally(0 if self._capacity <= 0 else self._claimed_quantity / self._capacity)
            self.available_quantity.tally(self._capacity - self._claimed_quantity)
            self._tryrequest()

        else:
            if quantity is not None:
                raise ValueError("no quantity allowed for non-anonymous resource")

            mx = self._claimers._head.successor
            while mx != self._claimers._tail:
                c = mx.component
                mx = mx.successor
                c.release(self)

    def requesters(self) -> Queue:
        """
        Return
        ------
        queue containing all components with not yet honored requests: Queue
        """
        return self._requesters

    def claimers(self) -> Queue:
        """
        Returns
        -------
        queue with all components claiming from the resource: Queue
            will be an empty queue for an anonymous resource
        """
        return self._claimers

    def set_capacity(self, cap: float) -> None:
        """
        Parameters
        ----------
        cap : float or int
            capacity of the resource

            this may lead to honoring one or more requests.

            if omitted, no change
        """
        self._capacity = cap
        self.capacity.tally(self._capacity)
        self.available_quantity.tally(self._capacity - self._claimed_quantity)
        self.occupancy.tally(0 if self._capacity <= 0 else self._claimed_quantity / self._capacity)
        self._tryrequest()

    def name(self, value: str = None) -> str:
        """
        Parameters
        ----------
        value : str
            new name of the resource
            if omitted, no change

        Returns
        -------
        Name of the resource : str

        Note
        ----
        base_name and sequence_number are not affected if the name is changed

        All derived named are updated as well.
        """
        if value is not None:
            self._name = value
            self._requesters.name("requesters of " + value)
            self._claimers.name("claimers of " + value)
            self.capacity.name("Capacity of " + value)
            self.claimed_quantity.name("Clamed quantity of " + value)
            self.available_quantity.name("Available quantity of " + value)
            self.occupancy.name("Occupancy of " + value)

        return self._name

    def base_name(self) -> str:
        """
        Returns
        -------
        base name of the resource (the name used at initialization): str
        """
        return getattr(self, "_base_name", self._name)

    def sequence_number(self) -> int:
        """
        Returns
        -------
        sequence_number of the resource : int
            (the sequence number at initialization)

            normally this will be the integer value of a serialized name.

            Non serialized names (without a dot or a comma at the end)
            will return 1)
        """
        return getattr(self, "_sequence_number", 1)


class _PeriodComponent(Component):
    def setup(self, pm):
        self.pm = pm

    def process(self):
        for iperiod, duration in itertools.cycle(enumerate(self.pm.periods)):
            self.pm.perperiod[self.pm.iperiod].monitor(False)
            self.pm.iperiod = iperiod
            if self.pm.m._level:
                self.pm.perperiod[self.pm.iperiod].tally(self.pm.m())
            self.pm.perperiod[self.pm.iperiod].monitor(True)
            yield self.hold(duration)


class PeriodMonitor:
    """
    defines a number of period monitors for a given monitor.

    Parameters
    ----------
    parent_monitor : Monitor
        parent_monitor to be divided into several period monitors for given time periods.

    periods : list or tuple of floats
        specifies the length of the period intervals.

        default: 24 * [1], meaning periods 0-1, 1-2, ..., 23-24

        the periods do not have to be all the same.

    period_monitor_names : list or tuple of string
        specifies the names of the period monitors.
        It is required that the length of period equals the length of period_monitor_names.
        By default the names are composed of the name of the parent monitor

    Note
    ----
    The period monitors can be accessed by indexing the instance of PeriodMonitor.
    """

    @staticmethod
    def new_tally(self, x, weight=1):
        for m in self.period_monitors:
            m.perperiod[m.iperiod].tally(x, weight)
        self.org_tally(x, weight)

    @staticmethod
    def new_reset(self, monitor=None, stats_only=None):
        for m in self.period_monitors:
            for iperiod in range(len(m.periods)):
                m.perperiod[iperiod].reset(stats_only=stats_only)
                # the individual monitors do not follow the monitor flag

        self.org_reset(monitor=monitor, stats_only=stats_only)

    def __getitem__(self, i):
        return self.perperiod[i]

    def remove(self):
        """
        removes the period monitor
        """
        self.pc.cancel()
        del self.periods
        self.m.period_monitors.remove(self)

    def __init__(self, parent_monitor: "Monitor", periods: Iterable = None, period_monitor_names: Iterable = None, env: "Environment" = None):
        self.pc = _PeriodComponent(pm=self, skip_standby=True, suppress_trace=True)
        self.env = _set_env(env)

        if periods is None:
            periods = 24 * [1]
        self.periods = periods
        cum = 0
        if period_monitor_names is None:
            period_monitor_names = []
            for duration in periods:
                period_monitor_names.append(parent_monitor.name() + ".period [" + str(cum) + " - " + str(cum + duration) + "]")
                cum += duration

        self.m = parent_monitor
        if not hasattr(self, "period_monitors"):
            self.m.period_monitors = []
            self.m.org_tally = self.m.tally
            self.m.tally = types.MethodType(self.new_tally, self.m)
            self.m.org_reset = self.m.reset
            self.m.reset = types.MethodType(self.new_reset, self.m)
            self.m.period_monitors.append(self)

        self.iperiod = 0
        if self.m._level:
            self.perperiod = [Monitor(name=period_monitor_name, level=True, monitor=False, env=self.env) for period_monitor_name in period_monitor_names]
        else:
            self.perperiod = [Monitor(name=period_monitor_name, monitor=False, env=self.env) for period_monitor_name in period_monitor_names]


class AudioClip:
    @staticmethod
    def send(command):
        buffer = ctypes.c_buffer(255)
        errorcode = ctypes.windll.winmm.mciSendStringA(str(command).encode(), buffer, 254, 0)
        if errorcode:
            return errorcode, AudioClip.get_error(errorcode)
        else:
            return errorcode, buffer.value

    @staticmethod
    def get_error(error):
        error = int(error)
        buffer = ctypes.c_buffer(255)
        ctypes.windll.winmm.mciGetErrorStringA(error, buffer, 254)
        return buffer.value

    @staticmethod
    def directsend(*args):
        command = " ".join(str(arg) for arg in args)
        (err, buf) = AudioClip.send(command)
        if err != 0:
            print("Error " + str(err) + " for" + command + " : " + str(buf))
        return (err, buf)

    seq = 1

    def __init__(self, filename):
        filename = filename.replace("/", "\\")
        if not os.path.isfile(filename):
            raise FileNotFoundError(filename)

        if not Windows:
            self.duration = 0
            self._alias = 0  # signal to dummy all methods`
            return  # on Unix and MacOS this is just dummy

        self._alias = str(AudioClip.seq)
        AudioClip.seq += 1

        AudioClip.directsend("open", '"' + filename + '"', "alias", self._alias)
        AudioClip.directsend("set", self._alias, "time format milliseconds")

        err, buf = AudioClip.directsend("status", self._alias, "length")
        self.duration = int(buf) / 1000

    def volume(self, level):
        """Sets the volume between 0 and 100."""
        if self._alias:
            AudioClip.directsend("setaudio", self._alias, "volume to ", level * 10)

    def play(self, start=None, end=None):
        if self._alias:
            start_ms = (0 if start is None else min(start, self.duration)) * 1000
            end_ms = (self.duration if end is None else min(end, self.duration)) * 1000
            err, buf = AudioClip.directsend("play", self._alias, "from", int(start_ms), "to", int(end_ms))

    def isplaying(self):
        return self._mode() == "playing"

    def _mode(self):
        if self._alias:
            err, buf = AudioClip.directsend("status", self._alias, "mode")
            return buf
        return "?"

    def pause(self):
        if self._alias:
            AudioClip.directsend("pause", self._alias)

    def unpause(self):
        if self._alias:
            AudioClip.directsend("resume", self._alias)

    def ispaused(self):
        return self._mode() == "paused"

    def stop(self):
        if self._alias:
            AudioClip.directsend("stop", self._alias)
            AudioClip.directsend("seek", self._alias, "to start")

    # TODO: this closes the file even if we're still playing.
    # no good.  detect isplaying(), and don't die till then!


#    def __del__(self):
#        AudioClip.directsend(f"close {self._alias}")


def audio_duration(filename: str) -> float:
    """
    duration of a audio file (usually mp3)

    Parameters
    ----------
    filename : str
        must be a valid audio file (usually mp3)

    Returns
    -------
    duration in seconds : float

    Note
    ----
    Only supported on Windows and Pythonista. On other platform returns 0
    """
    if Pythonista:
        import sound  # type: ignore

        return sound.Player(filename).duration
    audioclip = AudioClip(filename)
    return audioclip.duration


class AudioSegment:
    def __init__(self, start, t0, filename, duration):
        self.start = start
        self.t0 = t0
        self.filename = filename
        self.duration = duration


class _APNG:
    # The  _APNG class is derived from (more or less an excerpt) from the py_APNG module
    class Chunk(collections.namedtuple("Chunk", ["type", "data"])):
        pass

    class PNG:
        def __init__(self):
            self.hdr = None
            self.end = None
            self.width = None
            self.height = None
            self.chunks = []

        def init(self):
            for type_, data in self.chunks:
                if type_ == "IHDR":
                    self.hdr = data
                elif type_ == "IEND":
                    self.end = data

            if self.hdr:
                # grab w, h info
                self.width, self.height = struct.unpack("!II", self.hdr[8:16])

        @staticmethod
        def parse_chunks(b):
            i = 8
            while i < len(b):
                (data_len,) = struct.unpack("!I", b[i : i + 4])
                type_ = b[i + 4 : i + 8].decode("latin-1")
                yield _APNG.Chunk(type_, b[i : i + data_len + 12])
                i += data_len + 12

        @classmethod
        def from_bytes(cls, b):
            im = cls()
            im.chunks = list(cls.parse_chunks(b))
            im.init()
            return im

    class FrameControl:
        def __init__(self, width=None, height=None, x_offset=0, y_offset=0, delay=100, delay_den=1000, depose_op=1, blend_op=0):
            self.width = width
            self.height = height
            self.x_offset = x_offset
            self.y_offset = y_offset
            self.delay = delay
            self.delay_den = delay_den
            self.depose_op = depose_op
            self.blend_op = blend_op

        def to_bytes(self):
            return struct.pack("!IIIIHHbb", self.width, self.height, self.x_offset, self.y_offset, self.delay, self.delay_den, self.depose_op, self.blend_op)

    def __init__(self, num_plays=0):
        self.frames = []
        self.num_plays = num_plays

    @staticmethod
    def make_chunk(chunk_type, chunk_data):
        out = struct.pack("!I", len(chunk_data))
        chunk_data = chunk_type.encode("latin-1") + chunk_data
        out += chunk_data + struct.pack("!I", binascii.crc32(chunk_data) & 0xFFFFFFFF)
        return out

    def append(self, png, **options):
        if not isinstance(png, _APNG.PNG):
            raise TypeError(f"Expected an instance of `PNG` but got `{png}`")
        control = _APNG.FrameControl(**options)
        if control.width is None:
            control.width = png.width
        if control.height is None:
            control.height = png.height
        self.frames.append((png, control))

    def to_bytes(self):
        CHUNK_BEFORE_IDAT = {"cHRM", "gAMA", "iCCP", "sBIT", "sRGB", "bKGD", "hIST", "tRNS", "pHYs", "sPLT", "tIME", "PLTE"}
        PNG_SIGN = b"\x89\x50\x4e\x47\x0d\x0a\x1a\x0a"
        out = [PNG_SIGN]
        other_chunks = []
        seq = 0
        png, control = self.frames[0]
        out.append(png.hdr)
        out.append(self.make_chunk("acTL", struct.pack("!II", len(self.frames), self.num_plays)))
        if control:
            out.append(self.make_chunk("fcTL", struct.pack("!I", seq) + control.to_bytes()))
            seq += 1
        idat_chunks = []
        for type_, data in png.chunks:
            if type_ in ("IHDR", "IEND"):
                continue
            if type_ == "IDAT":
                # put at last
                idat_chunks.append(data)
                continue
            out.append(data)
        out.extend(idat_chunks)
        for png, control in self.frames[1:]:
            out.append(self.make_chunk("fcTL", struct.pack("!I", seq) + control.to_bytes()))
            seq += 1
            for type_, data in png.chunks:
                if type_ in ("IHDR", "IEND") or type_ in CHUNK_BEFORE_IDAT:
                    continue

                if type_ == "IDAT":
                    out.append(self.make_chunk("fdAT", struct.pack("!I", seq) + data[8:-4]))
                    seq += 1
                else:
                    other_chunks.append(data)

        out.extend(other_chunks)
        out.append(png.end)

        return b"".join(out)

    def save(self, file):
        b = self.to_bytes()
        if hasattr(file, "write_bytes"):
            file.write_bytes(b)
        elif hasattr(file, "write"):
            file.write(b)
        else:
            with open(file, "wb") as f:
                f.write(b)


def colornames() -> Dict:
    """
    available colornames

    Returns
    -------
    dict with name of color as key, #rrggbb or #rrggbbaa as value : dict
    """
    if not hasattr(colornames, "cached"):
        colornames.cached = pickle.loads(
            b"(dp0\nVfuchsia\np1\nV#FF00FF\np2\nsV\np3\nV#00000000\np4\nsVtransparent\np5\ng4\nsVpalevioletred\np6\nV#DB7093\np7\nsVskyblue\np8\nV#87CEEB\np9\nsVpaleturquoise\np10\nV#AFEEEE\np11\nsVcadetblue\np12\nV#5F9EA0\np13\nsVorangered\np14\nV#FF4500\np15\nsVsteelblue\np16\nV#4682B4\np17\nsVdimgray\np18\nV#696969\np19\nsVdarkseagreen\np20\nV#8FBC8F\np21\nsV60%gray\np22\nV#999999\np23\nsVroyalblue\np24\nV#4169E1\np25\nsVmediumblue\np26\nV#0000CD\np27\nsVgoldenrod\np28\nV#DAA520\np29\nsVmediumvioletred\np30\nV#C71585\np31\nsVblueviolet\np32\nV#8A2BE2\np33\nsVgainsboro\np34\nV#DCDCDC\np35\nsVdarkred\np36\nV#8B0000\np37\nsVrosybrown\np38\nV#BC8F8F\np39\nsVgold\np40\nV#FFD700\np41\nsVcoral\np42\nV#FF7F50\np43\nsVwhite\np44\nV#FFFFFF\np45\nsVdarkcyan\np46\nV#008B8B\np47\nsVblack\np48\nV#000000\np49\nsVorchid\np50\nV#DA70D6\np51\nsVmediumturquoise\np52\nV#48D1CC\np53\nsVlightgreen\np54\nV#90EE90\np55\nsVlime\np56\nV#00FF00\np57\nsVpapayawhip\np58\nV#FFEFD5\np59\nsVchocolate\np60\nV#D2691E\np61\nsV40%gray\np62\nV#666666\np63\nsVoldlace\np64\nV#FDF5E6\np65\nsVdarkblue\np66\nV#00008B\np67\nsVsilver\np68\nV#C0C0C0\np69\nsVaquamarine\np70\nV#7FFFD4\np71\nsVlightcoral\np72\nV#F08080\np73\nsVcyan\np74\nV#00FFFF\np75\nsVdodgerblue\np76\nV#1E90FF\np77\nsV10%gray\np78\nV#191919\np79\nsVmidnightblue\np80\nV#191970\np81\nsVgreen\np82\nV#008000\np83\nsVlightsalmon\np84\nV#FFA07A\np85\nsVazure\np86\nV#F0FFFF\np87\nsVred\np88\nV#FF0000\np89\nsVlightpink\np90\nV#FFB6C1\np91\nsVwhitesmoke\np92\nV#F5F5F5\np93\nsVyellow\np94\nV#FFFF00\np95\nsVlawngreen\np96\nV#7CFC00\np97\nsVmagenta\np98\ng2\nsVlightsteelblue\np99\nV#B0C4DE\np100\nsVolivedrab\np101\nV#6B8E23\np102\nsVlightslategray\np103\nV#778899\np104\nsVslategray\np105\nV#708090\np106\nsVlightblue\np107\nV#ADD8E6\np108\nsVmoccasin\np109\nV#FFE4B5\np110\nsVmediumspringgreen\np111\nV#00FA9A\np112\nsVlightgray\np113\nV#D3D3D3\np114\nsVseashell\np115\nV#FFF5EE\np116\nsVdarkkhaki\np117\nV#BDB76B\np118\nsVslateblue\np119\nV#6A5ACD\np120\nsVaqua\np121\ng75\nsVpalegoldenrod\np122\nV#EEE8AA\np123\nsVdeeppink\np124\nV#FF1493\np125\nsVdarkgreen\np126\nV#006400\np127\nsVblanchedalmond\np128\nV#FFEBCD\np129\nsVturquoise\np130\nV#40E0D0\np131\nsVnavy\np132\nV#000080\np133\nsVtomato\np134\nV#FF6347\np135\nsVyellowgreen\np136\nV#9ACD32\np137\nsVpeachpuff\np138\nV#FFDAB9\np139\nsV30%gray\np140\nV#464646\np141\nsVpink\np142\nV#FFC0CB\np143\nsVpalegreen\np144\nV#98FB98\np145\nsVlightskyblue\np146\nV#87CEFA\np147\nsVchartreuse\np148\nV#7FFF00\np149\nsVmediumorchid\np150\nV#BA55D3\np151\nsVolive\np152\nV#808000\np153\nsVdarkorange\np154\nV#FF8C00\np155\nsVbeige\np156\nV#F5F5DC\np157\nsVforestgreen\np158\nV#228B22\np159\nsVmediumpurple\np160\nV#9370DB\np161\nsVmintcream\np162\nV#F5FFFA\np163\nsVhotpink\np164\nV#FF69B4\np165\nsVdarkgoldenrod\np166\nV#B8860B\np167\nsVpowderblue\np168\nV#B0E0E6\np169\nsVhoneydew\np170\nV#F0FFF0\np171\nsVsalmon\np172\nV#FA8072\np173\nsVsnow\np174\nV#FFFAFA\np175\nsVmistyrose\np176\nV#FFE4E1\np177\nsVkhaki\np178\nV#F0E68C\np179\nsVmediumaquamarine\np180\nV#66CDAA\np181\nsVdarksalmon\np182\nV#E9967A\np183\nsValiceblue\np184\nV#F0F8FF\np185\nsVdarkturquoise\np186\nV#00CED1\np187\nsVlightyellow\np188\nV#FFFFE0\np189\nsVwheat\np190\nV#F5DEB3\np191\nsVlightseagreen\np192\nV#20B2AA\np193\nsVlightcyan\np194\nV#E0FFFF\np195\nsVantiquewhite\np196\nV#FAEBD7\np197\nsVsaddlebrown\np198\nV#8B4513\np199\nsVmediumseagreen\np200\nV#3CB371\np201\nsV70%gray\np202\nV#B2B2B2\np203\nsVsienna\np204\nV#A0522D\np205\nsVcornflowerblue\np206\nV#6495ED\np207\nsVseagreen\np208\nV#2E8B57\np209\nsVfloralwhite\np210\nV#FFFAF0\np211\nsVivory\np212\nV#FFFFF0\np213\nsVcornsilk\np214\nV#FFF8DC\np215\nsVindianred\np216\nV#CD5C5C\np217\nsVplum\np218\nV#DDA0DD\np219\nsV90%gray\np220\nV#E6E6E6\np221\nsVgreenyellow\np222\nV#ADFF2F\np223\nsVteal\np224\nV#008080\np225\nsVbrown\np226\nV#A52A2A\np227\nsVdarkslategray\np228\nV#2F4F4F\np229\nsVpurple\np230\nV#800080\np231\nsVviolet\np232\nV#EE82EE\np233\nsVdeepskyblue\np234\nV#00BFFF\np235\nsVghostwhite\np236\nV#F8F8FF\np237\nsVburlywood\np238\nV#DEB887\np239\nsVblue\np240\nV#0000FF\np241\nsVcrimson\np242\nV#DC143C\np243\nsVindigo\np244\nV#4B0082\np245\nsV20%gray\np246\nV#333333\np247\nsVdarkmagenta\np248\nV#8B008B\np249\nsV80%gray\np250\nV#CCCCCC\np251\nsVlightgoldenrodyellow\np252\nV#FAFAD2\np253\nsVtan\np254\nV#D2B48C\np255\nsVlimegreen\np256\nV#32CD32\np257\nsVlemonchiffon\np258\nV#FFFACD\np259\nsVbisque\np260\nV#FFE4C4\np261\nsVfirebrick\np262\nV#B22222\np263\nsVnavajowhite\np264\nV#FFDEAD\np265\nsVnone\np266\ng4\nsVmaroon\np267\nV#800000\np268\nsV50%gray\np269\nV#7F7F7F\np270\nsVdarkgray\np271\nV#A9A9A9\np272\nsVorange\np273\nV#FFA500\np274\nsVlavenderblush\np275\nV#FFF0F5\np276\nsVdarkorchid\np277\nV#9932CC\np278\nsVlavender\np279\nV#E6E6FA\np280\nsVspringgreen\np281\nV#00FF7F\np282\nsVthistle\np283\nV#D8BFD8\np284\nsVlinen\np285\nV#FAF0E6\np286\nsVdarkolivegreen\np287\nV#556B2F\np288\nsVdarkslateblue\np289\nV#483D8B\np290\nsVgray\np291\nV#808080\np292\nsVdarkviolet\np293\nV#9400D3\np294\nsVperu\np295\nV#CD853F\np296\nsVsandybrown\np297\nV#F4A460\np298\nsVmediumslateblue\np299\nV#7B68EE\np300\nsVlightgrey\np301\ng114\ns."
        )
    return colornames.cached


def salabim_logo_200():
    if not hasattr(salabim_logo_200, "cached"):
        salabim_logo_200.cached = Image.open(
            io.BytesIO(
                base64.b64decode(
                    " "  # NOQA
                    "".encode("ascii")
                )
            )
        ).convert("RGBA")
    return salabim_logo_200.cached


def hex_to_rgb(v):
    if v == "":
        return (0, 0, 0, 0)
    if v[0] == "#":
        v = v[1:]
    if len(v) == 6:
        return int(v[:2], 16), int(v[2:4], 16), int(v[4:6], 16)
    if len(v) == 8:
        return int(v[:2], 16), int(v[2:4], 16), int(v[4:6], 16), int(v[6:8], 16)
    raise ValueError("Incorrect value" + str(v))


def spec_to_image_width(spec):
    image_container = ImageContainer(spec)
    return image_container.images[0].size[0]


def _time_unit_lookup(descr):
    lookup = {
        "years": 1 / (86400 * 365),
        "weeks": 1 / (86400 * 7),
        "days": 1 / 86400,
        "hours": 1 / 3600,
        "minutes": 1 / 60,
        "seconds": 1,
        "milliseconds": 1e3,
        "microseconds": 1e6,
        "n/a": None,
    }

    if descr not in lookup:
        raise ValueError("time_unit " + descr + " not supported")
    return lookup[descr]


def _time_unit_factor(time_unit, env):
    if env is None:
        env = g.default_env
    if time_unit is None:
        return 1
    if (env is None) or (env._time_unit is None):
        raise AttributeError("time unit not set.")

    return env._time_unit / _time_unit_lookup(time_unit)


def _i(p, v0, v1):
    if v0 == v1:
        return v0  # avoid rounding problems
    if (v0 is None) or (v1 is None):
        return None
    return (1 - p) * v0 + p * v1


def interpolate(t: float, t0: float, t1: float, v0: Union[float, Iterable], v1: Union[float, Iterable]) -> Union[float, Tuple]:
    """
    does linear interpolation

    Parameters
    ----------
    t : float
        value to be interpolated from

    t0: float
        f(t0)=v0

    t1: float
        f(t1)=v1

    v0: float, list or tuple
        f(t0)=v0

    v1: float, list or tuple
        f(t1)=v1

        if list or tuple, len(v0) should equal len(v1)

    Returns
    -------
    linear interpolation between v0 and v1 based on t between t0 and t1 : float or tuple

    Note
    ----
    Note that no extrapolation is done, so if t<t0 ==> v0  and t>t1 ==> v1

    This function is heavily used during animation.
    """
    if v0 == v1:
        return v0

    if t0 > t1:
        (t0, t1) = (t1, t0)
        (v0, v1) = (v1, v0)
    if t1 == inf:
        return v0
    if t0 == t1:
        return v1
    if t <= t0:
        return v0
    if t >= t1:
        return v1
    p = (0.0 + t - t0) / (t1 - t0)
    if isinstance(v0, (list, tuple)):
        return tuple((_i(p, x0, x1) for x0, x1 in zip(v0, v1)))
    else:
        return _i(p, v0, v1)


def searchsorted(a: Iterable, v: float, side: str = "left") -> int:
    """
    search sorted

    Parameters
    ----------
    a : iterable
        iterable to be searched in, must be non descending

    v : float
        value to be searched for

    side : string
        If 'left' (default) the index of the first suitable location found is given.
        If 'right', return the last such index.
        If there is no suitable index, return either 0 or N (where N is the length of a).

    Returns
    -------
    Index where v should be inserted to maintain order : int

    Note
    ----
    If numpy is installed, uses numpy.searchstarted
    """

    if has_numpy():
        return numpy.searchsorted(a, v, side)

    if side == "left":
        return bisect.bisect_left(a, v)
    if side == "right":
        return bisect.bisect_right(a, v)
    raise ValueError(f"{repr(side)} is an invalid value for the keyword 'side'")


def arange(start: float, stop: float, step: float = 1) -> Iterable:
    """
    arange (like numpy)

    Parameters
    ----------
    start : float
        start value

    stop: : float
        stop value

    step : float
        default: 1

    Returns
    -------
    Iterable

    Note
    ----
    If numpy is installed, uses numpy.arange
    """
    if has_numpy():
        return numpy.arange(start, stop, step)

    result = []
    value = start
    while True:
        if (step > 0 and value >= stop) or (step < 0 and value <= stop):
            return result
        result.append(value)
        value += step


def linspace(start: float, stop: float, num: int = 50, endpoint: bool = True) -> Iterable:
    """
    like numpy.linspace, but returns a list

    Parameters
    ----------
    start : float
        start of the space

    stop : float
        stop of the space

    num : int
        number of points in the space

        default: 50

    endpoint : bool
        if True (default), stop is last point in the space

        if False, space ends before stop
    """
    if num == 0:
        return []
    if num == 1:
        return [start]
    if endpoint:
        step = (stop - start) / (num - 1)
    else:
        step = (stop - start) / num

    return [start + step * i for i in range(num)]


def interp(x: float, xp: Iterable, fp: Iterable, left: float = None, right: float = None) -> Any:
    """
    linear interpolatation

    Parameters
    ----------
    x : float
        target x-value

    xp : list of float, tuples or lists
        values on the x-axis

    fp : list of float, tuples of lists
        values on the y-axis

        should be same length as  p

    Returns
    -------
    interpolated value : float, tuple or list

    Notes
    -----
    If x < xp[0], fp[0] will be returned

    If x > xp[-1], fp[-1] will be returned


    This function is similar to the numpy interp function.
    """
    if len(xp) != len(fp):
        raise ValueError("xp and yp are not the same length")
    if len(xp) == 0:
        raise ValueError("list of sample points is empty")

    if x < xp[0]:
        return fp[0] if left is None else left
    if x > xp[-1]:
        return fp[-1] if right is None else right
    if len(xp) == 1:
        return fp[0]

    i = bisect.bisect_right(xp, x)

    if i >= len(xp):
        return fp[-1]

    if isinstance(fp[0], (tuple, list)):
        return type(fp[0])(el_i_min_1 + (el_i - el_i_min_1) * (x - xp[i - 1]) / (xp[i] - xp[i - 1]) for el_i_min_1, el_i in zip(fp[i - 1], fp[i]))

    return fp[i - 1] + (fp[i] - fp[i - 1]) * (x - xp[i - 1]) / (xp[i] - xp[i - 1])


def _set_name(name, _nameserialize, object):
    if name is None or name == "":
        name = object_to_str(object).lower() + "."
    elif name in (".", ","):
        name = f"{object_to_str(object).lower()}{name}"  # adds "." or ","

    if name.endswith((".", ",")):
        if name in _nameserialize:
            _nameserialize[name] = sequence_number = _nameserialize[name] + 1
        else:
            _nameserialize[name] = sequence_number = 1 if name.endswith(",") else 0

        object._name = f"{name[:-1]}.{sequence_number}"
        object._base_name = name
        object._sequence_number = sequence_number
    else:
        object._name = name


@functools.lru_cache()
def _screen_dimensions():
    if PythonInExcel or AnacondaCode:
        return 1024, 768
    if Pythonista:
        screen_width, screen_height = ui.get_screen_size()
    else:
        try:
            import tkinter
        except ImportError:
            raise ImportError("tkinter is required for to get screen dimensions")
        root = tkinter.Tk()
        screen_width = root.winfo_screenwidth()
        screen_height = root.winfo_screenheight()
        root.update()
        root.destroy()
    return screen_width, screen_height


def screen_width() -> int:
    """
    returns
    -------
    width of the screen (in pixels) : int
    """
    return _screen_dimensions()[0]


def screen_height() -> int:
    """
    returns
    -------
    height of the screen (in pixels) : int
    """
    return _screen_dimensions()[1]


def pad(txt, n):
    if n <= 0:
        return ""
    else:
        return txt.ljust(n)[:n]


def rpad(txt, n):
    return txt.rjust(n)[:n]


def un_na(s):
    if s is None or "n/a" in s:
        return ""
    else:
        return s


def fn(x, length, d):
    if math.isnan(x):
        return ("{:" + str(length) + "s}").format("")
    if x >= 10 ** (length - d - 1):
        return ("{:" + str(length) + "." + str(length - d - 3) + "e}").format(x)
    if x == int(x):
        return ("{:" + str(length - d - 1) + "d}{:" + str(d + 1) + "s}").format(int(x), "")
    return ("{:" + str(length) + "." + str(d) + "f}").format(x)


def _checkrandomstream(randomstream):
    if not isinstance(randomstream, random.Random):
        raise TypeError("Type randomstream or random.Random expected, got " + str(type(randomstream)))


def _checkismonitor(monitor):
    if not isinstance(monitor, Monitor):
        raise TypeError("Type Monitor expected, got " + str(type(monitor)))


def _checkisqueue(queue):
    if not isinstance(queue, Queue):
        raise TypeError("Type Queue expected, got " + str(type(queue)))


def type_to_typecode_off(type):
    lookup = {
        "bool": ("B", 255),
        "int8": ("b", -128),
        "uint8": ("B", 255),
        "int16": ("h", -32768),
        "uint16": ("H", 65535),
        "int32": ("i", -2147483648),
        "uint32": ("I", 4294967295),
        "int64": ("l", -9223372036854775808),
        "uint64": ("L", 18446744073709551615),
        "float": ("d", -inf),
        "double": ("d", -inf),
        "any": ("", -inf),
    }
    return lookup[type]


def do_force_numeric(arg):
    result = []
    for v in arg:
        if isinstance(v, numbers.Number):
            result.append(v)
        else:
            try:
                if int(v) == float(v):
                    result.append(int(v))
                else:
                    result.append(float(v))
            except (ValueError, TypeError):
                result.append(0)

    return result


def deep_flatten(arg):
    if hasattr(arg, "__iter__") and not isinstance(arg, str):
        for x in arg:
            #  the two following lines are equivalent to 'yield from deep_flatten(x)' (not supported in Python 2.7)
            for xx in deep_flatten(x):
                yield xx
    else:
        yield arg


def merge_blanks(*arg) -> str:
    """
    merges all non blank elements of l, separated by a blank

    Parameters
    ----------
    *arg : elements to be merged : str

    Returns
    -------
    string with merged elements of arg : str
    """
    return " ".join(x for x in arg if x)


def normalize(s):
    return "".join(c for c in s.lower() if c.isalpha() or c.isdigit())


def _urgenttxt(urgent):
    if urgent:
        return "!"
    else:
        return " "


def _prioritytxt(priority):
    return ""


def object_to_str(object, quoted=False):
    add = '"' if quoted else ""
    return add + type(object).__name__ + add


def _get_caller_frame():
    stack = inspect.stack()
    filename0 = inspect.getframeinfo(stack[0][0]).filename
    for i in range(len(inspect.stack())):
        frame = stack[i][0]
        if not inspect.getframeinfo(frame).filename in (filename0, "<string>"):
            break
    return frame


def return_or_print(result, as_str, file) -> str:
    result = "\n".join(result)
    if as_str:
        return result
    else:
        if file is None:
            print(result)
        else:
            print(result, file=file)


def _call(c, t, self):
    """
    special function to support scalars, methods (with one parameter) and function with zero, one or two parameters
    """
    if callable(c):
        if inspect.isfunction(c):
            nargs = c.__code__.co_argcount
            if nargs == 0:
                return c()
            if nargs == 1:
                return c(t)
            return c(self, t)
        if inspect.ismethod(c):
            return c(t)
    return c


def de_none(lst):
    if lst is None:
        return None
    lst = list(lst)  # it is necessary to convert to list, because input maybe a tuple or even a deque
    result = lst[:2]
    for item in lst[2:]:
        if item is None:
            result.append(result[-2])
        else:
            result.append(item)
    return result


def statuses() -> Tuple:
    """
    tuple of all statuses a component can be in, in alphabetical order.
    """

    return tuple("current data interrupted passive requesting scheduled standby waiting".split(" "))


current = "current"
data = "data"
interrupted = "interrupted"
passive = "passive"
requesting = "requesting"
scheduled = "scheduled"
standby = "standby"
waiting = "waiting"


def random_seed(seed: Hashable = None, randomstream: Any = None, set_numpy_random_seed: bool = True):
    """
    Reseeds a randomstream

    Parameters
    ----------
    seed : hashable object, usually int
        the seed for random, equivalent to random.seed()

        if "*", a purely random value (based on the current time) will be used
        (not reproducable)

        if the null string, no action on random is taken

        if None (the default), 1234567 will be used.

    set_numpy_random_seed : bool
        if True (default), numpy.random.seed() will be called with the given seed.

        This is particularly useful when using External distributions.

        If numpy is not installed, this parameter is ignored

        if False, numpy.random.seed is not called.

    randomstream: randomstream
        randomstream to be used

        if omitted, random will be used

    """
    if randomstream is None:
        randomstream = random
    if seed != "":
        if seed is None:
            seed = 1234567
        elif seed == "*":
            seed = None
        random.seed(seed)
        if set_numpy_random_seed and has_numpy():
            numpy.random.seed(seed)


_random_seed = random_seed  # used by Environment.__init__


def resize_with_pad(im, target_width, target_height):
    """
    Resize PIL image keeping ratio and using black background.
    """
    if im.height == target_height and im.width == target_width:
        return im
    target_ratio = target_height / target_width
    im_ratio = im.height / im.width
    if target_ratio > im_ratio:
        # It must be fixed by width
        resize_width = target_width
        resize_height = round(resize_width * im_ratio)
    else:
        # Fixed by height
        resize_height = target_height
        resize_width = round(resize_height / im_ratio)

    image_resize = im.resize((resize_width, resize_height), Image.LANCZOS)
    background = Image.new("RGBA", (target_width, target_height), (0, 0, 0, 255))
    offset = (round((target_width - resize_width) / 2), round((target_height - resize_height) / 2))
    background.paste(image_resize, offset)
    return background.convert("RGB")


class _AnimateIntro(Animate3dBase):
    def __init__(self, env):
        self.env = env
        super().__init__(env=env)

    def setup(self):
        self.layer = -math.inf
        self.field_of_view_y = 45
        self.z_near = 0.1
        self.z_far = 100000
        self.x_eye = 4
        self.y_eye = 4
        self.z_eye = 4
        self.x_center = 0
        self.y_center = 0
        self.z_center = 0
        self.model_lights_pname = None
        self.model_lights_param = (0.42, 0.42, 0.42, 1)
        self.lights_light = None
        self.lights_pname = None
        self.lights_param = (-1, -1, 1, 0)
        self.lag = 1

        self.register_dynamic_attributes("field_of_view_y z_near z_far x_eye y_eye z_eye x_center y_center z_center")
        self.register_dynamic_attributes("model_lights_pname model_lights_param lights_light lights_pname lights_param")

    def draw(self, t):
        x_eye = self.x_eye(t)
        y_eye = self.y_eye(t)
        z_eye = self.z_eye(t)
        x_center = self.x_center(t)
        y_center = self.y_center(t)
        z_center = self.z_center(t)
        x_up = 0
        y_up = 0

        dx = x_eye - x_center
        dy = y_eye - y_center
        dz = z_eye - z_center
        dxy = math.hypot(dx, dy)
        if dy > 0:
            dxy = -dxy
        alpha = math.degrees(math.atan2(dxy, dz))
        if alpha < 0:
            z_up = +1
        else:
            z_up = 1

        if self.model_lights_pname(t) is None:
            self.model_lights_pname = gl.GL_LIGHT_MODEL_AMBIENT  # in principal only at first call
        if self.lights_light(t) is None:
            self.lights_light = gl.GL_LIGHT0  # in principal only at first call
        if self.lights_pname(t) is None:
            self.lights_pname = gl.GL_POSITION  # in principal only at first call

        background_color = list(self.env.colorspec_to_gl_color(self.env._background3d_color)) + [0.0]
        gl.glClearColor(*background_color)

        gl.glClear(gl.GL_COLOR_BUFFER_BIT | gl.GL_DEPTH_BUFFER_BIT)

        gl.glMatrixMode(gl.GL_PROJECTION)

        gl.glLoadIdentity()
        glu.gluPerspective(self.field_of_view_y(t), glut.glutGet(glut.GLUT_WINDOW_WIDTH) / glut.glutGet(glut.GLUT_WINDOW_HEIGHT), self.z_near(t), self.z_far(t))

        glu.gluLookAt(x_eye, y_eye, z_eye, x_center, y_center, z_center, x_up, y_up, z_up)
        gl.glEnable(gl.GL_LIGHTING)
        gl.glLightModelfv(self.model_lights_pname(t), self.model_lights_param(t))
        gl.glLightfv(self.lights_light(t), self.lights_pname(t), self.lights_param(t))
        gl.glEnable(gl.GL_LIGHT0)

        gl.glMatrixMode(gl.GL_MODELVIEW)

        gl.glLoadIdentity()


class _AnimateExtro(Animate3dBase):
    def __init__(self, env):
        self.env = env
        super().__init__(env=env)

    def setup(self):
        self.layer = math.inf

    def draw(self, t):
        if self.env.an_objects_over3d:
            for ao in sorted(self.env.an_objects_over3d, key=lambda obj: (-obj.layer(t), obj.sequence)):
                ao.make_pil_image(t - self.env._offset)

                if ao._image_visible:
                    ao.x1 = ao._image_x
                    ao.x2 = ao._image_x + ao._image.size[0]
                    ao.y1 = ao._image_y
                    ao.y2 = ao._image_y + ao._image.size[1]

            overlap = False
            ao2_set = self.env.an_objects_over3d.copy()
            for ao1 in self.env.an_objects_over3d:
                ao2_set.discard(ao1)
                if ao1._image_visible:
                    for ao2 in ao2_set:
                        if ao2._image_visible and ao1 != ao2:
                            x_match = ao1.x1 <= ao2.x2 and ao2.x1 <= ao1.x2
                            y_match = ao1.y1 <= ao2.y2 and ao2.y1 <= ao1.y2
                            if x_match and y_match:
                                overlap = True
                                break
                    if overlap:
                        break

            gl.glEnable(gl.GL_TEXTURE_2D)
            gl.glEnable(gl.GL_BLEND)
            gl.glBlendFunc(gl.GL_SRC_ALPHA, gl.GL_ONE_MINUS_SRC_ALPHA)

            gl.glMatrixMode(gl.GL_PROJECTION)
            gl.glLoadIdentity()

            gl.glOrtho(0, self.env._width3d, 0, self.env._height3d, -1, 1)
            if overlap:
                overlay_image = Image.new("RGBA", (self.env._width3d, self.env._height3d), (0, 0, 0, 0))
                for ao in sorted(self.env.an_objects_over3d, key=lambda obj: (-obj.layer(t), obj.sequence)):
                    if ao._image_visible:
                        overlay_image.paste(ao._image, (int(ao._image_x), int(self.env._height3d - ao._image_y - ao._image.size[1])), ao._image)
                    imdata = overlay_image.tobytes("raw", "RGBA", 0, -1)

                w = overlay_image.size[0]
                h = overlay_image.size[1]

                gl.glRasterPos(0, 0)
                gl.glDrawPixels(w, h, gl.GL_RGBA, gl.GL_UNSIGNED_BYTE, imdata)

            else:
                for ao in sorted(self.env.an_objects_over3d, key=lambda obj: (-obj.layer(self.env._t), obj.sequence)):
                    if ao._image_visible:
                        imdata = ao._image.tobytes("raw", "RGBA", 0, -1)
                        w = ao._image.size[0]
                        h = ao._image.size[1]
                        gl.glRasterPos(int(ao._image_x), int(ao._image_y))
                        gl.glDrawPixels(w, h, gl.GL_RGBA, gl.GL_UNSIGNED_BYTE, imdata)

            gl.glDisable(gl.GL_BLEND)

        glut.glutSwapBuffers()
        glut.glutMainLoopEvent()


class Animate3dObj(Animate3dBase):
    """
    Creates a 3D animation object from an .obj file

    Parameters
    ----------
    filename : str or Path
        obj file to be read (default extension .obj)

        if there are .mtl or .jpg required by this file, they should be available

    x : float
        x position (default 0)

    y : float
        y position (default 0)

    z : float
        z position (default 0)

    x_angle : float
        angle along x axis (default: 0)

    y_angle : float
        angle along y axis (default: 0)

    z_angle : float
        angle along z axis (default: 0)

    x_translate : float
        translation in x direction (default: 0)

    y_translate : float
        translation in y direction (default: 0)

    z_translate : float
        translation in z direction (default: 0)

    x_scale : float
        scaling in x direction (default: 1)

    y_translate : float
        translation in y direction (default: 1)

    z_translate : float
        translation in z direction (default: 1)

    show_warnings : bool
        as pywavefront does not support all obj commands, reading the file sometimes leads
        to (many) warning log messages

        with this flag, they can be turned off (the deafult)

    visible : bool
        visible

        if False, animation object is not shown, shown otherwise
        (default True)

    layer : int
        layer value

        lower layer values are displayed later in the frame (default 0)

    arg : any
        this is used when a parameter is a function with two parameters, as the first argument or
        if a parameter is a method as the instance

        default: self (instance itself)

    parent : Component
        component where this animation object belongs to (default None)

        if given, the animation object will be removed
        automatically when the parent component is no longer accessible

    env : Environment
        environment where the component is defined

        if omitted, default_env will be used

    Note
    ----
    All parameters, apart from parent, arg and env can be specified as:

    - a scalar, like 10

    - a function with zero arguments, like lambda: my_x

    - a function with one argument, being the time t, like lambda t: t + 10

    - a function with two parameters, being arg (as given) and the time, like lambda comp, t: comp.state

    - a method instance arg for time t, like self.state, actually leading to arg.state(t) to be called


    Note
    ----
    This method requires the pywavefront and pyglet module to be installed
    """

    def __init__(
        self,
        filename: Union[str, Callable],
        x: Union[float, Callable] = 0,
        y: Union[float, Callable] = 0,
        z: Union[float, Callable] = 0,
        x_angle: Union[float, Callable] = 0,
        y_angle: Union[float, Callable] = 0,
        z_angle: Union[float, Callable] = 0,
        x_translate: Union[float, Callable] = 0,
        y_translate: Union[float, Callable] = 0,
        z_translate: Union[float, Callable] = 0,
        x_scale: Union[float, Callable] = 1,
        y_scale: Union[float, Callable] = 1,
        z_scale: Union[float, Callable] = 1,
        show_warnings: Union[bool, Callable] = False,
        visible: Union[bool, Callable] = True,
        arg: Any = None,
        layer: Union[float, Callable] = 0,
        parent: "Component" = None,
        env: "Environment" = None,
        **kwargs,
    ):
        super().__init__(visible=visible, arg=arg, layer=layer, parent=parent, env=env, **kwargs)

        global pywavefront
        global visualization
        global pyglet


        self.x = x
        self.y = y
        self.z = z
        self.x_angle = x_angle
        self.y_angle = y_angle
        self.z_angle = z_angle
        self.x_translate = x_translate
        self.y_translate = y_translate
        self.z_translate = z_translate

        self.x_scale = x_scale
        self.y_scale = y_scale
        self.z_scale = z_scale
        self.filename = filename
        self.show_warnings = show_warnings

        self.register_dynamic_attributes("x y z x_angle y_angle z_angle x_translate y_translate z_translate x_scale y_scale z_scale filename show_warnings")
        self.x_offset = 0
        self.y_offset = 0
        self.z_offset = 0

        try:
            import pywavefront
        except ImportError:
            pywavefront=None
                
        try:
            import pyglet   # this is a requirement for visualization!
        except ImportError:
            pyglet=None

        from pywavefront import visualization

    def draw(self, t):
        global pywavefront
        global visualization
        global pyglet
        if pywavefront is None:
            raise ImportError("Animate3dObj requires pywavefront. Not found")
        if pyglet is None:                    
            raise ImportError("Animate3dObj requires pyglet. Not found")

        obj_filename = Path(self.filename(t))
        if not obj_filename.suffix:
            obj_filename = obj_filename.with_suffix(".obj")
        obj_filename = obj_filename.resolve()

        if obj_filename not in self.env.obj_filenames:
            save_logging_level = logging.root.level
            if not self.show_warnings(t):
                logging.basicConfig(level=logging.ERROR)

            with open(obj_filename, "r") as obj_file:
                create_materials = False
                obj_file_path = Path(obj_filename).resolve().parent
                save_cwd = os.getcwd()
                os.chdir(obj_file_path)
                for f in obj_file:
                    if f.startswith("mtllib "):
                        mtllib_filename = Path(f[7:].strip())
                        if mtllib_filename.is_file():
                            create_materials = True
                        break
            os.chdir(save_cwd)
            logging.basicConfig(level=save_logging_level)

            self.env.obj_filenames[obj_filename] = pywavefront.Wavefront(obj_filename, create_materials=create_materials)

        obj = self.env.obj_filenames[obj_filename]

        gl.glMatrixMode(gl.GL_MODELVIEW)
        gl.glPushMatrix()
        gl.glTranslate(self.x(t) + self.x_offset, self.y(t) + self.y_offset, self.z(t) + self.z_offset)
        gl.glRotate(self.z_angle(t), 0.0, 0.0, 1.0)
        gl.glRotate(self.y_angle(t), 0.0, 1.0, 0.0)
        gl.glRotate(self.x_angle(t), 1.0, 0.0, 0.0)
        gl.glTranslate(self.x_translate(t), self.y_translate(t), self.z_translate(t))
        gl.glScale(self.x_scale(t), self.y_scale(t), self.z_scale(t))
        visualization.draw(obj)
        gl.glPopMatrix()


class Animate3dRectangle(Animate3dBase):
    """
    Creates a 3D rectangle

    Parameters
    ----------
    x0 : float
        lower left x position (default 0)

    y0 : float
        lower left y position (default 0)

    x1 : float
        upper right x position (default 1)

    y1 : float
        upper right y position (default 1)

    z : float
        z position of rectangle (default 0)

    color : colorspec
        color of the rectangle (default "white")

    visible : bool
        visible

        if False, animation object is not shown, shown otherwise
        (default True)

    layer : int
         layer value

         lower layer values are displayed later in the frame (default 0)

    arg : any
        this is used when a parameter is a function with two parameters, as the first argument or
        if a parameter is a method as the instance

        default: self (instance itself)

    parent : Component
        component where this animation object belongs to (default None)

        if given, the animation object will be removed
        automatically when the parent component is no longer accessible

    env : Environment
        environment where the component is defined

        if omitted, default_env will be used

    Note
    ----
    All parameters, apart from parent, arg and env can be specified as:

    - a scalar, like 10

    - a function with zero arguments, like lambda: my_x

    - a function with one argument, being the time t, like lambda t: t + 10

    - a function with two parameters, being arg (as given) and the time, like lambda comp, t: comp.state

    - a method instance arg for time t, like self.state, actually leading to arg.state(t) to be called

    """

    def __init__(
        self,
        x0: Union[float, Callable] = 0,
        y0: Union[float, Callable] = 0,
        x1: Union[float, Callable] = 1,
        y1: Union[float, Callable] = 1,
        z: Union[float, Callable] = 0,
        color: Union[ColorType, Callable] = "white",
        visible: Union[bool, Callable] = True,
        arg: Any = None,
        layer: Union[float, Callable] = 0,
        parent: "Component" = None,
        env: "Environment" = None,
        **kwargs,
    ):
        super().__init__(visible=visible, arg=arg, layer=layer, parent=parent, env=env, **kwargs)

        self.x0 = x0
        self.y0 = y0
        self.x1 = x1
        self.y1 = y1
        self.z = z
        self.color = color
        self.register_dynamic_attributes("x0 y0 x1 y1 z color")

    def draw(self, t):
        gl_color = self.env.colorspec_to_gl_color(self.color(t))
        x0 = self.x0(t)
        y0 = self.y0(t)
        x1 = self.x1(t)
        y1 = self.y1(t)
        z = self.z(t)
        draw_rectangle3d(x0=x0, y0=y0, z=z, x1=x1, y1=y1, gl_color=gl_color)


class Animate3dLine(Animate3dBase):
    """
    Creates a 3D line

    Parameters
    ----------
    x0 : float
        x coordinate of start point (default 0)

    y0 : float
        y coordinate of start point (default 0)

    z0 : float
        z coordinate of start point (default 0)

    x1 : float
        x coordinate of end point (default 0)

    y1 : float
        y coordinate of end point (default 0)

    z1 : float
        z coordinate of end point (default 0)

    color : colorspec
        color of the line (default "white")

    visible : bool
        visible

        if False, animation object is not shown, shown otherwise
        (default True)

    layer : int
         layer value

         lower layer values are displayed later in the frame (default 0)

    arg : any
        this is used when a parameter is a function with two parameters, as the first argument or
        if a parameter is a method as the instance

        default: self (instance itself)

    parent : Component
        component where this animation object belongs to (default None)

        if given, the animation object will be removed
        automatically when the parent component is no longer accessible

    env : Environment
        environment where the component is defined

        if omitted, default_env will be used

    Note
    ----
    All parameters, apart from parent, arg and env can be specified as:

    - a scalar, like 10

    - a function with zero arguments, like lambda: my_x

    - a function with one argument, being the time t, like lambda t: t + 10

    - a function with two parameters, being arg (as given) and the time, like lambda comp, t: comp.state

    - a method instance arg for time t, like self.state, actually leading to arg.state(t) to be called

    """

    def __init__(
        self,
        x0: Union[float, Callable] = 0,
        y0: Union[float, Callable] = 0,
        z0: Union[float, Callable] = 0,
        x1: Union[float, Callable] = 1,
        y1: Union[float, Callable] = 1,
        z1: Union[float, Callable] = 0,
        color: Union[ColorType, Callable] = "white",
        visible: Union[bool, Callable] = True,
        arg: Any = None,
        layer: Union[float, Callable] = 0,
        parent: "Component" = None,
        env: "Environment" = None,
        **kwargs,
    ):
        super().__init__(visible=visible, arg=arg, layer=layer, parent=parent, env=env, **kwargs)

        self.x0 = x0
        self.y0 = y0
        self.z0 = z0
        self.x1 = x1
        self.y1 = y1
        self.z1 = z1
        self.color = color
        self.register_dynamic_attributes("x0 y0 z0 x1 y1 z1 color")

    def draw(self, t):
        gl_color = self.env.colorspec_to_gl_color(self.color(t))
        x0 = self.x0(t)
        y0 = self.y0(t)
        z0 = self.z0(t)
        x1 = self.x1(t)
        y1 = self.y1(t)
        z1 = self.z1(t)
        draw_line3d(x0=x0, y0=y0, z0=z0, x1=x1, y1=y1, z1=z1, gl_color=gl_color)


class Animate3dGrid(Animate3dBase):
    """
    Creates a 3D grid

    Parameters
    ----------
    x_range : iterable
        x coordinates of grid lines (default [0])

    y_range : iterable
        y coordinates of grid lines (default [0])

    z_range : iterable
        z coordinates of grid lines (default [0])

    color : colorspec
        color of the line (default "white")

    visible : bool
        visible

        if False, animation object is not shown, shown otherwise
        (default True)

    layer : int
         layer value

         lower layer values are displayed later in the frame (default 0)

    arg : any
        this is used when a parameter is a function with two parameters, as the first argument or
        if a parameter is a method as the instance

        default: self (instance itself)

    parent : Component
        component where this animation object belongs to (default None)

        if given, the animation object will be removed
        automatically when the parent component is no longer accessible

    env : Environment
        environment where the component is defined

        if omitted, default_env will be used

    Note
    ----
    All parameters, apart from parent, arg and env can be specified as:

    - a scalar, like 10

    - a function with zero arguments, like lambda: my_x

    - a function with one argument, being the time t, like lambda t: t + 10

    - a function with two parameters, being arg (as given) and the time, like lambda comp, t: comp.state

    - a method instance arg for time t, like self.state, actually leading to arg.state(t) to be called

    """

    def __init__(
        self,
        x_range: Union[Iterable[float], Callable] = [0],
        y_range: Union[Iterable[float], Callable] = [0],
        z_range: Union[Iterable[float], Callable] = [0],
        color: Union[ColorType, Callable] = "white",
        visible: Union[bool, Callable] = True,
        arg: Any = None,
        layer: Union[float, Callable] = 0,
        parent: "Component" = None,
        env: "Environment" = None,
        **kwargs,
    ):
        super().__init__(visible=visible, arg=arg, layer=layer, parent=parent, env=env, **kwargs)

        self.x_range = x_range
        self.y_range = y_range
        self.z_range = z_range
        self.color = color
        self.register_dynamic_attributes("x_range y_range z_range color")

    def draw(self, t):
        gl_color = self.env.colorspec_to_gl_color(self.color(t))
        x_range = list(self.x_range(t))
        y_range = list(self.y_range(t))
        z_range = list(self.z_range(t))

        for x in x_range:
            for y in y_range:
                draw_line3d(x0=x, y0=y, z0=min(z_range), x1=x, y1=y, z1=max(z_range), gl_color=gl_color)

            for z in z_range:
                draw_line3d(x0=x, y0=min(y_range), z0=z, x1=x, y1=max(y_range), z1=z, gl_color=gl_color)

        for y in y_range:
            for x in x_range:
                draw_line3d(x0=x, y0=y, z0=min(z_range), x1=x, y1=y, z1=max(z_range), gl_color=gl_color)

            for z in z_range:
                draw_line3d(x0=min(x_range), y0=y, z0=z, x1=max(x_range), y1=y, z1=z, gl_color=gl_color)

        for z in z_range:
            for x in x_range:
                draw_line3d(x0=x, y0=min(y_range), z0=z, x1=x, y1=max(y_range), z1=z, gl_color=gl_color)

            for y in y_range:
                draw_line3d(x0=min(x_range), y0=y, z0=z, x1=max(x_range), y1=y, z1=z, gl_color=gl_color)


class Animate3dBox(Animate3dBase):
    """
    Creates a 3D box

    Parameters
    ----------
    x_len : float
        length of the box in x direction (deffult 1)

    y_len : float
        length of the box in y direction (default 1)

    z_len : float
        length of the box in z direction (default 1)

    x : float
        x position of the box (default 0)

    y : float
        y position of the box (default 0)

    z : float
        z position of the box (default 0)

    z_angle : float
        angle around the z-axis (default 0)

    x_ref : int
        if -1, the x parameter refers to the 'end' of the box

        if  0, the x parameter refers to the center of the box (default)

        if  1, the x parameter refers to the 'start' of the box

    y_ref : int
        if -1, the y parameter refers to the 'end' of the box

        if  0, the y parameter refers to the center of the box (default)

        if  1, the y parameter refers to the 'start' of the box

    z_ref : int
        if -1, the z parameter refers to the 'end' of the box

        if  0, the z parameter refers to the center of the box (default)

        if  1, the z parameter refers to the 'start' of the box

    color : colorspec
        color of the box (default "white")

        if the color is "" (or the alpha is 0), the sides will not be colored at all

    edge_color : colorspec
        color of the edges of the (default "")

        if the color is "" (or the alpha is 0), the edges will not be drawn at all

    shaded : bool
        if False (default), all sides will be colored with color
        if True, the various sides will have a sligtly different darkness, thus resulting in a pseudo shaded object

    visible : bool
        visible

        if False, animation object is not shown, shown otherwise
        (default True)

    layer : int
         layer value

         lower layer values are displayed later in the frame (default 0)

    arg : any
        this is used when a parameter is a function with two parameters, as the first argument or
        if a parameter is a method as the instance

        default: self (instance itself)

    parent : Component
        component where this animation object belongs to (default None)

        if given, the animation object will be removed
        automatically when the parent component is no longer accessible

    env : Environment
        environment where the component is defined

        if omitted, default_env will be used

    Note
    ----
    All parameters, apart from parent, arg and env can be specified as:

    - a scalar, like 10

    - a function with zero arguments, like lambda: my_x

    - a function with one argument, being the time t, like lambda t: t + 10

    - a function with two parameters, being arg (as given) and the time, like lambda comp, t: comp.state

    - a method instance arg for time t, like self.state, actually leading to arg.state(t) to be called

    """

    def __init__(
        self,
        x_len: Union[float, Callable] = 1,
        y_len: Union[float, Callable] = 1,
        z_len: Union[float, Callable] = 1,
        x: Union[float, Callable] = 0,
        y: Union[float, Callable] = 0,
        z: Union[float, Callable] = 0,
        z_angle: Union[float, Callable] = 0,
        x_ref: Union[float, Callable] = 0,
        y_ref: Union[float, Callable] = 0,
        z_ref: Union[float, Callable] = 0,
        color: Union[ColorType, Callable] = "white",
        edge_color: Union[ColorType, Callable] = "",
        shaded: Union[bool, Callable] = False,
        visible: Union[bool, Callable] = True,
        arg: Any = None,
        layer: Union[float, Callable] = 0,
        parent: "Component" = None,
        env: "Environment" = None,
        **kwargs,
    ):
        super().__init__(visible=visible, arg=arg, layer=layer, parent=parent, env=env, **kwargs)

        self.x_len = x_len
        self.y_len = y_len
        self.z_len = z_len
        self.x = x
        self.y = y
        self.z = z
        self.z_angle = z_angle
        self.x_ref = x_ref
        self.y_ref = y_ref
        self.z_ref = z_ref
        self.color = color
        self.edge_color = edge_color
        self.shaded = shaded
        self.register_dynamic_attributes("x_len y_len z_len x y z z_angle x_ref y_ref z_ref color edge_color shaded")
        self.x_offset = 0
        self.y_offset = 0
        self.z_offset = 0

    def draw(self, t):
        gl_color, show = self.env.colorspec_to_gl_color_alpha(self.color(t))
        gl_edge_color, show_edge = self.env.colorspec_to_gl_color_alpha(self.edge_color(t))

        draw_box3d(
            x_len=self.x_len(t),
            y_len=self.y_len(t),
            z_len=self.z_len(t),
            x=self.x(t) + self.x_offset,
            y=self.y(t) + self.y_offset,
            z=self.z(t) + self.z_offset,
            x_angle=0,
            y_angle=0,
            z_angle=self.z_angle(t),
            x_ref=self.x_ref(t),
            y_ref=self.y_ref(t),
            z_ref=self.z_ref(t),
            gl_color=gl_color,
            show=show,
            edge_gl_color=gl_edge_color,
            show_edge=show_edge,
            shaded=self.shaded(t),
        )


class Animate3dBar(Animate3dBase):
    """
    Creates a 3D bar between two given points

    Parameters
    ----------
    x0 : float
        x coordinate of start point (default 0)

    y0 : float
        y coordinate of start point (default 0)

    z0 : float
        z coordinate of start point (default 0)

    x1 : float
        x coordinate of end point (default 0)

    y1 : float
        y coordinate of end point (default 0)

    z1 : float
        z coordinate of end point (default 0)

    color : colorspec
        color of the bar (default "white")

        if the color is "" (or the alpha is 0), the sides will not be colored at all

    edge_color : colorspec
        color of the edges of the (default "")

        if the color is "" (or the alpha is 0), the edges will not be drawn at all

    shaded : bool
        if False (default), all sides will be colored with color
        if True, the various sides will have a sligtly different darkness, thus resulting in a pseudo shaded object

    bar_width : float
        width of the bar (default 1)

    bar_width_2 : float
        if not specified both sides will have equal width (bar_width)

        if specified, the bar will have width bar_width and bar_width_2

    rotation_angle : float
        rotation of the bar in degrees (default 0)

    show_lids : bool
        if True (default), show the 'lids'

        if False, it's a hollow bar

    visible : bool
        visible

        if False, animation object is not shown, shown otherwise
        (default True)

    layer : int
         layer value

         lower layer values are displayed later in the frame (default 0)

    arg : any
        this is used when a parameter is a function with two parameters, as the first argument or
        if a parameter is a method as the instance

        default: self (instance itself)

    parent : Component
        component where this animation object belongs to (default None)

        if given, the animation object will be removed
        automatically when the parent component is no longer accessible

    env : Environment
        environment where the component is defined

        if omitted, default_env will be used

    Note
    ----
    All parameters, apart from parent, arg and env can be specified as:

    - a scalar, like 10

    - a function with zero arguments, like lambda: my_x

    - a function with one argument, being the time t, like lambda t: t + 10

    - a function with two parameters, being arg (as given) and the time, like lambda comp, t: comp.state

    - a method instance arg for time t, like self.state, actually leading to arg.state(t) to be called

    """

    def __init__(
        self,
        x0: Union[float, Callable] = 0,
        y0: Union[float, Callable] = 0,
        z0: Union[float, Callable] = 0,
        x1: Union[float, Callable] = 1,
        y1: Union[float, Callable] = 1,
        z1: Union[float, Callable] = 1,
        color: Union[ColorType, Callable] = "white",
        edge_color: Union[ColorType, Callable] = "",
        bar_width: Union[float, Callable] = 1,
        bar_width_2: Union[float, Callable] = None,
        shaded: Union[bool, Callable] = False,
        rotation_angle: Union[float, Callable] = 0,
        show_lids: Union[bool, Callable] = True,
        visible: Union[bool, Callable] = True,
        arg: Any = None,
        layer: Union[float, Callable] = 0,
        parent: "Component" = None,
        env: "Environment" = None,
        **kwargs,
    ):
        super().__init__(visible=visible, arg=arg, layer=layer, parent=parent, env=env, **kwargs)

        self.x0 = x0
        self.y0 = y0
        self.z0 = z0
        self.x1 = x1
        self.y1 = y1
        self.z1 = z1
        self.color = color
        self.edge_color = edge_color
        self.bar_width = bar_width
        self.bar_width_2 = bar_width_2
        self.shaded = shaded
        self.rotation_angle = rotation_angle
        self.show_lids = show_lids
        self.register_dynamic_attributes("x0 y0 z0 x1 y1 z1 color bar_width bar_width_2 edge_color shaded rotation_angle show_lids")
        self.x_offset = 0
        self.y_offset = 0
        self.z_offset = 0

    def draw(self, t):
        x0, x1 = self.x0(t) + self.x_offset, self.x1(t) + self.x_offset
        y0, y1 = self.y0(t) + self.y_offset, self.y1(t) + self.y_offset
        z0, z1 = self.z0(t) + self.z_offset, self.z1(t) + self.z_offset

        bar_width = self.bar_width(t)
        bar_width_2 = self.bar_width_2(t)
        gl_color, show = self.env.colorspec_to_gl_color_alpha(self.color(t))
        edge_gl_color, show_edge = self.env.colorspec_to_gl_color_alpha(self.edge_color(t))
        shaded = self.shaded(t)
        rotation_angle = self.rotation_angle(t)
        show_lids = self.show_lids(t)
        draw_bar3d(
            x0=x0,
            y0=y0,
            z0=z0,
            x1=x1,
            y1=y1,
            z1=z1,
            bar_width=bar_width,
            bar_width_2=bar_width_2,
            gl_color=gl_color,
            show=show,
            edge_gl_color=edge_gl_color,
            show_edge=show_edge,
            shaded=shaded,
            rotation_angle=rotation_angle,
            show_lids=show_lids,
        )


class Animate3dCylinder(Animate3dBase):
    """
    Creates a 3D cylinder between two given points

    Parameters
    ----------
    x0 : float
        x coordinate of start point (default 0)

    y0 : float
        y coordinate of start point (default 0)

    z0 : float
        z coordinate of start point (default 0)

    x1 : float
        x coordinate of end point (default 0)

    y1 : float
        y coordinate of end point (default 0)

    z1 : float
        z coordinate of end point (default 0)

    color : colorspec
        color of the cylinder (default "white")

    radius : float
        radius of the cylinder (default 1)

    number_of_sides : int
        number of sides of the cylinder (default 8)

        must be >= 3

    rotation_angle : float
        rotation of the bar in degrees (default 0)

    show_lids : bool
        if True (default), the lids will be drawn
        if False, tyhe cylinder will be open at both sides

    visible : bool
        visible

        if False, animation object is not shown, shown otherwise
        (default True)

    layer : int
         layer value

         lower layer values are displayed later in the frame (default 0)

    arg : any
        this is used when a parameter is a function with two parameters, as the first argument or
        if a parameter is a method as the instance

        default: self (instance itself)

    parent : Component
        component where this animation object belongs to (default None)

        if given, the animation object will be removed
        automatically when the parent component is no longer accessible

    env : Environment
        environment where the component is defined

        if omitted, default_env will be used

    Note
    ----
    All parameters, apart from parent, arg and env can be specified as:

    - a scalar, like 10

    - a function with zero arguments, like lambda: my_x

    - a function with one argument, being the time t, like lambda t: t + 10

    - a function with two parameters, being arg (as given) and the time, like lambda comp, t: comp.state

    - a method instance arg for time t, like self.state, actually leading to arg.state(t) to be called

    """

    def __init__(
        self,
        x0: Union[float, Callable] = 0,
        y0: Union[float, Callable] = 0,
        z0: Union[float, Callable] = 0,
        x1: Union[float, Callable] = 1,
        y1: Union[float, Callable] = 1,
        z1: Union[float, Callable] = 1,
        color: Union[ColorType, Callable] = "white",
        radius: Union[float, Callable] = 1,
        number_of_sides: Union[int, Callable] = 8,
        rotation_angle: Union[float, Callable] = 0,
        show_lids: Union[bool, Callable] = True,
        visible: Union[bool, Callable] = True,
        arg: Any = None,
        layer: Union[float, Callable] = 0,
        parent: "Component" = None,
        env: "Environment" = None,
        **kwargs,
    ):
        super().__init__(visible=visible, arg=arg, layer=layer, parent=parent, env=env, **kwargs)
        self.x0 = x0
        self.y0 = y0
        self.z0 = z0
        self.x1 = x1
        self.y1 = y1
        self.z1 = z1
        self.color = color
        self.radius = radius
        self.number_of_sides = number_of_sides
        self.rotation_angle = rotation_angle
        self.show_lids = show_lids
        self.register_dynamic_attributes("x0 y0 z0 x1 y1 z1 color radius number_of_sides rotation_angle show_lids")
        self.x_offset = 0
        self.y_offset = 0
        self.z_offset = 0

    def draw(self, t):
        x0, x1 = self.x0(t) + self.x_offset, self.x1(t) + self.x_offset
        y0, y1 = self.y0(t) + self.y_offset, self.y1(t) + self.y_offset
        z0, z1 = self.z0(t) + self.z_offset, self.z1(t) + self.z_offset

        gl_color = self.env.colorspec_to_gl_color(self.color(t))
        rotation_angle = self.rotation_angle(t)
        radius = self.radius(t)
        number_of_sides = self.number_of_sides(t)
        show_lids = self.show_lids(t)
        draw_cylinder3d(
            x0=x0,
            y0=y0,
            z0=z0,
            x1=x1,
            y1=y1,
            z1=z1,
            radius=radius,
            number_of_sides=number_of_sides,
            gl_color=gl_color,
            rotation_angle=rotation_angle,
            show_lids=show_lids,
        )


class Animate3dSphere(Animate3dBase):
    """
    Creates a 3D box

    Parameters
    ----------
    radius : float
        radius of the sphere

    x : float
        x position of the box (default 0)

    y : float
        y position of the box (default 0)

    z : float
        z position of the box (default 0)

    color : colorspec
        color of the sphere (default "white")

    visible : bool
        visible

        if False, animation object is not shown, shown otherwise
        (default True)

    layer : int
         layer value

         lower layer values are displayed later in the frame (default 0)

    arg : any
        this is used when a parameter is a function with two parameters, as the first argument or
        if a parameter is a method as the instance

        default: self (instance itself)

    parent : Component
        component where this animation object belongs to (default None)

        if given, the animation object will be removed
        automatically when the parent component is no longer accessible

    env : Environment
        environment where the component is defined

        if omitted, default_env will be used

    Note
    ----
    All parameters, apart from parent, arg and env can be specified as:

    - a scalar, like 10

    - a function with zero arguments, like lambda: my_x

    - a function with one argument, being the time t, like lambda t: t + 10

    - a function with two parameters, being arg (as given) and the time, like lambda comp, t: comp.state

    - a method instance arg for time t, like self.state, actually leading to arg.state(t) to be called

    """

    def __init__(
        self,
        radius: Union[float, Callable] = 1,
        x: Union[float, Callable] = 0,
        y: Union[float, Callable] = 0,
        z: Union[float, Callable] = 0,
        color: Union[ColorType, Callable] = "white",
        number_of_slices: Union[int, Callable] = 32,
        number_of_stacks: Union[int, Callable] = None,
        visible: Union[bool, Callable] = True,
        arg: Any = None,
        layer: Union[float, Callable] = 0,
        parent: "Component" = None,
        env: "Environment" = None,
        **kwargs,
    ):
        super().__init__(visible=visible, arg=arg, layer=layer, parent=parent, env=env, **kwargs)

        self.radius = radius
        self.x = x
        self.y = y
        self.z = z
        self.color = color
        self.number_of_slices = number_of_slices
        self.number_of_stacks = number_of_stacks
        self.register_dynamic_attributes("radius x y z color number_of_slices number_of_stacks")
        self.x_offset = 0
        self.y_offset = 0
        self.z_offset = 0

    def draw(self, t):
        gl_color = self.env.colorspec_to_gl_color(self.color(t))
        draw_sphere3d(
            radius=self.radius(t),
            x=self.x(t) + self.x_offset,
            y=self.y(t) + self.y_offset,
            z=self.z(t) + self.z_offset,
            gl_color=gl_color,
            number_of_slices=self.number_of_slices(t),
            number_of_stacks=self.number_of_stacks(t),
        )


def draw_bar3d(
    x0=0,
    y0=0,
    z0=0,
    x1=1,
    y1=1,
    z1=1,
    gl_color=(1, 1, 1),
    show=True,
    edge_gl_color=(1, 1, 1),
    show_edge=False,
    bar_width=1,
    bar_width_2=None,
    rotation_angle=0,
    shaded=False,
    show_lids=True,
):
    """
    draws a 3d bar (should be added to the event loop by encapsulating with Animate3dBase)

    Parameters
    ----------
    x0 : int, optional
        [description], by default 0
    y0 : int, optional
        [description], by default 0
    z0 : int, optional
        [description], by default 0
    x1 : int, optional
        [description], by default 1
    y1 : int, optional
        [description], by default 1
    z1 : int, optional
        [description], by default 1
    gl_color : tuple, optional
        [description], by default (1, 1, 1)
    show : bool, optional
        [description], by default True
    edge_gl_color : tuple, optional
        [description], by default (1, 1, 1)
    show_edge : bool, optional
        [description], by default False
    bar_width : int, optional
        [description], by default 1
    bar_width_2 : [type], optional
        [description], by default None
    rotation_angle : int, optional
        [description], by default 0
    shaded : bool, optional
        [description], by default False
    show_lids : bool, optional
        [description], by default True
    """
    dx = x1 - x0
    dy = y1 - y0
    dz = z1 - z0

    length = math.sqrt(dx**2 + dy**2 + dz**2)
    y_angle = -math.degrees(math.atan2(dz, math.sqrt(dx**2 + dy**2)))
    z_angle = math.degrees(math.atan2(dy, dx))
    bar_width_2 = bar_width if bar_width_2 is None else bar_width_2

    draw_box3d(
        x=x0,
        y=y0,
        z=z0,
        x_len=length,
        y_len=bar_width,
        z_len=bar_width_2,
        x_angle=rotation_angle,
        y_angle=y_angle,
        z_angle=z_angle,
        x_ref=1,
        y_ref=0,
        z_ref=0,
        gl_color=gl_color,
        show=show,
        edge_gl_color=edge_gl_color,
        show_edge=show_edge,
        shaded=shaded,
        _show_lids=show_lids,
    )


def draw_cylinder3d(x0=0, y0=0, z0=0, x1=1, y1=1, z1=1, gl_color=(1, 1, 1), radius=1, number_of_sides=8, rotation_angle=0, show_lids=True):
    """
    draws a 3d cylinder (should be added to the event loop by encapsulating with Animate3dBase)

    Parameters
    ----------
    x0 : int, optional
        [description], by default 0
    y0 : int, optional
        [description], by default 0
    z0 : int, optional
        [description], by default 0
    x1 : int, optional
        [description], by default 1
    y1 : int, optional
        [description], by default 1
    z1 : int, optional
        [description], by default 1
    gl_color : tuple, optional
        [description], by default (1, 1, 1)
    radius : int, optional
        [description], by default 1
    number_of_sides : int, optional
        [description], by default 8
    rotation_angle : int, optional
        [description], by default 0
    show_lids : bool, optional
        [description], by default True
    """
    dx = x1 - x0
    dy = y1 - y0
    dz = z1 - z0

    length = math.sqrt(dx**2 + dy**2 + dz**2)
    y_angle = -math.degrees(math.atan2(dz, math.sqrt(dx**2 + dy**2)))
    z_angle = math.degrees(math.atan2(dy, dx))
    x_angle = rotation_angle
    gl.glPushMatrix()
    gl.glMaterialfv(gl.GL_FRONT, gl.GL_AMBIENT_AND_DIFFUSE, gl_color)

    gl.glTranslate(x0, y0, z0)
    if z_angle:
        gl.glRotate(z_angle, 0.0, 0.0, 1.0)
    if y_angle:
        gl.glRotate(y_angle, 0.0, 1.0, 0.0)
    if x_angle:
        gl.glRotate(x_angle, 1.0, 0.0, 0.0)

    step_angle = 360 / number_of_sides
    start_angle = -90 + step_angle / 2

    two_d_vertices = []
    for i in range(number_of_sides):
        angle = math.radians((i * step_angle + start_angle))
        two_d_vertices.append((radius * math.cos(angle), radius * math.sin(angle)))
    two_d_vertices.append(two_d_vertices[0])

    if show_lids:
        """draw front lid"""
        gl.glBegin(gl.GL_TRIANGLE_FAN)
        gl.glNormal3f(-1, 0, 0)
        for two_d_vertex in two_d_vertices:
            gl.glVertex3f(0, two_d_vertex[0], two_d_vertex[1])
        gl.glEnd()

        """ draw back lid """
        gl.glBegin(gl.GL_TRIANGLE_FAN)
        gl.glNormal3f(1, 0, 0)
        for two_d_vertex in two_d_vertices:
            gl.glVertex3f(length, two_d_vertex[0], two_d_vertex[1])
        gl.glEnd()

    """ draw sides """
    gl.glBegin(gl.GL_QUADS)
    for i, (two_d_vertex0, two_d_vertex1) in enumerate(zip(two_d_vertices, two_d_vertices[1:])):
        a1 = math.radians((start_angle + (i + 0.5) * step_angle))
        gl.glNormal3f(0, math.cos(a1), math.sin(a1))
        gl.glVertex3f(0, *two_d_vertex0)
        gl.glVertex3f(length, *two_d_vertex0)
        gl.glVertex3f(length, *two_d_vertex1)
        gl.glVertex3f(0, *two_d_vertex1)
    gl.glEnd()

    gl.glPopMatrix()


def draw_line3d(x0=0, y0=0, z0=0, x1=1, y1=1, z1=1, gl_color=(1, 1, 1)):
    """
    draws a 3d line (should be added to the event loop by encapsulating with Animate3dBase)

    Parameters
    ----------
    x0 : int, optional
        [description], by default 0
    y0 : int, optional
        [description], by default 0
    z0 : int, optional
        [description], by default 0
    x1 : int, optional
        [description], by default 1
    y1 : int, optional
        [description], by default 1
    z1 : int, optional
        [description], by default 1
    gl_color : tuple, optional
        [description], by default (1, 1, 1)
    """
    gl.glMaterialfv(gl.GL_FRONT, gl.GL_AMBIENT_AND_DIFFUSE, gl_color)

    gl.glBegin(gl.GL_LINES)
    gl.glVertex3f(x0, y0, z0)
    gl.glVertex3f(x1, y1, z1)
    gl.glEnd()


def draw_rectangle3d(x0=0, y0=0, z=0, x1=1, y1=1, gl_color=(1, 1, 1)):
    """
    draws a 3d rectangle (should be added to the event loop by encapsulating with Animate3dBase)

    Parameters
    ----------
    x0 : int, optional
        [description], by default 0
    y0 : int, optional
        [description], by default 0
    z : int, optional
        [description], by default 0
    x1 : int, optional
        [description], by default 1
    y1 : int, optional
        [description], by default 1
    gl_color : tuple, optional
        [description], by default (1, 1, 1)
    """
    gl.glMaterialfv(gl.GL_FRONT, gl.GL_AMBIENT_AND_DIFFUSE, gl_color)

    gl.glBegin(gl.GL_QUADS)
    gl.glVertex3f(x0, y0, z)
    gl.glVertex3f(x1, y0, z)
    gl.glVertex3f(x1, y1, z)
    gl.glVertex3f(x0, y1, z)
    gl.glEnd()


def draw_box3d(
    x_len=1,
    y_len=1,
    z_len=1,
    x=0,
    y=0,
    z=0,
    x_angle=0,
    y_angle=0,
    z_angle=0,
    x_ref=0,
    y_ref=0,
    z_ref=0,
    gl_color=(1, 1, 1),
    show=True,
    edge_gl_color=(1, 1, 1),
    show_edge=False,
    shaded=False,
    _show_lids=True,
):
    """
    draws a 3d box (should be added to the event loop by encapsulating with Animate3dBase)

    Parameters
    ----------
    x_len : int, optional
        [description], by default 1
    y_len : int, optional
        [description], by default 1
    z_len : int, optional
        [description], by default 1
    x : int, optional
        [description], by default 0
    y : int, optional
        [description], by default 0
    z : int, optional
        [description], by default 0
    x_angle : int, optional
        [description], by default 0
    y_angle : int, optional
        [description], by default 0
    z_angle : int, optional
        [description], by default 0
    x_ref : int, optional
        [description], by default 0
    y_ref : int, optional
        [description], by default 0
    z_ref : int, optional
        [description], by default 0
    gl_color : tuple, optional
        [description], by default (1, 1, 1)
    show : bool, optional
        [description], by default True
    edge_gl_color : tuple, optional
        [description], by default (1, 1, 1)
    show_edge : bool, optional
        [description], by default False
    shaded : bool, optional
        [description], by default False
    _show_lids : bool, optional
        [description], by default True
    """
    gl_color0 = gl_color
    if shaded:
        gl_color1 = (gl_color0[0] * 0.9, gl_color0[1] * 0.9, gl_color0[2] * 0.9)
        gl_color2 = (gl_color0[0] * 0.8, gl_color0[1] * 0.8, gl_color0[2] * 0.8)
    else:
        gl_color1 = gl_color2 = gl_color0

    x1 = ((x_ref - 1) / 2) * x_len
    x2 = ((x_ref + 1) / 2) * x_len

    y1 = ((y_ref - 1) / 2) * y_len
    y2 = ((y_ref + 1) / 2) * y_len

    z1 = ((z_ref - 1) / 2) * z_len
    z2 = ((z_ref + 1) / 2) * z_len

    bv = [[x1, y1, z1], [x2, y1, z1], [x2, y2, z1], [x1, y2, z1], [x1, y1, z2], [x2, y1, z2], [x2, y2, z2], [x1, y2, z2]]

    gl.glPushMatrix()

    gl.glTranslate(x, y, z)
    if z_angle:
        gl.glRotate(z_angle, 0.0, 0.0, 1.0)
    if y_angle:
        gl.glRotate(y_angle, 0.0, 1.0, 0.0)
    if x_angle:
        gl.glRotate(x_angle, 1.0, 0.0, 0.0)

    if show:
        gl.glBegin(gl.GL_QUADS)

        # bottom z-
        gl.glMaterialfv(gl.GL_FRONT, gl.GL_AMBIENT_AND_DIFFUSE, gl_color0)
        gl.glNormal(0, 0, -1)
        gl.glVertex3f(*bv[0])
        gl.glVertex3f(*bv[1])
        gl.glVertex3f(*bv[2])
        gl.glVertex3f(*bv[3])

        # top z+
        gl.glMaterialfv(gl.GL_FRONT, gl.GL_AMBIENT_AND_DIFFUSE, gl_color0)
        gl.glNormal3f(0, 0, 1)
        gl.glVertex3f(*bv[4])
        gl.glVertex3f(*bv[5])
        gl.glVertex3f(*bv[6])
        gl.glVertex3f(*bv[7])

        # left y-
        gl.glMaterialfv(gl.GL_FRONT, gl.GL_AMBIENT_AND_DIFFUSE, gl_color1)
        gl.glNormal3f(0, -1, 0)
        gl.glVertex3f(*bv[0])
        gl.glVertex3f(*bv[1])
        gl.glVertex3f(*bv[5])
        gl.glVertex3f(*bv[4])

        # right y+
        gl.glMaterialfv(gl.GL_FRONT, gl.GL_AMBIENT_AND_DIFFUSE, gl_color1)
        gl.glNormal3f(0, 1, 0)
        gl.glVertex3f(*bv[2])
        gl.glVertex3f(*bv[3])
        gl.glVertex3f(*bv[7])
        gl.glVertex3f(*bv[6])

        if _show_lids:
            # front x+
            gl.glMaterialfv(gl.GL_FRONT, gl.GL_AMBIENT_AND_DIFFUSE, gl_color2)
            gl.glNormal3f(1, 0, 0)
            gl.glVertex3f(*bv[1])
            gl.glVertex3f(*bv[2])
            gl.glVertex3f(*bv[6])
            gl.glVertex3f(*bv[5])

            # front x-
            gl.glMaterialfv(gl.GL_FRONT, gl.GL_AMBIENT_AND_DIFFUSE, gl_color2)
            gl.glNormal3f(-1, 0, 0)
            gl.glVertex3f(*bv[3])
            gl.glVertex3f(*bv[0])
            gl.glVertex3f(*bv[4])
            gl.glVertex3f(*bv[7])

        gl.glEnd()

    if show_edge:
        gl.glBegin(gl.GL_LINES)
        gl_color = (1, 0, 1)
        gl.glMaterialfv(gl.GL_FRONT, gl.GL_AMBIENT_AND_DIFFUSE, edge_gl_color)

        gl.glVertex3f(*bv[0])
        gl.glVertex3f(*bv[4])

        gl.glVertex3f(*bv[4])
        gl.glVertex3f(*bv[7])

        gl.glVertex3f(*bv[7])
        gl.glVertex3f(*bv[3])

        gl.glVertex3f(*bv[3])
        gl.glVertex3f(*bv[0])

        gl.glVertex3f(*bv[5])
        gl.glVertex3f(*bv[6])

        gl.glVertex3f(*bv[6])
        gl.glVertex3f(*bv[2])

        gl.glVertex3f(*bv[2])
        gl.glVertex3f(*bv[1])

        gl.glVertex3f(*bv[1])
        gl.glVertex3f(*bv[5])

        gl.glVertex3f(*bv[0])
        gl.glVertex3f(*bv[1])

        gl.glVertex3f(*bv[4])
        gl.glVertex3f(*bv[5])

        gl.glVertex3f(*bv[7])
        gl.glVertex3f(*bv[6])

        gl.glVertex3f(*bv[3])
        gl.glVertex3f(*bv[2])

        gl.glEnd()

    gl.glPopMatrix()


def draw_sphere3d(x=0, y=0, z=0, radius=1, number_of_slices=32, number_of_stacks=None, gl_color=(1, 1, 1)):
    """
    draws a 3d spere (should be added to the event loop by encapsulating with Animate3dBase)

    Parameters
    ----------
    radius : float, optional
    """
    quadratic = glu.gluNewQuadric()
    glu.gluQuadricNormals(quadratic, glu.GLU_SMOOTH)
    glu.gluQuadricTexture(quadratic, gl.GL_TRUE)

    gl.glPushMatrix()

    gl.glTranslate(x, y, z)
    gl.glMaterialfv(gl.GL_FRONT, gl.GL_AMBIENT_AND_DIFFUSE, gl_color)

    glu.gluSphere(quadratic, radius, number_of_slices, number_of_slices if number_of_stacks is None else number_of_stacks)

    gl.glPopMatrix()


def _std_fonts():
    # the names of the standard fonts are generated by ttf fontdict.py on the standard development machine
    if not hasattr(_std_fonts, "cached"):
        _std_fonts.cached = pickle.loads(
            b"(dp0\nVHuxley_Titling\np1\nVHuxley Titling\np2\nsVGlock___\np3\nVGlockenspiel\np4\nsVPENLIIT_\np5\nVPenultimateLightItal\np6\nsVERASMD\np7\nVEras Medium ITC\np8\nsVNirmala\np9\nVNirmala UI\np10\nsVebrimabd\np11\nVEbrima Bold\np12\nsVostrich-dashed\np13\nVOstrich Sans Dashed Medium\np14\nsVLato-Hairline\np15\nVLato Hairline\np16\nsVLTYPEO\np17\nVLucida Sans Typewriter Oblique\np18\nsVbnmachine\np19\nVBN Machine\np20\nsVLTYPEB\np21\nVLucida Sans Typewriter Bold\np22\nsVBOOKOSI\np23\nVBookman Old Style Italic\np24\nsVEmmett__\np25\nVEmmett\np26\nsVCURLZ___\np27\nVCurlz MT\np28\nsVhandmeds\np29\nVHand Me Down S (BRK)\np30\nsVsegoesc\np31\nVSegoe Script\np32\nsVTCM_____\np33\nVTw Cen MT\np34\nsVJosefinSlab-ThinItalic\np35\nVJosefin Slab Thin Italic\np36\nsVSTENCIL\np37\nVStencil\np38\nsVsanss___\np39\nVSansSerif\np40\nsVBOD_CI\np41\nVBodoni MT Condensed Italic\np42\nsVGreek_i\np43\nVGreek Diner Inline TT\np44\nsVHTOWERT\np45\nVHigh Tower Text\np46\nsVTCCB____\np47\nVTw Cen MT Condensed Bold\np48\nsVCools___\np49\nVCoolsville\np50\nsVbnjinx\np51\nVBN Jinx\np52\nsVFREESCPT\np53\nVFreestyle Script\np54\nsVGARA\np55\nVGaramond\np56\nsVDejaVuSansMono\np57\nVDejaVu Sans Mono Book\np58\nsVCALVIN__\np59\nVCalvin\np60\nsVGIL_____\np61\nVGill Sans MT\np62\nsVCandaraz\np63\nVCandara Bold Italic\np64\nsVVollkorn-Bold\np65\nVVollkorn Bold\np66\nsVariblk\np67\nVArial Black\np68\nsVGOTHIC\np69\nVCentury Gothic\np70\nsVMAIAN\np71\nVMaiandra GD\np72\nsVBSSYM7\np73\nVBookshelf Symbol 7\np74\nsVAcme____\np75\nVAcmeFont\np76\nsVDetente_\np77\nVDetente\np78\nsVCandarai\np79\nVCandara Italic\np80\nsVFTLTLT\np81\nVFootlight MT Light\np82\nsVGILC____\np83\nVGill Sans MT Condensed\np84\nsVLFAXD\np85\nVLucida Fax Demibold\np86\nsVNIAGSOL\np87\nVNiagara Solid\np88\nsVLFAXI\np89\nVLucida Fax Italic\np90\nsVCandarab\np91\nVCandara Bold\np92\nsVFRSCRIPT\np93\nVFrench Script MT\np94\nsVLBRITE\np95\nVLucida Bright\np96\nsVFRABK\np97\nVFranklin Gothic Book\np98\nsVostrich-bold\np99\nVOstrich Sans Bold\np100\nsVTCCM____\np101\nVTw Cen MT Condensed\np102\nsVcorbelz\np103\nVCorbel Bold Italic\np104\nsVTCMI____\np105\nVTw Cen MT Italic\np106\nsVethnocen\np107\nVEthnocentric\np108\nsVVINERITC\np109\nVViner Hand ITC\np110\nsVROCKB\np111\nVRockwell Bold\np112\nsVconsola\np113\nVConsolas\np114\nsVcorbeli\np115\nVCorbel Italic\np116\nsVPENUL___\np117\nVPenultimate\np118\nsVMAGNETOB\np119\nVMagneto Bold\np120\nsVisocp___\np121\nVISOCP\np122\nsVQUIVEIT_\np123\nVQuiverItal\np124\nsVARLRDBD\np125\nVArial Rounded MT Bold\np126\nsVJosefinSlab-SemiBold\np127\nVJosefin Slab SemiBold\np128\nsVntailub\np129\nVMicrosoft New Tai Lue Bold\np130\nsVflubber\np131\nVFlubber\np132\nsVBASKVILL\np133\nVBaskerville Old Face\np134\nsVGILB____\np135\nVGill Sans MT Bold\np136\nsVPERTILI\np137\nVPerpetua Titling MT Light\np138\nsVLato-HairlineItalic\np139\nVLato Hairline Italic\np140\nsVComfortaa-Light\np141\nVComfortaa Light\np142\nsVtrebucit\np143\nVTrebuchet MS Italic\np144\nsVmalgunbd\np145\nVMalgun Gothic Bold\np146\nsVITCBLKAD\np147\nVBlackadder ITC\np148\nsVsansso__\np149\nVSansSerif Oblique\np150\nsVCALISTBI\np151\nVCalisto MT Bold Italic\np152\nsVsyastro_\np153\nVSyastro\np154\nsVSamsungIF_Md\np155\nVSamsung InterFace Medium\np156\nsVHombre__\np157\nVHombre\np158\nsVseguiemj\np159\nVSegoe UI Emoji\np160\nsVFRAHVIT\np161\nVFranklin Gothic Heavy Italic\np162\nsVJUICE___\np163\nVJuice ITC\np164\nsVFRAMDCN\np165\nVFranklin Gothic Medium Cond\np166\nsVseguisb\np167\nVSegoe UI Semibold\np168\nsVconsolai\np169\nVConsolas Italic\np170\nsVGLECB\np171\nVGloucester MT Extra Condensed\np172\nsVframd\np173\nVFranklin Gothic Medium\np174\nsVSCHLBKI\np175\nVCentury Schoolbook Italic\np176\nsVCENTAUR\np177\nVCentaur\np178\nsVromantic\np179\nVRomantic\np180\nsVBOD_CB\np181\nVBodoni MT Condensed Bold\np182\nsVverdana\np183\nVVerdana\np184\nsVTangerine_Regular\np185\nVTangerine\np186\nsVseguili\np187\nVSegoe UI Light Italic\np188\nsVNunito-Regular\np189\nVNunito\np190\nsVSCHLBKB\np191\nVCentury Schoolbook Bold\np192\nsVGOTHICB\np193\nVCentury Gothic Bold\np194\nsVpalai\np195\nVPalatino Linotype Italic\np196\nsVBKANT\np197\nVBook Antiqua\np198\nsVLato-Italic\np199\nVLato Italic\np200\nsVPERBI___\np201\nVPerpetua Bold Italic\np202\nsVGOTHICI\np203\nVCentury Gothic Italic\np204\nsVROCKBI\np205\nVRockwell Bold Italic\np206\nsVLTYPEBO\np207\nVLucida Sans Typewriter Bold Oblique\np208\nsVAmeth___\np209\nVAmethyst\np210\nsVyearsupplyoffairycakes\np211\nVYear supply of fairy cakes\np212\nsVGILBI___\np213\nVGill Sans MT Bold Italic\np214\nsVBOOKOS\np215\nVBookman Old Style\np216\nsVVollkorn-Italic\np217\nVVollkorn Italic\np218\nsVswiss\np219\nVSwis721 BT Roman\np220\nsVcomsc\np221\nVCommercialScript BT\np222\nsVchinyen\np223\nVChinyen Normal\np224\nsVeurr____\np225\nVEuroRoman\np226\nsVROCK\np227\nVRockwell\np228\nsVPERTIBD\np229\nVPerpetua Titling MT Bold\np230\nsVCHILLER\np231\nVChiller\np232\nsVtechb___\np233\nVTechnicBold\np234\nsVLato-Light\np235\nVLato Light\np236\nsVOUTLOOK\np237\nVMS Outlook\np238\nsVmtproxy6\np239\nVProxy 6\np240\nsVdutcheb\np241\nVDutch801 XBd BT Extra Bold\np242\nsVgadugib\np243\nVGadugi Bold\np244\nsVBOD_CR\np245\nVBodoni MT Condensed\np246\nsVmtproxy7\np247\nVProxy 7\np248\nsVnobile_bold\np249\nVNobile Bold\np250\nsVELEPHNT\np251\nVElephant\np252\nsVCOPRGTL\np253\nVCopperplate Gothic Light\np254\nsVMTCORSVA\np255\nVMonotype Corsiva\np256\nsVconsolaz\np257\nVConsolas Bold Italic\np258\nsVBOOKOSBI\np259\nVBookman Old Style Bold Italic\np260\nsVtrebuc\np261\nVTrebuchet MS\np262\nsVcomici\np263\nVComic Sans MS Italic\np264\nsVJosefinSlab-BoldItalic\np265\nVJosefin Slab Bold Italic\np266\nsVMycalc__\np267\nVMycalc\np268\nsVmarlett\np269\nVMarlett\np270\nsVsymeteo_\np271\nVSymeteo\np272\nsVcandles_\np273\nVCandles\np274\nsVbobcat\np275\nVBobcat Normal\np276\nsVLSANSDI\np277\nVLucida Sans Demibold Italic\np278\nsVINFROMAN\np279\nVInformal Roman\np280\nsVsf movie poster2\np281\nVSF Movie Poster\np282\nsVcomicz\np283\nVComic Sans MS Bold Italic\np284\nsVcracj___\np285\nVCracked Johnnie\np286\nsVcourbd\np287\nVCourier New Bold\np288\nsVItali___\np289\nVItalianate\np290\nsVITCEDSCR\np291\nVEdwardian Script ITC\np292\nsVcourbi\np293\nVCourier New Bold Italic\np294\nsVcalibrili\np295\nVCalibri Light Italic\np296\nsVgazzarelli\np297\nVGazzarelli\np298\nsVGabriola\np299\nVGabriola\np300\nsVVollkorn-BoldItalic\np301\nVVollkorn Bold Italic\np302\nsVromant__\np303\nVRomanT\np304\nsVisoct3__\np305\nVISOCT3\np306\nsVsegoeuib\np307\nVSegoe UI Bold\np308\nsVtimesbd\np309\nVTimes New Roman Bold\np310\nsVgoodtime\np311\nVGood Times\np312\nsVsegoeuii\np313\nVSegoe UI Italic\np314\nsVBOD_BLAR\np315\nVBodoni MT Black\np316\nsVhimalaya\np317\nVMicrosoft Himalaya\np318\nsVsegoeuil\np319\nVSegoe UI Light\np320\nsVPermanentMarker\np321\nVPermanent Marker\np322\nsVBOD_BLAI\np323\nVBodoni MT Black Italic\np324\nsVTCBI____\np325\nVTw Cen MT Bold Italic\np326\nsVarial\np327\nVArial\np328\nsVBrand___\np329\nVBrandish\np330\nsVsegoeuiz\np331\nVSegoe UI Bold Italic\np332\nsVswisscb\np333\nVSwis721 Cn BT Bold\np334\nsVPAPYRUS\np335\nVPapyrus\np336\nsVANTIC___\np337\nVAnticFont\np338\nsVGIGI\np339\nVGigi\np340\nsVENGR\np341\nVEngravers MT\np342\nsVsegmdl2\np343\nVSegoe MDL2 Assets\np344\nsVBRLNSDB\np345\nVBerlin Sans FB Demi Bold\np346\nsVLato-BoldItalic\np347\nVLato Bold Italic\np348\nsVholomdl2\np349\nVHoloLens MDL2 Assets\np350\nsVBRITANIC\np351\nVBritannic Bold\np352\nsVNirmalaB\np353\nVNirmala UI Bold\np354\nsVVollkorn-Regular\np355\nVVollkorn\np356\nsVStephen_\np357\nVStephen\np358\nsVbabyk___\np359\nVBaby Kruffy\np360\nsVHARVEST_\np361\nVHarvest\np362\nsVKUNSTLER\np363\nVKunstler Script\np364\nsVstylu\np365\nVStylus BT Roman\np366\nsVWINGDNG3\np367\nVWingdings 3\np368\nsVWINGDNG2\np369\nVWingdings 2\np370\nsVlucon\np371\nVLucida Console\np372\nsVCandara\np373\nVCandara\np374\nsVBERNHC\np375\nVBernard MT Condensed\np376\nsVtechnic_\np377\nVTechnic\np378\nsVLimou___\np379\nVLimousine\np380\nsVTCB_____\np381\nVTw Cen MT Bold\np382\nsVPirate__\np383\nVPirate\np384\nsVFrnkvent\np385\nVFrankfurter Venetian TT\np386\nsVromand__\np387\nVRomanD\np388\nsVLTYPE\np389\nVLucida Sans Typewriter\np390\nsVSHOWG\np391\nVShowcard Gothic\np392\nsVMOD20\np393\nVModern No. 20\np394\nsVostrich-rounded\np395\nVOstrich Sans Rounded Medium\np396\nsVJosefinSlab-Italic\np397\nVJosefin Slab Italic\np398\nsVneon2\np399\nVNeon Lights\np400\nsVpalabi\np401\nVPalatino Linotype Bold Italic\np402\nsVwoodcut\np403\nVWoodcut\np404\nsVToledo__\np405\nVToledo\np406\nsVverdanai\np407\nVVerdana Italic\np408\nsVSamsungIF_Rg\np409\nVSamsung InterFace\np410\nsVtrebucbd\np411\nVTrebuchet MS Bold\np412\nsVPALSCRI\np413\nVPalace Script MT\np414\nsVComfortaa-Regular\np415\nVComfortaa\np416\nsVmicross\np417\nVMicrosoft Sans Serif\np418\nsVseguisli\np419\nVSegoe UI Semilight Italic\np420\nsVtaile\np421\nVMicrosoft Tai Le\np422\nsVcour\np423\nVCourier New\np424\nsVparryhotter\np425\nVParry Hotter\np426\nsVgreekc__\np427\nVGreekC\np428\nsVRAGE\np429\nVRage Italic\np430\nsVMATURASC\np431\nVMatura MT Script Capitals\np432\nsVBASTION_\np433\nVBastion\np434\nsVREFSAN\np435\nVMS Reference Sans Serif\np436\nsVterminat\np437\nVTerminator Two\np438\nsVmmrtextb\np439\nVMyanmar Text Bold\np440\nsVgothici_\np441\nVGothicI\np442\nsVmonotxt_\np443\nVMonotxt\np444\nsVcorbelb\np445\nVCorbel Bold\np446\nsVVALKEN__\np447\nVValken\np448\nsVRowdyhe_\np449\nVRowdyHeavy\np450\nsVLato-Black\np451\nVLato Black\np452\nsVswisski\np453\nVSwis721 Blk BT Black Italic\np454\nsVcouri\np455\nVCourier New Italic\np456\nsVMTEXTRA\np457\nVMT Extra\np458\nsVsanssbo_\np459\nVSansSerif BoldOblique\np460\nsVl_10646\np461\nVLucida Sans Unicode\np462\nsVLato-BlackItalic\np463\nVLato Black Italic\np464\nsVseguibli\np465\nVSegoe UI Black Italic\np466\nsVGeotype\np467\nVGeotype TT\np468\nsVxfiles\np469\nVX-Files\np470\nsVjavatext\np471\nVJavanese Text\np472\nsVseguisym\np473\nVSegoe UI Symbol\np474\nsVverdanaz\np475\nVVerdana Bold Italic\np476\nsVGILI____\np477\nVGill Sans MT Italic\np478\nsVALGER\np479\nVAlgerian\np480\nsVAGENCYR\np481\nVAgency FB\np482\nsVnobile\np483\nVNobile\np484\nsVHaxton\np485\nVHaxton Logos TT\np486\nsVswissbo\np487\nVSwis721 BdOul BT Bold\np488\nsVBELLI\np489\nVBell MT Italic\np490\nsVBROADW\np491\nVBroadway\np492\nsVsegoepr\np493\nVSegoe Print\np494\nsVGILLUBCD\np495\nVGill Sans Ultra Bold Condensed\np496\nsVverdanab\np497\nVVerdana Bold\np498\nsVSalina__\np499\nVSalina\np500\nsVAGENCYB\np501\nVAgency FB Bold\np502\nsVAutumn__\np503\nVAutumn\np504\nsVGOUDOS\np505\nVGoudy Old Style\np506\nsVconstanz\np507\nVConstantia Bold Italic\np508\nsVPOORICH\np509\nVPoor Richard\np510\nsVPRISTINA\np511\nVPristina\np512\nsVLATINWD\np513\nVWide Latin\np514\nsVromanc__\np515\nVRomanC\np516\nsVLeelawUI\np517\nVLeelawadee UI\np518\nsVitalict_\np519\nVItalicT\np520\nsVostrich-regular\np521\nVOstrich Sans Medium\np522\nsVmonosbi\np523\nVMonospac821 BT Bold Italic\np524\nsVcambriai\np525\nVCambria Italic\np526\nsVisocp2__\np527\nVISOCP2\np528\nsVltromatic\np529\nVLetterOMatic!\np530\nsVbgothm\np531\nVBankGothic Md BT Medium\np532\nsVbgothl\np533\nVBankGothic Lt BT Light\np534\nsVSwkeys1\np535\nVSWGamekeys MT\np536\nsVCENSCBK\np537\nVCentury Schoolbook\np538\nsVgothicg_\np539\nVGothicG\np540\nsValmosnow\np541\nVAlmonte Snow\np542\nsVTangerine_Bold\np543\nVTangerine Bold\np544\nsVswisseb\np545\nVSwis721 Ex BT Bold\np546\nsVCOLONNA\np547\nVColonna MT\np548\nsVsupef___\np549\nVSuperFrench\np550\nsVTCCEB\np551\nVTw Cen MT Condensed Extra Bold\np552\nsVsylfaen\np553\nVSylfaen\np554\nsVcomicbd\np555\nVComic Sans MS Bold\np556\nsVRoland__\np557\nVRoland\np558\nsVELEPHNTI\np559\nVElephant Italic\np560\nsVmmrtext\np561\nVMyanmar Text\np562\nsVsymap___\np563\nVSymap\np564\nsVswissko\np565\nVSwis721 BlkOul BT Black\np566\nsVswissck\np567\nVSwis721 BlkCn BT Black\np568\nsVWhimsy\np569\nVWhimsy TT\np570\nsVsanssb__\np571\nVSansSerif Bold\np572\nsVtaileb\np573\nVMicrosoft Tai Le Bold\np574\nsVcomic\np575\nVComic Sans MS\np576\nsVGLSNECB\np577\nVGill Sans MT Ext Condensed Bold\np578\nsVColbert_\np579\nVColbert\np580\nsVJOKERMAN\np581\nVJokerman\np582\nsVARIALNB\np583\nVArial Narrow Bold\np584\nsVDOMIN___\np585\nVDominican\np586\nsVBRUSHSCI\np587\nVBrush Script MT Italic\np588\nsVCALLI___\np589\nVCalligraphic\np590\nsVFRADM\np591\nVFranklin Gothic Demi\np592\nsVJosefinSlab-LightItalic\np593\nVJosefin Slab Light Italic\np594\nsVsimplex_\np595\nVSimplex\np596\nsVphagspab\np597\nVMicrosoft PhagsPa Bold\np598\nsVswissek\np599\nVSwis721 BlkEx BT Black\np600\nsVscripts_\np601\nVScriptS\np602\nsVswisscl\np603\nVSwis721 LtCn BT Light\np604\nsVCASTELAR\np605\nVCastellar\np606\nsVdutchi\np607\nVDutch801 Rm BT Italic\np608\nsVnasaliza\np609\nVNasalization Medium\np610\nsVariali\np611\nVArial Italic\np612\nsVOpinehe_\np613\nVOpineHeavy\np614\nsVPLAYBILL\np615\nVPlaybill\np616\nsVROCCB___\np617\nVRockwell Condensed Bold\np618\nsVCALIST\np619\nVCalisto MT\np620\nsVCALISTB\np621\nVCalisto MT Bold\np622\nsVHATTEN\np623\nVHaettenschweiler\np624\nsVntailu\np625\nVMicrosoft New Tai Lue\np626\nsVCALISTI\np627\nVCalisto MT Italic\np628\nsVsegoeprb\np629\nVSegoe Print Bold\np630\nsVDAYTON__\np631\nVDayton\np632\nsVswissel\np633\nVSwis721 LtEx BT Light\np634\nsVmael____\np635\nVMael\np636\nsVisoct2__\np637\nVISOCT2\np638\nsVBorea___\np639\nVBorealis\np640\nsVwingding\np641\nVWingdings\np642\nsVONYX\np643\nVOnyx\np644\nsVmonosi\np645\nVMonospac821 BT Italic\np646\nsVtimesi\np647\nVTimes New Roman Italic\np648\nsVostrich-light\np649\nVOstrich Sans Condensed Light\np650\nsVseguihis\np651\nVSegoe UI Historic\np652\nsVNovem___\np653\nVNovember\np654\nsVOCRAEXT\np655\nVOCR A Extended\np656\nsVostrich-black\np657\nVOstrich Sans Black\np658\nsVnarrow\np659\nVPR Celtic Narrow Normal\np660\nsVitalic__\np661\nVItalic\np662\nsVmonosb\np663\nVMonospac821 BT Bold\np664\nsVPERB____\np665\nVPerpetua Bold\np666\nsVCreteRound-Regular\np667\nVCrete Round\np668\nsVcalibri\np669\nVCalibri\np670\nsVSCRIPTBL\np671\nVScript MT Bold\np672\nsVComfortaa-Bold\np673\nVComfortaa Bold\np674\nsVARIALN\np675\nVArial Narrow\np676\nsVHARNGTON\np677\nVHarrington\np678\nsVJosefinSlab-Bold\np679\nVJosefin Slab Bold\np680\nsVVIVALDII\np681\nVVivaldi Italic\np682\nsVhollh___\np683\nVHollywood Hills\np684\nsVBOD_R\np685\nVBodoni MT\np686\nsVSkinny__\np687\nVSkinny\np688\nsVLBRITED\np689\nVLucida Bright Demibold\np690\nsVframdit\np691\nVFranklin Gothic Medium Italic\np692\nsVsymusic_\np693\nVSymusic\np694\nsVgadugi\np695\nVGadugi\np696\nsVswissbi\np697\nVSwis721 BT Bold Italic\np698\nsVBOD_B\np699\nVBodoni MT Bold\np700\nsVERASDEMI\np701\nVEras Demi ITC\np702\nsVWaverly_\np703\nVWaverly\np704\nsVcompi\np705\nVCommercialPi BT\np706\nsVBOD_I\np707\nVBodoni MT Italic\np708\nsVconstan\np709\nVConstantia\np710\nsVARIALNBI\np711\nVArial Narrow Bold Italic\np712\nsVarialbi\np713\nVArial Bold Italic\np714\nsVJosefinSlab-Light\np715\nVJosefin Slab Light\np716\nsVBOD_CBI\np717\nVBodoni MT Condensed Bold Italic\np718\nsVwebdings\np719\nVWebdings\np720\nsVRAVIE\np721\nVRavie\np722\nsVROCC____\np723\nVRockwell Condensed\np724\nsVFELIXTI\np725\nVFelix Titling\np726\nsVRussrite\np727\nVRussel Write TT\np728\nsVisocteur\np729\nVISOCTEUR\np730\nsVLSANSD\np731\nVLucida Sans Demibold Roman\np732\nsVmalgun\np733\nVMalgun Gothic\np734\nsVheavyhea2\np735\nVHeavy Heap\np736\nsVGOUDYSTO\np737\nVGoudy Stout\np738\nsVVLADIMIR\np739\nVVladimir Script\np740\nsVARIALUNI\np741\nVArial Unicode MS\np742\nsVJosefinSlab-Thin\np743\nVJosefin Slab Thin\np744\nsVFRADMCN\np745\nVFranklin Gothic Demi Cond\np746\nsVBlackout-2am\np747\nVBlackout 2 AM\np748\nsVpalab\np749\nVPalatino Linotype Bold\np750\nsVDejaVuSansMono-Oblique\np751\nVDejaVu Sans Mono Oblique\np752\nsVANTQUABI\np753\nVBook Antiqua Bold Italic\np754\nsVswissc\np755\nVSwis721 Cn BT Roman\np756\nsVSPLASH__\np757\nVSplash\np758\nsVNIAGENG\np759\nVNiagara Engraved\np760\nsVCOPRGTB\np761\nVCopperplate Gothic Bold\np762\nsVBruss___\np763\nVBrussels\np764\nsVconsolab\np765\nVConsolas Bold\np766\nsVGOTHICBI\np767\nVCentury Gothic Bold Italic\np768\nsVmtproxy4\np769\nVProxy 4\np770\nsVmtproxy5\np771\nVProxy 5\np772\nsVromai___\np773\nVRomantic Italic\np774\nsVFRABKIT\np775\nVFranklin Gothic Book Italic\np776\nsVBELL\np777\nVBell MT\np778\nsVmtproxy1\np779\nVProxy 1\np780\nsVmtproxy2\np781\nVProxy 2\np782\nsVmtproxy3\np783\nVProxy 3\np784\nsVLCALLIG\np785\nVLucida Calligraphy Italic\np786\nsVphagspa\np787\nVMicrosoft PhagsPa\np788\nsVANTQUAI\np789\nVBook Antiqua Italic\np790\nsVmtproxy8\np791\nVProxy 8\np792\nsVmtproxy9\np793\nVProxy 9\np794\nsVLato-Bold\np795\nVLato Bold\np796\nsVtxt_____\np797\nVTxt\np798\nsVconstanb\np799\nVConstantia Bold\np800\nsVERASBD\np801\nVEras Bold ITC\np802\nsVLato-LightItalic\np803\nVLato Light Italic\np804\nsVRONDALO_\np805\nVRondalo\np806\nsVconstani\np807\nVConstantia Italic\np808\nsVBRLNSB\np809\nVBerlin Sans FB Bold\np810\nsVgeorgiaz\np811\nVGeorgia Bold Italic\np812\nsVgothice_\np813\nVGothicE\np814\nsVcalibriz\np815\nVCalibri Bold Italic\np816\nsVgeorgiab\np817\nVGeorgia Bold\np818\nsVLeelaUIb\np819\nVLeelawadee UI Bold\np820\nsVtimesbi\np821\nVTimes New Roman Bold Italic\np822\nsVPERI____\np823\nVPerpetua Italic\np824\nsVromab___\np825\nVRomantic Bold\np826\nsVBRLNSR\np827\nVBerlin Sans FB\np828\nsVBELLB\np829\nVBell MT Bold\np830\nsVgeorgiai\np831\nVGeorgia Italic\np832\nsVNirmalaS\np833\nVNirmala UI Semilight\np834\nsVdutchb\np835\nVDutch801 Rm BT Bold\np836\nsVdigifit\np837\nVDigifit Normal\np838\nsVROCKEB\np839\nVRockwell Extra Bold\np840\nsVgdt_____\np841\nVGDT\np842\nsVmonbaiti\np843\nVMongolian Baiti\np844\nsVsegoescb\np845\nVSegoe Script Bold\np846\nsVsymath__\np847\nVSymath\np848\nsVisoct___\np849\nVISOCT\np850\nsVTarzan__\np851\nVTarzan\np852\nsVsnowdrft\np853\nVSnowdrift\np854\nsVHTOWERTI\np855\nVHigh Tower Text Italic\np856\nsVCENTURY\np857\nVCentury\np858\nsVmalgunsl\np859\nVMalgun Gothic Semilight\np860\nsVseguibl\np861\nVSegoe UI Black\np862\nsVCreteRound-Italic\np863\nVCrete Round Italic\np864\nsVAlfredo_\np865\nVAlfredo\np866\nsVCOMMONS_\np867\nVCommons\np868\nsVLFAX\np869\nVLucida Fax\np870\nsVLBRITEI\np871\nVLucida Bright Italic\np872\nsVFRAHV\np873\nVFranklin Gothic Heavy\np874\nsVisocteui\np875\nVISOCTEUR Italic\np876\nsVManorly_\np877\nVManorly\np878\nsVBolstbo_\np879\nVBolsterBold Bold\np880\nsVsegoeui\np881\nVSegoe UI\np882\nsVNunito-Light\np883\nVNunito Light\np884\nsVIMPRISHA\np885\nVImprint MT Shadow\np886\nsVgeorgia\np887\nVGeorgia\np888\nsV18cents\np889\nV18thCentury\np890\nsVMOONB___\np891\nVMoonbeam\np892\nsVPER_____\np893\nVPerpetua\np894\nsVHansen__\np895\nVHansen\np896\nsVLato-Regular\np897\nVLato\np898\nsVBOUTON_International_symbols\np899\nVBOUTON International Symbols\np900\nsVCOOPBL\np901\nVCooper Black\np902\nsVmonos\np903\nVMonospac821 BT Roman\np904\nsVtahoma\np905\nVTahoma\np906\nsVcityb___\np907\nVCityBlueprint\np908\nsVswisscbi\np909\nVSwis721 Cn BT Bold Italic\np910\nsVEnliven_\np911\nVEnliven\np912\nsVLeelUIsl\np913\nVLeelawadee UI Semilight\np914\nsVCALIFR\np915\nVCalifornian FB\np916\nsVumath\np917\nVUniversalMath1 BT\np918\nsVswisscbo\np919\nVSwis721 BdCnOul BT Bold Outline\np920\nsVcomplex_\np921\nVComplex\np922\nsVBOOKOSB\np923\nVBookman Old Style Bold\np924\nsVMartina_\np925\nVMartina\np926\nsVromans__\np927\nVRomanS\np928\nsVmvboli\np929\nVMV Boli\np930\nsVCALIFI\np931\nVCalifornian FB Italic\np932\nsVGARABD\np933\nVGaramond Bold\np934\nsVebrima\np935\nVEbrima\np936\nsVTEMPSITC\np937\nVTempus Sans ITC\np938\nsVCALIFB\np939\nVCalifornian FB Bold\np940\nsVitalicc_\np941\nVItalicC\np942\nsVisocp3__\np943\nVISOCP3\np944\nsVscriptc_\np945\nVScriptC\np946\nsValiee13\np947\nVAlien Encounters\np948\nsVnobile_italic\np949\nVNobile Italic\np950\nsVGARAIT\np951\nVGaramond Italic\np952\nsVswissli\np953\nVSwis721 Lt BT Light Italic\np954\nsVCabinSketch-Bold\np955\nVCabinSketch Bold\np956\nsVcorbel\np957\nVCorbel\np958\nsVseguisbi\np959\nVSegoe UI Semibold Italic\np960\nsVSCHLBKBI\np961\nVCentury Schoolbook Bold Italic\np962\nsVasimov\np963\nVAsimov\np964\nsVLFAXDI\np965\nVLucida Fax Demibold Italic\np966\nsVBRADHITC\np967\nVBradley Hand ITC\np968\nsVswisscki\np969\nVSwis721 BlkCn BT Black Italic\np970\nsVGILSANUB\np971\nVGill Sans Ultra Bold\np972\nsVHARLOWSI\np973\nVHarlow Solid Italic Italic\np974\nsVHARVEIT_\np975\nVHarvestItal\np976\nsVcambriab\np977\nVCambria Bold\np978\nsVswissci\np979\nVSwis721 Cn BT Italic\np980\nsVcounb___\np981\nVCountryBlueprint\np982\nsVNotram__\np983\nVNotram\np984\nsVPENULLI_\np985\nVPenultimateLight\np986\nsVtahomabd\np987\nVTahoma Bold\np988\nsVMISTRAL\np989\nVMistral\np990\nsVpala\np991\nVPalatino Linotype\np992\nsVOLDENGL\np993\nVOld English Text MT\np994\nsVinductio\np995\nVInduction Normal\np996\nsVJosefinSlab-SemiBoldItalic\np997\nVJosefin Slab SemiBold Italic\np998\nsVMinerva_\np999\nVMinerva\np1000\nsVsymbol\np1001\nVSymbol\np1002\nsVcambriaz\np1003\nVCambria Bold Italic\np1004\nsVtrebucbi\np1005\nVTrebuchet MS Bold Italic\np1006\nsVtimes\np1007\nVTimes New Roman\np1008\nsVERASLGHT\np1009\nVEras Light ITC\np1010\nsVSteppes\np1011\nVSteppes TT\np1012\nsVREFSPCL\np1013\nVMS Reference Specialty\np1014\nsVPARCHM\np1015\nVParchment\np1016\nsVDejaVuSansMono-Bold\np1017\nVDejaVu Sans Mono Bold\np1018\nsVswisscli\np1019\nVSwis721 LtCn BT Light Italic\np1020\nsVLSANS\np1021\nVLucida Sans\np1022\nsVPhrasme_\np1023\nVPhrasticMedium\np1024\nsVDejaVuSansMono-BoldOblique\np1025\nVDejaVu Sans Mono Bold Oblique\np1026\nsVarialbd\np1027\nVArial Bold\np1028\nsVSNAP____\np1029\nVSnap ITC\np1030\nsVArchitectsDaughter\np1031\nVArchitects Daughter\np1032\nsVCorpo___\np1033\nVCorporate\np1034\nsVeurro___\np1035\nVEuroRoman Oblique\np1036\nsVimpact\np1037\nVImpact\np1038\nsVlittlelo\np1039\nVLittleLordFontleroy\np1040\nsVsimsunb\np1041\nVSimSun-ExtB\np1042\nsVARIALNI\np1043\nVArial Narrow Italic\np1044\nsVdutchbi\np1045\nVDutch801 Rm BT Bold Italic\np1046\nsVcalibrii\np1047\nVCalibri Italic\np1048\nsVDeneane_\np1049\nVDeneane\np1050\nsVFRADMIT\np1051\nVFranklin Gothic Demi Italic\np1052\nsVANTQUAB\np1053\nVBook Antiqua Bold\np1054\nsVcalibril\np1055\nVCalibri Light\np1056\nsVisocpeui\np1057\nVISOCPEUR Italic\np1058\nsVpanroman\np1059\nVPanRoman\np1060\nsVMelodbo_\np1061\nVMelodBold Bold\np1062\nsVcalibrib\np1063\nVCalibri Bold\np1064\nsVdistant galaxy 2\np1065\nVDistant Galaxy\np1066\nsVPacifico\np1067\nVPacifico\np1068\nsVnobile_bold_italic\np1069\nVNobile Bold Italic\np1070\nsVmsyi\np1071\nVMicrosoft Yi Baiti\np1072\nsVBOD_PSTC\np1073\nVBodoni MT Poster Compressed\np1074\nsVLSANSI\np1075\nVLucida Sans Italic\np1076\nsVcreerg__\np1077\nVCreepygirl\np1078\nsVsegoeuisl\np1079\nVSegoe UI Semilight\np1080\nsVvinet\np1081\nVVineta BT\np1082\nsVisocpeur\np1083\nVISOCPEUR\np1084\nsVtechl___\np1085\nVTechnicLite\np1086\nsVswissb\np1087\nVSwis721 BT Bold\np1088\nsVCLARE___\np1089\nVClarendon\np1090\nsVdutch\np1091\nVDutch801 Rm BT Roman\np1092\nsVLBRITEDI\np1093\nVLucida Bright Demibold Italic\np1094\nsVswisse\np1095\nVSwis721 Ex BT Roman\np1096\nsVswissk\np1097\nVSwis721 Blk BT Black\np1098\nsVswissi\np1099\nVSwis721 BT Italic\np1100\nsVfingerpop2\np1101\nVFingerpop\np1102\nsVswissl\np1103\nVSwis721 Lt BT Light\np1104\nsVBAUHS93\np1105\nVBauhaus 93\np1106\nsVVivian__\np1107\nVVivian\np1108\nsVgreeks__\np1109\nVGreekS\np1110\nsVGOUDOSI\np1111\nVGoudy Old Style Italic\np1112\nsVBOD_BI\np1113\nVBodoni MT Bold Italic\np1114\nsVLHANDW\np1115\nVLucida Handwriting Italic\np1116\nsVITCKRIST\np1117\nVKristen ITC\np1118\nsVBALTH___\np1119\nVBalthazar\np1120\nsVFORTE\np1121\nVForte\np1122\nsVJosefinSlab-Regular\np1123\nVJosefin Slab\np1124\nsVROCKI\np1125\nVRockwell Italic\np1126\nsVGOUDOSB\np1127\nVGoudy Old Style Bold\np1128\nsVLEELAWAD\np1129\nVLeelawadee\np1130\nsVLEELAWDB\np1131\nVLeelawadee Bold\np1132\nsVmarlett_0\np1133\nVMarlett\np1134\nsVmplus-1m-bold\np1135\nVM+ 1m bold\np1136\nsVmplus-1m-light\np1137\nVM+ 1m light\np1138\nsVmplus-1m-medium\np1139\nVM+ 1m medium\np1140\nsVmplus-1m-regular\np1141\nVM+ 1m\np1142\nsVmplus-1m-thin\np1143\nVM+ 1m thin\np1144\nsVMSUIGHUB\np1145\nVMicrosoft Uighur Bold\np1146\nsVMSUIGHUR\np1147\nVMicrosoft Uighur\np1148\nsVSamsungIF_Md_0\np1149\nVSamsung InterFace Medium\np1150\nsVSamsungIF_Rg_0\np1151\nVSamsung InterFace\np1152\nsVbahnschrift\np1153\nVBahnschrift\np1154\nsVBowlbyOneSC-Regular\np1155\nVBowlby One SC\np1156\nsVCabinSketch-Regular\np1157\nVCabin Sketch\np1158\nsVCookie-Regular\np1159\nVCookie\np1160\nsVCourgette-Regular\np1161\nVCourgette\np1162\nsVdead\np1163\nVDead Kansas\np1164\nsVDoppioOne-Regular\np1165\nVDoppio One\np1166\nsVeuphorig\np1167\nVEuphorigenic\np1168\nsVGreatVibes-Regular\np1169\nVGreat Vibes\np1170\nsVKalam-Bold\np1171\nVKalam Bold\np1172\nsVKalam-Light\np1173\nVKalam Light\np1174\nsVKalam-Regular\np1175\nVKalam\np1176\nsVLemon-Regular\np1177\nVLemon\np1178\nsVLimelight-Regular\np1179\nVLimelight\np1180\nsVMegrim\np1181\nVMegrim Medium\np1182\nsVMontserratSubrayada-Bold\np1183\nVMontserrat Subrayada Bold\np1184\nsVNotoSans-Regular\np1185\nVNoto Sans\np1186\nsVRussoOne-Regular\np1187\nVRusso One\np1188\nsVSigmarOne-Regular\np1189\nVSigmar One\np1190\nsVYellowtail-Regular\np1191\nVYellowtail\np1192\ns."  # NOQA
        )
    return _std_fonts.cached


def fonts():
    if PythonInExcel or AnacondaCode:
        return []
    if not hasattr(fonts, "font_list"):
        fonts.font_list = []
        if Pythonista:
            UIFont = objc_util.ObjCClass("UIFont")
            for family in UIFont.familyNames():
                family = str(family)
                try:
                    ImageFont.truetype(family)
                    fonts.font_list.append(((family,), family))
                except Exception:
                    pass

                for name in UIFont.fontNamesForFamilyName_(family):
                    name = str(name)
                    fonts.font_list.append(((name,), name))

        salabim_dir = Path(__file__).parent
        cur_dir = Path.cwd()
        dir_recursives = [(salabim_dir, False)]
        if cur_dir != salabim_dir:
            dir_recursives.append((cur_dir, False))
        if Windows:
            dir_recursives.append((Path("c:/windows/fonts"), True))
            if os.getenv("LOCALAPPDATA"):
                dir_recursives.append((Path(os.getenv("LOCALAPPDATA")) / "Microsoft" / "Windows" / "Fonts", True))

        else:
            dir_recursives.append((Path("/usr/share/fonts"), True))  # for linux
            dir_recursives.append((Path("/system/fonts"), True))  # for android

        for dir, recursive in dir_recursives:
            for file_path in dir.glob("**/*.*" if recursive else "*.*"):
                if file_path.suffix.lower() == ".ttf":
                    file = str(file_path)
                    fn = os.path.basename(file).split(".")[0]
                    if "_std_fonts" in globals() and fn in _std_fonts():  # test for availabiitly, because of minimized version
                        fullname = _std_fonts()[fn]
                    else:
                        try:
                            f = ImageFont.truetype(file, 12)
                        except OSError:  # to avoid PyDroid problems
                            continue
                        if f is None:
                            fullname = ""
                        else:
                            if str(f.font.style).lower() == "regular":
                                fullname = str(f.font.family)
                            else:
                                fullname = str(f.font.family) + " " + str(f.font.style)
                    if fullname != "":
                        if fn.lower() == fullname.lower():
                            fonts.font_list.append(((fullname,), file))
                        else:
                            fonts.font_list.append(((fn, fullname), file))
    return fonts.font_list


def standardfonts():
    return {"": "Calibri", "std": "Calibri", "mono": "DejaVuSansMono", "narrow": "mplus-1m-regular"}


def fallback_font(name, size):
    if name == "dejavusansmono":
        s = (
            " "  # NOQA
            " "  # NOQA
        )
        # this is dejavusans

    elif name == "mplus_1m_regular":
        s = " "  # NOQA
        # this is mplus_1m_regular
    else:
        s = (
            " "  # NOQA
            " "  # NOQA
            " "  # NOQA
            " "  # NOQA
            " "  # NOQA
        )
        # this is calibri

    return ImageFont.truetype(io.BytesIO(base64.b64decode(s.encode("ascii"))), size)


def getfont(fontname, fontsize):
    # fontsize in screen_coordinates!
    if hasattr(getfont, "lookup"):
        if (fontname, fontsize) in getfont.lookup:
            return getfont.lookup[(fontname, fontsize)]
    else:
        getfont.lookup = {}
    if isinstance(fontname, str):
        fontlist1 = [fontname]
    else:
        fontlist1 = list(fontname)

    fontlist = [standardfonts().get(f.lower(), f) for f in fontlist1]
    if Pythonista:
        fontlist = ["arial" if "calibri" in f.lower() else f for f in fontlist]

    result = None

    for ifont in fontlist:
        try:
            result = ImageFont.truetype(font=ifont, size=int(fontsize))
            break
        except Exception:
            pass

        filename = ""
        for fns, ifilename in fonts():
            for fn in fns:
                if normalize(fn) == normalize(ifont):
                    filename = ifilename
                    break
            if filename != "":
                break
        if filename != "":
            try:
                if Pythonista:
                    result = ImageFont.truetype(filename, size=int(fontsize))
                else:
                    #  refer to https://github.com/python-pillow/Pillow/issues/3730 for explanation (in order to load >= 500 fonts)
                    with open(filename, "rb") as f:
                        result = ImageFont.truetype(font=io.BytesIO(f.read()), size=int(fontsize))
                break
            except Exception:
                raise

    if result is None:
        if Pythonista:
            result = ImageFont.truetype("Arial", size=int(fontsize))  # fallback to arial, because Pythonista does not support file handles in ImageFont
        else:
            result = fallback_font(normalize(fontlist[0]), size=int(fontsize))

    heightA = result.getbbox("A")[3]
    getfont.lookup[(fontname, fontsize)] = result, heightA
    return result, heightA


def show_fonts() -> None:
    """
    show (print) all available fonts on this machine
    """
    can_animate(try_only=False)
    fontnames = []
    for fns, ifilename in fonts():
        for fn in fns:
            fontnames.append(fn)
    fontnames.extend(list(standardfonts()))
    last = ""
    for font in sorted(fontnames, key=normalize):
        if font != last:  # remove duplicates
            print(font)
            last = font


def show_colornames() -> None:
    """
    show (print) all available color names and their value.
    """
    names = sorted(colornames())
    for name in names:
        print(f"{name:22s}{colornames()[name]}")


def arrow_polygon(size: float) -> Tuple:
    """
    creates a polygon tuple with a centered arrow for use with sim.Animate

    Parameters
    ----------
    size : float
        length of the arrow
    """
    size /= 4
    return (-2 * size, -size, 0, -size, 0, -2 * size, 2 * size, 0, 0, 2 * size, 0, size, -2 * size, size)


def centered_rectangle(width: float, height: float) -> Tuple:
    """
    creates a rectangle tuple with a centered rectangle for use with sim.Animate

    Parameters
    ----------
    width : float
        width of the rectangle

    height : float
        height of the rectangle
    """
    return -width / 2, -height / 2, width / 2, height / 2


def regular_polygon(radius: float = 1, number_of_sides: int = 3, initial_angle: float = 0) -> List:
    """
    creates a polygon tuple with a regular polygon (within a circle) for use with sim.Animate

    Parameters
    ----------
    radius : float
        radius of the corner points of the polygon

        default : 1

    number_of_sides : int
        number of sides (corners)

        must be >= 3

        default : 3

    initial_angle : float
        angle of the first corner point, relative to the origin

        default : 0
    """
    number_of_sides = int(number_of_sides)
    if number_of_sides < 3:
        raise ValueError("number of sides < 3")
    tangle = 2 * math.pi / number_of_sides
    sint = math.sin(tangle)
    cost = math.cos(tangle)
    p = []
    x = radius * math.cos(math.radians(initial_angle))
    y = radius * math.sin(math.radians(initial_angle))

    for i in range(number_of_sides):
        x, y = (x * cost - y * sint, x * sint + y * cost)
        p.append(x + radius)
        p.append(y + radius)

    return p


def can_animate(try_only: bool = True) -> bool:
    """
    Tests whether animation is supported.

    Parameters
    ----------
    try_only : bool
        if True (default), the function does not raise an error when the required modules cannot be imported

        if False, the function will only return if the required modules could be imported.

    Returns
    -------
    True, if required modules could be imported, False otherwise : bool
    """
    global Image
    global ImageDraw
    global ImageFont
    global GifImagePlugin
    global ImageGrab
    global ImageTk
    global tkinter
    try:
        import PIL  # NOQA
        from PIL import Image
        from PIL import ImageDraw
        from PIL import ImageFont

        #        from PIL import GifImagePlugin

        try:
            from PIL import ImageGrab
        except ImportError:
            ImageGrab = None

        if not Pythonista and not (PythonInExcel or AnacondaCode):
            from PIL import ImageTk
    except ImportError:
        if try_only:
            return False
        raise ImportError("PIL is required for animation. Install with pip install Pillow or see salabim manual")

    g.dummy_image = Image.new("RGBA", (1, 1), (0, 0, 0, 0))

    if not Pythonista and not (PythonInExcel or AnacondaCode):
        if PyDroid:
            if not g.tkinter_loaded:
                if try_only:
                    return False
                raise ImportError("PyDroid animation requires that the main program imports tkinter")
        try:
            import tkinter
            import tkinter.font

        except ImportError:
            try:
                import Tkinter as tkinter  # type: ignore

            except ImportError:
                if try_only:
                    return False
                raise ImportError("tkinter is required for animation")

    return True


def can_animate3d(try_only: bool = True) -> bool:
    """
    Tests whether 3d animation is supported.

    Parameters
    ----------
    try_only : bool
        if True (default), the function does not raise an error when the required modules cannot be imported

        if False, the function will only return if the required modules could be imported.

    Returns
    -------
    True, if required modules were imported, False otherwise : bool
    """
    global gl
    global glu
    global glut

    if can_animate(try_only=True):
        try:
            import OpenGL.GL as gl
            import OpenGL.GLU as glu
            import OpenGL.GLUT as glut
            import OpenGL
        except ImportError:
            if try_only:
                return False
            else:
                raise ImportError("OpenGL is required for animation3d. Install with pip install PyOpenGL or see salabim manual")
        try:
            glut.glutInit()
        except OpenGL.error.NullFunctionError:
            raise ImportError("Installed OpenGL does not support glut. Try 'pip install OpenGL-glut' or see the salabim documentation")
    
        return True
    else:
        if try_only:
            return False
        else:
            raise ImportError("cannot animate, let alone animate3d")


def can_video(try_only: bool = True) -> bool:
    """
    Tests whether video is supported.

    Parameters
    ----------
    try_only : bool
        if True (default), the function does not raise an error when the required modules cannot be imported

        if False, the function will only return if the required modules could be imported.

    Returns
    -------
    True, if required modules could be imported, False otherwise : bool
    """
    global cv2
    global numpy
    if Pythonista:
        if try_only:
            return False
        raise NotImplementedError("video production not supported on Pythonista")
    else:
        try:
            import cv2
            import numpy
        except ImportError:
            if try_only:
                return False
            if PyPy:
                raise NotImplementedError("video production is not supported under PyPy.")
            else:
                raise ImportError("cv2 required for video production. Install with pip install opencv-python")
    return True


def has_numpy() -> bool:
    """
    Tests whether numpy is installed. If so, the global numpy is set accordingly.
    If not, the global numpy is set to False.

    Returns
    -------
    True, if numpy is installed. False otherwise.
    """
    global numpy
    try:
        return bool(numpy)
    except NameError:
        try:
            import numpy

            return True
        except ModuleNotFoundError:
            numpy = False
            return False


def video_duration(spec: Union["PIL.Image.Image", str]) -> float:
    """
    returns the duration of an image specification

    Parameters
    ----------
    spec : str or PIL.Image.Image

    Returns
    -------
    Length of file/URL specified : float

    If not animated, 0 will be returned
    """
    image_container = ImageContainer(spec)
    return image_container._duration


class ImageContainer:
    """
    based on https://gist.github.com/BigglesZX/4016539
    """

    def __new__(cls, spec):
        # specs that are PIL images are excempt from the cache, because they can't be hashed
        if isinstance(spec, (str, Path)):
            if spec in g.image_container_cache:
                return g.image_container_cache[spec]
        elif not isinstance(spec, Image.Image):
            raise TypeError("spec should be str, Path or Pillow Image")

        self = super().__new__(cls)
        self.init(spec)
        if isinstance(spec, (str, Path)):
            g.image_container_cache[spec] = self

        return self

    def init(self, spec):
        can_animate(try_only=True)  # to load PIL

        if isinstance(spec, (str, Path)):
            spec = str(spec)  # to process Path correctly

            if spec == "":
                im = Image.new("RGBA", (1, 1), (0, 0, 0, 0))
                # (0, 0) raises an error on some platforms
            else:
                if Path(spec).suffix.lower() == ".heic":
                    if Pythonista:
                        raise ImportError(".heic files not supported under Pythonista.")
                    try:
                        from pillow_heif import register_heif_opener  # type: ignore
                    except ImportError:
                        raise ImportError("pillow_heif is required for reading .heic files. Install with pip install pillow_heif")
                    register_heif_opener()
                if spec.count("//") == 1 and spec.count("///") == 0:
                    try:
                        im = Image.open(spec)  # try a file with one //
                    except Exception:
                        try:
                            im = Image.open(io.BytesIO(urllib.request.urlopen(spec).read()))
                        except Exception as e:
                            raise FileNotFoundError(f"could not open URL {spec}: {e}")
                else:
                    if "|" in spec:
                        zip_part, image_part = spec.split("|")
                        zip_part = zip_part.strip()
                        if zip_part.lower().endswith(".zip"):
                            image_part = image_part.strip()
                            with zipfile.ZipFile(zip_part) as ziphandle:
                                image_data = ziphandle.read(image_part)
                                im = Image.open(io.BytesIO(image_data))
                        else:
                            im = Image.open(spec)
                    else:
                        im = Image.open(spec)
        else:  # it's a PIL Image, for sure
            im = spec

        try:
            n_frames = im.n_frames
        except AttributeError:
            n_frames = 1

        if n_frames == 1:
            self.images = [im.convert("RGBA")]
            self._duration = 0

        else:  # >1
            im.convert("RGBA")  # without this .webp does not work!
            end_time = im.info["duration"] / 1000
            self.images = [im]
            self.images[-1].end_time = end_time

            while True:
                try:
                    im.seek(im.tell() + 1)
                    end_time += im.info["duration"] / 1000
                    self.images.append(im.convert("RGBA"))
                    self.images[-1].end_time = end_time
                except EOFError:
                    break
            self._duration = end_time

            self.images[-1].end_time = inf  # so we can't accidently have a problem at the end

    def duration(self):
        return self._duration

    def get_image(self, t, repeat, pingpong, t_from, t_to):
        # id represents the image sequence number
        if len(self.images) == 1:
            return self.images[0], 0
        if t < 0:
            return g.dummy_image, -1
        if t_to == inf:
            t_to = self._duration

        if not (0 <= t_from < t_to):
            raise ValueError(f"animation_from={t_from} not with 0 and animation_to={t_to}")
        if t_to > self._duration:
            raise ValueError(f"animation_to={t_to} > duration={self._duration}")
        if pingpong:
            interval = 2 * (t_to - t_from)
        else:
            interval = t_to - t_from

        if not repeat:
            if t > interval:
                return g.dummy_image, 0
        t = t % interval
        if pingpong:
            if t <= interval / 2:
                t = t_from + t
            else:
                t = t_from + interval - t
        else:
            t = t_from + t

        for id, frame in enumerate(self.images):
            if t <= frame.end_time:
                return frame, id


def default_env() -> "Environment":
    """
    Returns
    -------
    default environment : Environment
    """
    return g.default_env


@contextlib.contextmanager
def over3d(val: bool = True):
    """
    context manager to change temporarily default_over3d

    Parameters
    ----------
    val : bool
        temporary value of default_over3d

        default: True

    Notes
    -----
    Use as ::

        with over3d():
            an = AnimateText('test')
    """
    save_default_over3d = default_over3d()
    default_over3d(val)
    yield
    default_over3d(save_default_over3d)


_default_over3d = False


def default_over3d(val: bool = None):
    """
    Set default_over3d

    Parameters
    ----------
    val : bool
        if not None, set the default_over3d to val

    Returns
    -------
    Current (new) value of default_over3d
    """
    global _default_over3d
    if val is not None:
        _default_over3d = val
    return _default_over3d


@contextlib.contextmanager
def cap_now(val: bool = True):
    """
    context manager to change temporarily default_cap_now

    Parameters
    ----------
    val : bool
        temporary value of default_cap_now

        default: True

    Notes
    -----
    Use as ::

        with cap_now():
            an = AnimateText('test')
    """
    save_default_cap_now = default_cap_now()
    default_cap_now(val)
    yield
    default_cap_now(save_default_cap_now)


def default_cap_now(val: bool = None) -> bool:
    """
    Set default_cap_now

    Parameters
    ----------
    val : bool
        if not None, set the default_cap_now to val

    Returns
    -------
    Current (new) value of default_cap_now
    """
    if val is not None:
        g._default_cap_now = val
    return g._default_cap_now


def reset() -> None:
    """
    resets global variables

    used internally at import of salabim

    might be useful for REPLs or for Pythonista
    """
    try:
        g.default_env.video_close()
    except Exception:
        pass

    try:
        g.animation_env.root.destroy()
    except Exception:
        pass
    g.default_env = None
    g.animation_env = None
    g.animation_scene = None
    g.in_draw = False
    g.tkinter_loaded = "?"
    g.image_container_cache = {}
    g._default_cap_now = False
    g._captured_stdout = []

    random_seed()  # always start with seed 1234567


def _set_env(env):
    """
    returns g._default_env if env is None
    returns env, otherwise

    will raise a ValueError if g.default_env is None (nit initialized)
    """

    if env is None:
        if g.default_env is None:
            raise ValueError("no default environment. Did yout forget to call sim.Environment()?")
        return g.default_env
    return env


App = Environment


def set_environment_aliases():
    cwd_parts = Path.cwd().parts
    if len(cwd_parts) >= 2 and cwd_parts[-2] == "salabim" and cwd_parts[-1] in ("manual", "manual_yield"):
        return  # do not set when using Sphinx build!

    for name, obj in list(globals().items()):
        if (not name.startswith("_") or name in ("_Trajectory", "_Distribution")) and name != "yieldless" and name != "Environment":
            if inspect.isclass(obj) and obj.__module__ == Environment.__module__:
                if issubclass(obj, Exception):
                    exec(f"Environment.{name}={name}")
                else:
                    try:
                        sig = inspect.signature(obj)
                    except ValueError:  # Python 3.10 special
                        sig = None
                    if sig is not None and "env" in sig.parameters:
                        sig1 = "self," + ",".join(map(str, sig.parameters.values()))
                        sig2 = ",".join(str(v) if str(v.kind).startswith("VAR") else k for k, v in sig.parameters.items())
                        s = f"""\
def {name}({sig1}):
    if env is None:
        env=self
    return {name}({sig2})
Environment.{name}={name}
Environment.{name}.__doc__ = {name}.__doc__"""
                        exec(s)
                    else:
                        exec(f"Environment.{name}={name}")

            elif inspect.isfunction(obj):
                exec(f"Environment.{name}=staticmethod({name})")
            else:
                exec(f"Environment.{name}={name}")


def clear_captured_stdout():
    """
    empties the captured stdout
    """
    g._captured_stdout.clear()


def captured_stdout_as_str() -> str:
    """
    returns the captured stdout as a str

    Returns
    -------
    captured stdout : str
    """

    return "".join(g._captured_stdout)


def captured_stdout_as_file(file: Union[str, Path, "file"], mode: str = None):
    """
    puts the captured stdout in a file

    Parameters
    ----------
    file : str, Path or file handle
        specifies where to store the captured stdout

    mode : str
        write mode ("a" or "w" (default))

        may not be specified for file handles

    Note
    ----
    If file is a file handle, the file will not be closed.
    """
    if isinstance(file, (str, bytes, Path)):
        if mode is None:
            mode = "w"
        else:
            if mode not in ("a", "w"):
                raise ValueError(f"mode should be w or a, not {mode}")
        with open(file, mode) as f:
            f.write(captured_stdout_as_str())
    else:
        if mode is not None:
            raise ValueError(f"mode may not be specified. so not {mode}")
        file.write(captured_stdout_as_str())


def captured_stdout_as_list(column_name: str = None) -> list:
    """
    returns the captured stdout as a list of strings

    Returns
    -------
    captured stdout : list
        each line is an element of the list
    """

    return captured_stdout_as_str().splitlines()


class capture_stdout:
    """
    specifies how to capture stdout

    Parameters
    ----------
    include_print : bool
        if True (default), the output is also printed out as normal

        if False, no output is printed

    Note
    ----
    This function is normally used as a context manager, like ::

        with capture_stdout():
            ...
    """

    def __init__(self, include_print: bool = True):
        self.stdout = sys.stdout
        self.include_print = include_print

    def __enter__(self):
        sys.stdout = self

    def __exit__(self, exc_type, exc_value, tb):
        sys.stdout = self.stdout

    def write(self, data):
        g._captured_stdout.append(data)
        if self.include_print:
            self.stdout.write(data)

    def flush(self):
        if self.include_print:
            self.stdout.flush()
        g._captured_stdout.append("\n")


class redirect_stdout:
    """
    specifies how to capture stdout

    Parameters
    ----------
    include_print : bool
        if True (default), the output is also printed out as normal

        if False, no output is printed

    mode : str
        write mode ("a" or "w" (default))

        may not be specified for file handles

    Note
    ----
    This function is normally used as a context manager, like ::

        with redirect_stdout():
            ...

    If file is a file handle, the file will not be closed.
    """

    def __init__(self, file: Union[str, Path, "file"] = None, mode: str = None, include_print: bool = True):
        self.stdout = sys.stdout
        self.include_print = include_print
        if isinstance(file, (str, bytes, Path)):
            if mode is None:
                mode = "w"
            else:
                if mode not in ("a", "w"):
                    raise ValueError(f"mode should be w or a, not {mode}")
            self.file = open(file, mode)
        else:
            if mode is not None:
                raise ValueError(f"mode may not be specified. so not {mode}")
            self.file = file
        self.mode = mode
        sys.stdout = self

    def __enter__(self): ...

    def close(self):
        self.flush()
        if self.mode:  # it is not a file handle
            self.file.close()
        sys.stdout = self.stdout

    def __exit__(self, exc_type, exc_value, tb):
        self.close()

    def write(self, data):
        global _captured_stdout
        self.file.write(data)
        if self.include_print:
            self.stdout.write(data)

    def flush(self):
        self.file.flush()
        if self.include_print:
            self.stdout.flush()


reset()

set_environment_aliases()

if __name__ == "__main__":
    sys.path.insert(0, str(Path(__file__).parent / ".." / "misc"))
    try:
        import salabim_exp
    except Exception as e:
        print("salabim_exp.py not found or ?")
        raise e

    try:
        salabim_exp.__dict__["exp"]
    except KeyError:
        print("salabim_exp.exp() not found")
        quit()

    salabim_exp.exp()
