import inspect
import threading

try:
    import RPi.GPIO as GPIO
except Exception:
    GPIO = None


_LOCK = threading.RLock()
_PATCHED = False
_ORIGINALS = {}
_PIN_STATES = {}
_EVENT_STATES = {}
_PWM_STATES = {}


def _caller_owner_key():
    frame = inspect.currentframe()
    try:
        caller = frame.f_back.f_back
        while caller is not None:
            owner = caller.f_locals.get("self")
            if owner is not None:
                return ("self", id(owner))
            caller = caller.f_back
    finally:
        del frame
    return None


def _callback_owner_key(callback):
    owner = getattr(callback, "__self__", None)
    if owner is not None:
        return ("self", id(owner))
    caller_owner = _caller_owner_key()
    if caller_owner is not None:
        return caller_owner
    return ("callback", id(callback))


def _normalize_pins(channel):
    if channel is None:
        return None
    if isinstance(channel, (list, tuple, set)):
        return [int(pin) for pin in channel]
    return [int(channel)]


def _edge_matches(requested_edge, actual_edge):
    if requested_edge == GPIO.BOTH or actual_edge == GPIO.BOTH:
        return True
    return requested_edge == actual_edge


def _dispatch_pin_event(pin):
    with _LOCK:
        state = _EVENT_STATES.get(pin)
        if not state:
            return
        callbacks = list(state["callbacks"])
        previous_value = state["last_value"]

    try:
        current_value = GPIO.input(pin)
    except Exception:
        current_value = None

    if current_value is None:
        actual_edge = GPIO.BOTH
    elif previous_value is None:
        actual_edge = GPIO.RISING if current_value else GPIO.FALLING
    elif current_value != previous_value:
        actual_edge = GPIO.RISING if current_value else GPIO.FALLING
    else:
        actual_edge = GPIO.BOTH

    with _LOCK:
        state = _EVENT_STATES.get(pin)
        if state is not None:
            state["last_value"] = current_value

    for entry in callbacks:
        if not _edge_matches(entry["edge"], actual_edge):
            continue
        try:
            entry["callback"](pin)
        except Exception:
            pass


def _register_master_event(pin):
    state = _EVENT_STATES.get(pin)
    if not state:
        return

    callbacks = state["callbacks"]
    if not callbacks:
        if state["registered"]:
            try:
                _ORIGINALS["remove_event_detect"](pin)
            except Exception:
                pass
        _EVENT_STATES.pop(pin, None)
        return

    bounce_time = max(int(entry["bouncetime"] or 0) for entry in callbacks)
    if state["registered"] and state["bouncetime"] == bounce_time:
        return

    if state["registered"]:
        try:
            _ORIGINALS["remove_event_detect"](pin)
        except Exception:
            pass

    try:
        _ORIGINALS["add_event_detect"](
            pin,
            GPIO.BOTH,
            callback=lambda channel, _pin=pin: _dispatch_pin_event(_pin),
            bouncetime=bounce_time,
        )
        state["registered"] = True
        state["bouncetime"] = bounce_time
        try:
            state["last_value"] = GPIO.input(pin)
        except Exception:
            state["last_value"] = None
    except Exception:
        state["registered"] = False


def _patched_setup(channel, direction, pull_up_down=None, initial=None):
    pin = int(channel)
    with _LOCK:
        state = _PIN_STATES.get(pin)
        if state is None:
            _ORIGINALS["setup"](pin, direction, pull_up_down=pull_up_down, initial=initial)
            _PIN_STATES[pin] = {
                "refs": 1,
                "direction": direction,
                "pull_up_down": pull_up_down,
                "initial": initial,
            }
            return

        state["refs"] += 1
        if state["direction"] != direction:
            return


def _teardown_pin(pin):
    event_state = _EVENT_STATES.pop(pin, None)
    if event_state and event_state.get("registered"):
        try:
            _ORIGINALS["remove_event_detect"](pin)
        except Exception:
            pass

    pwm_state = _PWM_STATES.pop(pin, None)
    if pwm_state and pwm_state.get("started"):
        try:
            pwm_state["backend"].stop()
        except Exception:
            pass

    try:
        _ORIGINALS["cleanup"](pin)
    except Exception:
        pass


