import socket
import time
from typing import Iterator

from .error import VBANCMDConnectionError


def ratelimit(func):
    """ratelimit decorator for {VbanCmd}.sendtext, to prevent flooding the network with script requests."""

    def wrapper(*args, **kwargs):
        self, *rem = args
        if self.script_ratelimit > 0:
            now = time.time()
            elapsed = now - self._last_script_request_time
            if elapsed < self.script_ratelimit:
                time.sleep(self.script_ratelimit - elapsed)
            self._last_script_request_time = time.time()
        return func(*args, **kwargs)

    return wrapper


def pong_timeout(func):
    """pong_timeout decorator for {VbanCmd}._handle_pong, to handle timeout logic and socket management."""

    def wrapper(self, timeout: float = None):
        if timeout is None:
            timeout = min(self.timeout, 3.0)

        original_timeout = self.sock.gettimeout()
        self.sock.settimeout(0.5)

        try:
            start_time = time.time()
            response_count = 0

            while time.time() - start_time < timeout:
                try:
                    response_count += 1

                    if func(self):
                        return

                except socket.timeout:
                    continue

            self.logger.debug(
                f'PING timeout after {timeout}s, received {response_count} non-PONG packets'
            )
            raise VBANCMDConnectionError(
                f'PING timeout: No response from {self.host}:{self.port} after {timeout}s'
            )

        finally:
            self.sock.settimeout(original_timeout)

    return wrapper


def cache_bool(func, param):
    """Check cache for a bool prop"""

    def wrapper(*args, **kwargs):
        self, *rem = args
        if self._cmd(param) in self._remote.cache:
            return self._remote.cache.pop(self._cmd(param)) == 1
        if self._remote.sync:
            self._remote.clear_dirty()
        return func(*args, **kwargs)

    return wrapper


def cache_int(func, param):
    """Check cache for an int prop"""

    def wrapper(*args, **kwargs):
        self, *rem = args
        if self._cmd(param) in self._remote.cache:
            return self._remote.cache.pop(self._cmd(param))
        if self._remote.sync:
            self._remote.clear_dirty()
        return func(*args, **kwargs)

    return wrapper


def cache_string(func, param):
    """Check cache for a string prop"""

    def wrapper(*args, **kwargs):
        self, *rem = args
        if self._cmd(param) in self._remote.cache:
            return self._remote.cache.pop(self._cmd(param)).strip('"')
        if self._remote.sync:
            self._remote.clear_dirty()
        return func(*args, **kwargs)

    return wrapper


def cache_float(func, param):
    """Check cache for a float prop"""

    def wrapper(*args, **kwargs):
        self, *rem = args
        if self._cmd(param) in self._remote.cache:
            return round(self._remote.cache.pop(self._cmd(param)), 2)
        if self._remote.sync:
            self._remote.clear_dirty()
        return func(*args, **kwargs)

    return wrapper


def depth(d):
    if isinstance(d, dict):
        return 1 + (max(map(depth, d.values())) if d else 0)
    return 0


def comp(t0: tuple, t1: tuple) -> Iterator[bool]:
    """
    Generator function, accepts two tuples of dB values.

    Returns True when levels are equal (no change), False when different.
    """

    for a, b in zip(t0, t1):
        yield a == b


def deep_merge(dict1, dict2):
    """Generator function for deep merging two dicts"""
    for k in set(dict1) | set(dict2):
        if k in dict1 and k in dict2:
            if isinstance(dict1[k], dict) and isinstance(dict2[k], dict):
                yield k, dict(deep_merge(dict1[k], dict2[k]))
            else:
                yield k, dict2[k]
        elif k in dict1:
            yield k, dict1[k]
        else:
            yield k, dict2[k]


def bump_framecounter(framecounter: int) -> int:
    """Increment framecounter with rollover at 0xFFFFFFFF."""
    if framecounter > 0xFFFFFFFF:
        return 0
    else:
        return framecounter + 1
