import atexit
import logging
import re
import threading
from typing import Set

from uqbar.objects import new

import supriya.exceptions
from supriya.commands import (
    FailResponse,
    GroupQueryTreeRequest,
    NotifyRequest,
    QueryTreeGroup,
    QueryTreeSynth,
    QuitRequest,
    SyncRequest,
)
from supriya.enums import NodeAction
from supriya.realtime import BlockAllocator, BootOptions, NodeIdAllocator
from supriya.system import PubSub

# TODO: Implement connect() and disconnect()
# TODO: Handle clientID return via [/done /notify 0 64] for allocators


logger = logging.getLogger("supriya.server")


class Server:
    """
    An scsynth server proxy.

    ::

        >>> import supriya.realtime
        >>> server = supriya.realtime.Server.default()
        >>> server.boot()
        <Server: udp://127.0.0.1:57751, 8i8o>

    ::

        >>> server.quit()
        <Server: offline>

    """

    ### CLASS VARIABLES ###

    __documentation_section__ = "Main Classes"

    _default_server = None

    _servers: Set["Server"] = set()

    ### CONSTRUCTOR ###

    """
    def __new__(cls, ip_address="127.0.0.1", port=57751, **kwargs):
        key = (ip_address, port)
        if key not in cls._servers:
            instance = object.__new__(cls)
            instance.__init__(ip_address=ip_address, port=port, **kwargs)
            cls._servers[key] = instance
        return cls._servers[key]
    """

    ### INITIALIZER ###

    def __init__(self, ip_address="127.0.0.1", port=57751):
        import supriya.osc
        import supriya.realtime

        type(self)._servers.add(self)

        ### NET ADDRESS ###

        self._ip_address = ip_address
        self._port = port

        ### OSC MESSAGING ###

        self._latency = 0.1
        self._lock = threading.Lock()
        self._osc_io = supriya.osc.OscIO()

        ### ALLOCATORS ###

        self._audio_bus_allocator = None
        self._buffer_allocator = None
        self._control_bus_allocator = None
        self._node_id_allocator = None
        self._sync_id = 0

        ### SERVER PROCESS ###

        self._client_id = None
        self._maximum_logins = None
        self._is_owner = False
        self._is_running = False
        self._options = BootOptions()
        self._server_process = None
        self._status = None
        self._status_watcher = None

        ### PROXIES ###

        self._audio_input_bus_group = None
        self._audio_output_bus_group = None
        self._default_group = None
        self._root_node = None
        self._meters = supriya.realtime.ServerMeters(self)
        self._recorder = supriya.realtime.ServerRecorder(self)

        ### PROXY MAPPINGS ###

        self._audio_buses = {}
        self._buffer_proxies = {}
        self._buffers = {}
        self._control_bus_proxies = {}
        self._control_buses = {}
        self._nodes = {}
        self._synthdefs = {}

        ### DEBUG ###

        self.debug_request_names = False

        ### REGISTER WITH ATEXIT ###

        atexit.register(self._shutdown)

    ### SPECIAL METHODS ###

    def __contains__(self, expr):
        import supriya.realtime
        import supriya.synthdefs

        if isinstance(expr, supriya.realtime.Node):
            if expr.server is not self:
                return False
            node_id = expr.node_id
            if node_id in self._nodes and self._nodes[node_id] is expr:
                return True
        elif isinstance(expr, supriya.synthdefs.SynthDef):
            name = expr.actual_name
            if name in self._synthdefs and self._synthdefs[name] == expr:
                return True
        elif isinstance(expr, supriya.realtime.ServerObject):
            return expr.server is self
        return False

    def __enter__(self):
        self.boot()
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        self.sync()
        self.quit()

    def __getitem__(self, item):
        """
        Get ``item`` from server.

        ::

            >>> server = supriya.Server.default().boot()
            >>> supriya.Synth(name='foo').allocate()
            <+ Synth: 1000 (foo)>

        ::

            >>> server[1000]
            <+ Synth: 1000 (foo)>

        ::

            >>> server['foo']
            <+ Synth: 1000 (foo)>

        ::

            >>> server['b10']
            <+ Buffer: 10>

        ::

            >>> server['a0']
            <+ Bus: 0 (audio)>

        ::

            >>> server['c16']
            <+ Bus: 16 (control)>

        ::

            >>> server = server.quit()
            >>> server['c16']
            Traceback (most recent call last):
            ...
            supriya.exceptions.ServerOffline

        """
        import supriya

        if not self.is_running:
            raise supriya.exceptions.ServerOffline
        if isinstance(item, str):
            match = re.match(r"b(?P<id>\d+)", item)
            if match:
                id_ = int(match.groupdict()["id"])
                return supriya.realtime.Buffer(id_).allocate(server=self)
            match = re.match(r"c(?P<id>\d+)", item)
            if match:
                id_ = int(match.groupdict()["id"])
                return supriya.realtime.Bus(id_, "control").allocate(server=self)
            match = re.match(r"a(?P<id>\d+)", item)
            if match:
                id_ = int(match.groupdict()["id"])
                return supriya.realtime.Bus(id_, "audio").allocate(server=self)
            result = self.root_node[item]
        elif isinstance(item, int):
            result = self._nodes.get(item)
        else:
            raise ValueError(item)
        if isinstance(result, set) and len(result) == 1:
            return tuple(result)[0]
        return result

    def __graph__(self):
        """
        Graph server.

        ::

            >>> import supriya
            >>> server = supriya.Server.default().boot()
            >>> group = supriya.Group([
            ...     supriya.Synth(),
            ...     supriya.Group([
            ...         supriya.Synth(),
            ...         supriya.Synth(),
            ...         ]),
            ...     ]).allocate()

        ::

            >>> graph = server.__graph__()
            >>> print(format(graph, 'graphviz'))
            digraph G {
                graph [bgcolor=transparent,
                    color=lightslategrey,
                    dpi=72,
                    fontname=Arial,
                    outputorder=edgesfirst,
                    overlap=prism,
                    penwidth=2,
                    rankdir=TB,
                    ranksep=0.5,
                    splines=spline,
                    style="dotted, rounded"];
                node [fontname=Arial,
                    fontsize=12,
                    penwidth=2,