def _patched_cleanup(channel=None):
    pins = _normalize_pins(channel)
    if pins is None:
        with _LOCK:
            pins = list(_PIN_STATES.keys())
            _PIN_STATES.clear()
        for pin in pins:
            _teardown_pin(pin)
        _EVENT_STATES.clear()
        _PWM_STATES.clear()
        return _ORIGINALS["cleanup"]()

    to_cleanup = []
    with _LOCK:
        for pin in pins:
            state = _PIN_STATES.get(pin)
            if state is None:
                to_cleanup.append(pin)
                continue
            state["refs"] -= 1
            if state["refs"] <= 0:
                _PIN_STATES.pop(pin, None)
                to_cleanup.append(pin)

    for pin in to_cleanup:
        _teardown_pin(pin)


def _patched_add_event_detect(channel, edge, callback=None, bouncetime=0):
    if callback is None:
        return _ORIGINALS["add_event_detect"](channel, edge, callback=callback, bouncetime=bouncetime)

    pin = int(channel)
    owner_key = _callback_owner_key(callback)
    with _LOCK:
        state = _EVENT_STATES.setdefault(
            pin,
            {"callbacks": [], "registered": False, "bouncetime": 0, "last_value": None},
        )
        for entry in state["callbacks"]:
            if entry["owner"] == owner_key and entry["callback"] == callback and entry["edge"] == edge:
                entry["bouncetime"] = max(int(entry["bouncetime"] or 0), int(bouncetime or 0))
                break
        else:
            state["callbacks"].append(
                {
                    "owner": owner_key,
                    "callback": callback,
                    "edge": edge,
                    "bouncetime": int(bouncetime or 0),
                }
            )
        _register_master_event(pin)
    return True


def _patched_remove_event_detect(channel):
    pin = int(channel)
    owner_key = _caller_owner_key()
    with _LOCK:
        state = _EVENT_STATES.get(pin)
        if state is None:
            try:
                return _ORIGINALS["remove_event_detect"](pin)
            except Exception:
                return None

        if owner_key is None:
            state["callbacks"] = []
        else:
            state["callbacks"] = [
                entry for entry in state["callbacks"] if entry["owner"] != owner_key
            ]

        if not state["callbacks"]:
            if state["registered"]:
                try:
                    _ORIGINALS["remove_event_detect"](pin)
                except Exception:
                    pass
            _EVENT_STATES.pop(pin, None)
            return None

        _register_master_event(pin)
    return None


class _SharedPWMProxy:
    def __init__(self, pin, state):
        self._pin = pin
        self._state = state
        self._active = True
        self._started = False

    def start(self, duty_cycle):
        with _LOCK:
            if not self._active:
                return
            if not self._state["started"]:
                self._state["backend"].start(duty_cycle)
                self._state["started"] = True
            self._started = True

    def ChangeDutyCycle(self, duty_cycle):
        with _LOCK:
            if not self._active:
                return
            if not self._state["started"]:
                self._state["backend"].start(duty_cycle)
                self._state["started"] = True
            else:
                self._state["backend"].ChangeDutyCycle(duty_cycle)
            self._started = True

    def ChangeFrequency(self, frequency):
        with _LOCK:
            if not self._active:
                return
            self._state["backend"].ChangeFrequency(frequency)

    def stop(self):
        with _LOCK:
            if not self._active:
                return
            self._active = False
            self._state["owners"] -= 1
            if self._state["owners"] <= 0:
                try:
                    self._state["backend"].stop()
                finally:
                    _PWM_STATES.pop(self._pin, None)


def _patched_pwm(channel, frequency):
    pin = int(channel)
    with _LOCK:
        state = _PWM_STATES.get(pin)
        if state is None:
            state = {
                "backend": _ORIGINALS["PWM"](pin, frequency),
                "owners": 0,
                "started": False,
            }
            _PWM_STATES[pin] = state
        state["owners"] += 1
        return _SharedPWMProxy(pin, state)


def ensure_gpio_patched():
    global _PATCHED

    if GPIO is None:
        return False

    with _LOCK:
        if _PATCHED:
            return True

        _ORIGINALS["setup"] = GPIO.setup
        _ORIGINALS["cleanup"] = GPIO.cleanup
        _ORIGINALS["add_event_detect"] = GPIO.add_event_detect
        _ORIGINALS["remove_event_detect"] = GPIO.remove_event_detect
        _ORIGINALS["PWM"] = GPIO.PWM

        GPIO.setup = _patched_setup
        GPIO.cleanup = _patched_cleanup
        GPIO.add_event_detect = _patched_add_event_detect
        GPIO.remove_event_detect = _patched_remove_event_detect
        GPIO.PWM = _patched_pwm
        _PATCHED = True
        return True


__all__ = ["ensure_gpio_patched"]