# -----------------------------------------------------------------------------
# This is free and unencumbered software released into the public domain.
#
# Anyone is free to copy, modify, publish, use, compile, sell, or distribute
# this software, either in source code form or as a compiled binary, for any
# purpose, commercial or non-commercial, and by any means.
#
# In jurisdictions that recognize copyright laws, the author or authors of this
# software dedicate any and all copyright interest in the software to the
# public domain. We make this dedication for the benefit of the public at large
# and to the detriment of our heirs and successors. We intend this dedication
# to be an overt act of relinquishment in perpetuity of all present and future
# rights to this software under copyright law.
#
# OPTIONAL ATTRIBUTION:
# Credit to E. Luttmann is appreciated but not required.
#
# For more information, please refer to <https://unlicense.org>
# -----------------------------------------------------------------------------
"""
pycessingame.py

PROCESSING like API implemented for PYTHON only using pygame.

Features:
  - implementing lots of the processing API (already)
  - camelCase/snake_case support for variables and functions
  - getPressed(<key>) function to query any <key> at any time

Prerequisites:
  - pygame must be installed (in case it is missing: "pip install pygame")

Usage:
    import pycessingame 

    def setup():
        size(640, 480)
        frameRate(60)

    def draw():
        background(30, 120, 200)
        fill(255, 200, 0)
        stroke(255)
        strokeWeight(4)
        ellipse(width/2, height/2, 150, 150)

    run()
"""

# -------------------------- Imports --------------------------

import sys
import colorsys
import time
import math
import __main__
import re
import datetime
import pygame
import random as _pyrandom
import types
import builtins
import numbers

# -------------------------- Constants --------------------------

_CONSTANTS = {
    'LEFT' : 'LEFT',
    'RIGHT' : 'RIGHT',
    'CENTER' : 'CENTER',
    'TOP' : 'TOP',
    'PI' : math.pi,
    'TWO_PI' : 2*math.pi,
    'HALF_PI' : math.pi/2,
    'QUARTER_PI' : math.pi/4,
    'DEGREES' : 180.0 / math.pi,
    'RADIANS' : math.pi / 180.0,
    'CORNER' : 'CORNER',
    'RADIUS' : 'RADIUS',
    'CORNERS' : 'CORNERS',
    'RGB' : 'RGB',
    'HSB' : 'HSB',
}

ARROW = "ARROW"
CROSS = "CROSS"
HAND = "HAND"
MOVE = "MOVE"
WAIT = "WAIT"
_CURSOR_CONSTANTS = {ARROW, CROSS, HAND, MOVE, WAIT}

_keycode_map = {
    pygame.K_LEFT:      "LEFT",
    pygame.K_RIGHT:     "RIGHT",
    pygame.K_UP:        "UP",
    pygame.K_DOWN:      "DOWN",

    pygame.K_RETURN:    "ENTER",
    pygame.K_KP_ENTER:  "ENTER",
    pygame.K_BACKSPACE: "BACKSPACE",
    pygame.K_TAB:       "TAB",
    pygame.K_ESCAPE:    "ESC",
    pygame.K_DELETE:    "DELETE",
    pygame.K_HOME:      "HOME",
    pygame.K_END:       "END",
    pygame.K_PAGEUP:    "PAGE_UP",
    pygame.K_PAGEDOWN:  "PAGE_DOWN",
    pygame.K_CAPSLOCK: "CAPSLOCK",
    pygame.K_INSERT:   "INSERT",
    
    pygame.K_LSHIFT:    "SHIFT",
    pygame.K_RSHIFT:    "SHIFT",
    pygame.K_LCTRL:     "CONTROL",
    pygame.K_RCTRL:     "CONTROL",
    pygame.K_LALT:      "ALT",
    pygame.K_RALT:      "ALT",

    pygame.K_F1: "F1",  pygame.K_F2: "F2",  pygame.K_F3: "F3",
    pygame.K_F4: "F4",  pygame.K_F5: "F5",  pygame.K_F6: "F6",
    pygame.K_F7: "F7",  pygame.K_F8: "F8",  pygame.K_F9: "F9",
    pygame.K_F10:"F10", pygame.K_F11:"F11", pygame.K_F12:"F12",
}


_PROCESSING_KEY_CONSTANTS = {
    "LEFT", "RIGHT", "UP", "DOWN",
    "ENTER", "RETURN", "BACKSPACE", "TAB", "ESC", "DELETE",
    "SHIFT", "CONTROL", "ALT",
    "HOME", "END", "PAGE_UP", "PAGE_DOWN",
    "F1","F2","F3","F4","F5","F6",
    "F7","F8","F9","F10","F11","F12"
}

# ------- Function Handle for overloaded function -----------

_python_map = builtins.map

# -------------------------- API Functions --------------------------

def exit():
    """
    Stop the sketch immediately.
    Equivalent to Processing exit().
    """
    _P._running = False

def map(*args):
    """
    Map a number from one range to another.

    Usage:
    map(value, start1, stop1, start2, stop2) -> float
    Returns the mapped float value.

    Also behaves as Python's built-in map if called differently.
    """
    if len(args) == 5 and all(isinstance(x, numbers.Real) for x in args):
        value, start1, stop1, start2, stop2 = args
        if start1 == stop1:
            raise ValueError("map() input range must not be zero")
        return start2 + (stop2 - start2) * ((value - start1) / (stop1 - start1))

    return _python_map(*args)

def getPressed(key=None):
    """
    Check if a key is currently pressed.

    Parameters:
    key: None, string, or Processing key constant
    None: any key pressed -> returns True/False
    str: single character or special key

    Returns:
    bool: True if the key is pressed

    Examples:
    getPressed() -> True if any key is down
    getPressed('a') -> True if 'a' is pressed
    getPressed(LEFT) -> True if left arrow pressed
    """
    # Any key?
    if key is None:
        return bool(_P._keys_down)

    # Normalize
    if isinstance(key, str):
        key = key.upper() if len(key) > 1 else key

    return key in _P._keys_down


def noLoop():
    """Stop automatic calling of draw() each frame."""
    _P._looping = False

def loop():
    """Resume automatic calling of draw() each frame."""
    _P._looping = True

def redraw():
    """Force draw() to execute once, even if noLoop() is active."""
    _P._draw_next_frame = True
    
def random(*args):
    """
    Processing-compatible random() function.

    Usage:
    random() -> float 0..1
    random(high) -> float 0..high
    random(low, high) -> float low..high
    """
    if len(args) == 0:
        # Processing: random() without args → returns a float between 0 and 1
        return _pyrandom.random()

    if len(args) == 1:
        high = args[0]
        return _pyrandom.random() * high

    if len(args) == 2:
        low, high = args
        return low + _pyrandom.random() * (high - low)

    raise TypeError("random() takes 0, 1 or 2 arguments")

def randomSeed(v):
    """Set the random number generator seed."""
    _pyrandom.seed(v)
    
def randomGaussian():
    # Box-Muller transform
    u1 = 1.0 - _pyrandom.random()
    u2 = 1.0 - _pyrandom.random()
    return ( (-2 * math.log(u1)) ** 0.5 ) * math.cos(2 * math.pi * u2 )

def millis():
    """
    Returns milliseconds since sketch start.
    Processing: millis()
    """
    if _P._last_time_global is None:
        return 0
    return int((time.time() - _P._last_time_global) * 1000)

def second():
    """Return current second 0–59."""
    return datetime.datetime.now().second

def minute():
    """Return current minute 0–59."""
    return datetime.datetime.now().minute

def hour():
    """Return current hour 0–23."""
    return datetime.datetime.now().hour

def day():
    """Return day of month 1–31."""
    return datetime.datetime.now().day

def month():
    """Return month 1–12."""
    return datetime.datetime.now().month

def year():
    """Return full year (e.g. 2025)."""
    return datetime.datetime.now().year

def weekday():
    """
    Monday=1 ... Sunday=7
    (Processing hat das nicht, aber viele wollen es)
    """
    return datetime.datetime.now().isoweekday()

def timestamp():
    """
    Returns UNIX timestamp as int.
    (Zusatzfunktion – optional)
    """
    return int(time.time())

def textSize(size):
    _P._text_size = size
    
def textAlign(align_x, align_y = None):
    _P._text_align_x = align_x
    if align_y:
        _P._text_align_y = align_y
        print("WARNING: Y alignment not implemented")

def textFont(which, size = None):
    print("WARNING: textFont not implemented")

def text(s, x, y):
    """
    Draw text onto the _P.surface.
    - s: text (any -> str)
    - x, y: position (top-left by default, affected by align)
    - align: 'left'|'center'|'right'
    """
    if _P.surface is None:
        return

    # coerce to str
    txt = str(s)

    # choose color from current fill (fallback white)
    color = _P._fill_color[:3]
    font = _get_font(_P._text_size)

    # render text surface (antialias = True)
    try:
        text_surf = font.render(txt, _P._smooth, color)
    except Exception:
        # fallback if render fails
        text_surf = font.render(txt, False, color)

    tw, th = text_surf.get_size()

    # alignment handling
    ax = x
    if _P._text_align_x == _CONSTANTS['CENTER']:
        ax = x - tw // 2
    elif _P._text_align_x == _CONSTANTS['RIGHT']:
        ax = x - tw

    # blit to main surface
    _P.surface.blit(text_surf, (int(ax), int(y)))

def size(w,h):
    _P.width = int(w)
    _P.height = int(h)
    if pygame.get_init():
        pygame.display.set_mode((_P.width, _P.height))
        _P.surface = pygame.Surface((_P.width,_P.height), pygame.SRCALPHA).convert_alpha()

def windowTitle(title):
    _P._window_title = str(title)
    try:
        pygame.display.set_caption(_P._window_title)
    except Exception:
        pass

def smooth():
    """Enable anti-aliasing where supported."""
    _P._smooth = True

def noSmooth():
    """Disable all anti-aliasing."""
    _P._smooth = False


def noCursor():
    """
    Hide the mouse cursor (Processing-compatible).
    Works before and after run().
    """
    _P._cursor_visible = False
    _P._cursor_image = None
    _P._cursor_mode = None
    try:
        pygame.mouse.set_visible(False)
    except Exception:
        pass

def cursor(mode_or_img=None, x=None, y=None):
    """
    Processing-compatible cursor() API.

    Supported calls:
        cursor()                   → default arrow
        cursor(MODE)               → ARROW, CROSS, HAND, MOVE, WAIT
        cursor(image, x, y)        → custom image cursor
    """

    # ------------------------------
    # CASE 1: cursor()  → default arrow
    # ------------------------------
    if mode_or_img is None:
        _P._cursor_visible = True
        _P._cursor_mode = ARROW
        _P._cursor_image = None

        try:
            pygame.mouse.set_visible(True)
            pygame.mouse.set_cursor(pygame.Cursor(pygame.SYSTEM_CURSOR_ARROW))
        except Exception:
            pass

        return

    # ------------------------------
    # CASE 2: cursor(img, x, y) → custom image
    # (img = pygame.Surface)
    # ------------------------------
    if isinstance(mode_or_img, pygame.Surface):
        img = mode_or_img
        hx = int(x or 0)
        hy = int(y or 0)

        _P._cursor_visible = True
        _P._cursor_image = img
        _P._cursor_hotspot = (hx, hy)
        _P._cursor_mode = None  # disable system cursor modes

        try:
            pygame.mouse.set_visible(True)
            # Pygame cannot use ARGB images as real cursors → so fake minimal cursor
            pygame.mouse.set_cursor(
                pygame.Cursor(
                    (img.get_width(), img.get_height()),
                    (hx, hy),
                    pygame.cursors.compile(['X'], black='X')
                )
            )
        except Exception:
            pass

        return

    # ------------------------------
    # CASE 3: cursor(MODE)
    # mode is a string: ARROW, CROSS, HAND, MOVE, WAIT
    # ------------------------------
    if isinstance(mode_or_img, str):
        mode = mode_or_img.upper()

        if mode not in _CURSOR_CONSTANTS:
            raise ValueError(f"cursor(): unknown mode '{mode}'")

        _P._cursor_visible = True
        _P._cursor_mode = mode
        _P._cursor_image = None  # disable custom img cursor

        pygame_cursor_map = {
            ARROW: pygame.SYSTEM_CURSOR_ARROW,
            CROSS: pygame.SYSTEM_CURSOR_CROSSHAIR,
            HAND: pygame.SYSTEM_CURSOR_HAND,
            MOVE: pygame.SYSTEM_CURSOR_SIZEALL,
            WAIT: pygame.SYSTEM_CURSOR_WAIT,
        }

        try:
            pygame.mouse.set_visible(True)
            pygame.mouse.set_cursor(pygame.Cursor(pygame_cursor_map[mode]))
        except Exception:
            pass

        return

    # ------------------------------
    # Anything else is invalid
    # ------------------------------
    raise TypeError("cursor(): expected None, str mode, or pygame.Surface image")

def frameRate(f):
    """
    Set target frame rate (Processing-compatible).
    """
    try:
        f = float(f)
    except (TypeError, ValueError):
        return

    if f <= 0:
        return

    _P._target_fps = f

def colorMode(mode, *args):
    mode = mode.upper()
    if mode not in (_CONSTANTS['RGB'], _CONSTANTS['HSB']):
        raise ValueError("colorMode(): invalid mode")

    _P._color_mode = mode

    # --- Defaults ---
    if not args:
        _P._color_max = (
            [255,255,255,255] if mode == _CONSTANTS['RGB']
            else [360,100,100,100]
        )
        return

    # --- colorMode(mode, max) ---
    if len(args) == 1:
        m = args[0]
        _P._color_max = [m, m, m, m]
        return

    # --- colorMode(mode, x, y, z) ---
    if len(args) == 3:
        _P._color_max = [args[0], args[1], args[2], 255]
        return

    # --- colorMode(mode, x, y, z, a) ---
    if len(args) == 4:
        _P._color_max = list(args)
        return

    raise TypeError("colorMode() accepts 1, 2, 4 or 5 arguments")

import colorsys

def color(*args):
    """
    Processing-konforme color() Funktion.
    - RGB oder HSB, abhängig von _P._color_mode
    - Graustufen unterstützung
    - Alpha optional
    - HSB: Hue wird gewrappt
    """
    # Unpack single tuple/list
    while len(args) == 1 and isinstance(args[0], (tuple, list)):
        args = tuple(args[0])

    n = len(args)

    if n == 1:
        comps = [args[0], args[0], args[0], _P._color_max[3]]
    elif n == 2:
        comps = [args[0], args[0], args[0], args[1]]
    elif n == 3:
        comps = [args[0], args[1], args[2], _P._color_max[3]]
    elif n == 4:
        comps = list(args)
    else:
        raise TypeError("color() accepts 1–4 arguments")

    # --- normalize via colorMode scales ---
    max_vals = _P._color_max
    norm = [0 if maxv == 0 else comps[i]/max_vals[i] for i, maxv in enumerate(max_vals)]

    if _P._color_mode == _CONSTANTS['HSB']:
        # Hue wrap-around wie Processing: Hue % 1.0
        h = norm[0] % 1.0
        s = min(max(norm[1], 0), 1)
        v = min(max(norm[2], 0), 1)

        r, g, b = [int(c*255) for c in colorsys.hsv_to_rgb(h, s, v)]
    else:
        r, g, b = [min(max(int(n*255), 0), 255) for n in norm[:3]]

    a = min(max(int(norm[3]*255), 0), 255)
    return (r, g, b, a)



def alpha(c):
    return _as_rgba(c)[3]

def red(c):
    return _as_rgba(c)[0]

def green(c):
    return _as_rgba(c)[1]

def blue(c):
    return _as_rgba(c)[2]


def hue(c):
    r, g, b, _ = _as_rgba(c)
    h, _, _ = _rgb_to_hsb_scaled(r, g, b)
    return h

def saturation(c):
    r, g, b, _ = _as_rgba(c)
    _, s, _ = _rgb_to_hsb_scaled(r, g, b)
    return s

def brightness(c):
    r, g, b, _ = _as_rgba(c)
    _, _, v = _rgb_to_hsb_scaled(r, g, b)
    return v

def lerpColor(c1, c2, amt):
    """
    Linearly interpolates between two colors.
    Processing-compatible implementation.

    Parameters:
        c1, c2 : color tuples (r, g, b) or (r, g, b, a)
        amt    : interpolation amount (float)

    Returns:
        (r, g, b, a) tuple
    """
    if _P._color_mode == _CONSTANTS['HSB']:
        max_h, max_s, max_b = _P._color_max[:3]
        h1, s1, b1 = _rgb_to_hsb_scaled(*_as_rgba(c1)[:3])
        h2, s2, b2 = _rgb_to_hsb_scaled(*_as_rgba(c2)[:3])

        def _lerp_hue(h1, h2, t, max_h):
            """Interpolate hue on a circle (0..max_h) correctly."""
            delta = (h2 - h1) % max_h
            if delta > max_h / 2:
                delta -= max_h  # take shorter path
            h = (h1 + delta * t) % max_h
            return h

        h = _lerp_hue(h1, h2, amt, max_h)
        s = s1 + (s2 - s1) * amt
        b = b1 + (b2 - b1) * amt

        return color(h, s, b)
    else:    
        if c1 is None or c2 is None:
            raise ValueError("lerpColor(): colors must not be None")

        # Normalize colors to RGBA
        if len(c1) == 3:
            r1, g1, b1 = c1
            a1 = 255
        elif len(c1) == 4:
            r1, g1, b1, a1 = c1
        else:
            raise ValueError("lerpColor(): invalid color c1")

        if len(c2) == 3:
            r2, g2, b2 = c2
            a2 = 255
        elif len(c2) == 4:
            r2, g2, b2, a2 = c2
        else:
            raise ValueError("lerpColor(): invalid color c2")

        def lerp(a, b, t):
            return a + (b - a) * t

        r = int(lerp(r1, r2, amt))
        g = int(lerp(g1, g2, amt))
        b = int(lerp(b1, b2, amt))
        a = int(lerp(a1, a2, amt))

        # Clamp like Processing's internal color storage
        def clamp(v):
            return max(0, min(255, v))

        return (clamp(r), clamp(g), clamp(b), clamp(a),)

def background(*args):
    col4 = color(*args)
    if _P.surface is None: return
    if col4[3]<255:
        tmp = pygame.Surface((_P.width,_P.height), pygame.SRCALPHA)
        tmp.fill(col4)
        _P.surface.blit(tmp,(0,0))
    else:
        _P.surface.fill(col4[:3])

def clear():
    if _P.surface:
        _P.surface.fill((0,0,0,0))

def fill(*args):
    """Processing-kompatible fill()"""
    if len(args)==0:
        return
    _P._fill_color = color(*args)
    _P._use_fill = True

def noFill():
    _P._use_fill = False

def stroke(*args):
    """Processing-kompatible stroke()"""
    if len(args)==0:
        return
    _P._stroke_color = color(*args)
    _P._use_stroke = True

def noStroke():
    _P._use_stroke = False


def strokeWeight(w):
    _P._stroke_weight = max(1,int(w))
    
def rectMode(mode):
    """
    Setzt den Rechteckmodus.
    Optionen: 'CORNER', 'CORNERS', 'CENTER', 'RADIUS'
    """
    mode = mode.upper()
    if mode not in ('CORNER', 'CORNERS', 'CENTER', 'RADIUS'):
        raise ValueError(f"rectMode(): unknown mode '{mode}'")
    _P.rect_mode = mode

def ellipseMode(mode):
    """
    Setzt den Ellipsenmodus.
    Optionen: 'CORNER', 'CORNERS', 'CENTER', 'RADIUS'
    """
    mode = mode.upper()
    if mode not in ('CORNER', 'CORNERS', 'CENTER', 'RADIUS'):
        raise ValueError(f"ellipseMode(): unknown mode '{mode}'")
    _P.ellipse_mode = mode
        
def rect(x, y, w, h):
    x, y, w, h = float(x), float(y), float(w), float(h)

    if _P.rect_mode == 'CORNER':
        rx, ry, rw, rh = x, y, w, h
    elif _P.rect_mode == 'CORNERS':
        rx, ry = min(x, w), min(y, h)
        rw, rh = abs(w - x), abs(h - y)
    elif _P.rect_mode == 'CENTER':
        rx = x - w/2
        ry = y - h/2
        rw, rh = w, h
    elif _P.rect_mode == 'RADIUS':
        rx = x - w
        ry = y - h
        rw, rh = w*2, h*2
    else:
        raise ValueError(f"Invalid rectMode: {_P.rect_mode}")

    r = pygame.Rect(int(rx), int(ry), int(rw), int(rh))
    _apply_fill(_P.surface, pygame.draw.rect, r)
    _apply_stroke(_P.surface, pygame.draw.rect, r)

def ellipse(x, y, w, h):
    x, y, w, h = float(x), float(y), float(w), float(h)

    if _P.ellipse_mode == 'CENTER':
        rx, ry = x - w/2, y - h/2
        rw, rh = w, h
    elif _P.ellipse_mode == 'RADIUS':
        rx, ry = x - w, y - h
        rw, rh = w*2, h*2
    elif _P.ellipse_mode == 'CORNER':
        rx, ry, rw, rh = x, y, w, h
    elif _P.ellipse_mode == 'CORNERS':
        rx, ry = min(x, w), min(y, h)
        rw, rh = abs(w - x), abs(h - y)
    else:
        raise ValueError(f"Invalid ellipseMode: {_P.ellipse_mode}")

    rect_ = pygame.Rect(int(rx), int(ry), int(rw), int(rh))
    _apply_fill(_P.surface, pygame.draw.ellipse, rect_)
    _apply_stroke(_P.surface, pygame.draw.ellipse, rect_)



def line(x1, y1, x2, y2):
    if not _P._use_stroke:
        return

    x1, y1, x2, y2 = _python_map(int, (x1, y1, x2, y2))
    color = _P._stroke_color[:3]

    if _P._smooth and _P._stroke_weight == 1:
        # aaline: Antialias, nur 1px
        pygame.draw.aaline(_P.surface, color, (x1, y1), (x2, y2))
    else:
        # normale Linie, Breite beachten
        pygame.draw.line(_P.surface, color, (x1, y1), (x2, y2), _P._stroke_weight)     

def point(x,y):
    if _P._use_stroke: pygame.draw.circle(_P.surface,_P._stroke_color[:3],(int(x),int(y)),max(1,_P._stroke_weight)//2)

def circle(cx,cy,r): ellipse(cx,cy,r*2,r*2)
def triangle(x1,y1,x2,y2,x3,y3):
    pts=[(int(x1),int(y1)),(int(x2),int(y2)),(int(x3),int(y3))]
    _apply_fill(_P.surface,pygame.draw.polygon,pts)
    _apply_stroke(_P.surface,pygame.draw.polygon,pts)

# -------------------------- State --------------------------
class _State:
    def __init__(self):
        self.width = 640
        self.height = 480
        self.surface = None

        # Looping
        self._looping = True
        self._draw_next_frame = True
        
        # Environment
        self._window_title = "pygame processing-Style"
        self._cursor_visible = True
        self._cursor_mode = ARROW
        self._cursor_image = None
        self._cursor_hotspot = (0, 0)
        self._smooth = True

        # Styles & Color mode
        self._color_mode = 'RGB'     # 'RGB' or 'HSB'
        self._color_max = [255, 255, 255, 255]  # r,g,b,a OR h,s,b,a
        self._fill_color = (255, 255, 255, 255)
        self._stroke_color = (0, 0, 0, 255)
        self._background_color = (200,200,200, 255)
        self._use_fill = True
        self._use_stroke = True
        self._stroke_weight = 1
        self._text_size = 16
        self._text_align_x = _CONSTANTS['LEFT']
        self._text_align_y = None
        self._text_font_cache = {}

        # Modes
        self.rect_mode = 'CORNER'
        self.ellipse_mode = 'CENTER'

        # Timing
        self._target_fps = 60.0
        self.frameRate = 60
        self.frameCount = 0
        self._clock = None
        self._running = True
        self._last_time_global = 0
        self._last_frame_time = None

        # Input
        self.mouseX = 0
        self.mouseY = 0
        self.pmouseX = 0
        self.pmouseY = 0
        self.mousePressed = False
        self.mouseButton = None

        self.key = None
        self.keyCode = None
        self.keyPressed = False
        # Key state (continuous pressed state)
        self._keys_down = set()

        self._verbose = True

_P = _State()

# -------------------------- Helpers --------------------------

def _as_rgba(c):
    if not isinstance(c, (tuple, list)):
        raise TypeError("Expected color tuple")
    if len(c) == 3:
        return c[0], c[1], c[2], 255
    if len(c) == 4:
        return c
    raise ValueError("Invalid color format")

def _rgb_to_hsb_scaled(r, g, b):
    h, s, v = colorsys.rgb_to_hsv(r/255, g/255, b/255)
    return (
        h * _P._color_max[0],
        s * _P._color_max[1],
        v * _P._color_max[2],
    )

def _camel_to_snake(name):
    return re.sub(r'(?<!^)(?=[A-Z])', '_', name).lower()


# -------------------------- Full Proxy System --------------------------

class _BaseProxy:
    def __init__(self, state, attr):
        self._state = state
        self._attr = attr
        self._callback = None

    def get(self):
        return getattr(self._state, self._attr)

    def set(self, value):
        setattr(self._state, self._attr, value)

    def register_callback(self, fn):
        if callable(fn):
            self._callback = fn

    # Descriptor
    def __get__(self, instance=None, owner=None):
        return self.get()
    def __set__(self, instance, value):
        self.set(value)

    # Callable
    def __call__(self, *args, **kwargs):
        if self._callback:
            return self._callback(*args, **kwargs)
        raise TypeError(f"{self._attr} is not callable (no callback assigned)")

    # Representation
    def __repr__(self): return repr(self.get())
    def __str__(self): return str(self.get())

    # Bool / Comparison
    def __bool__(self): return bool(self.get())
    def __eq__(self, other): return self.get() == other
    def __ne__(self, other): return self.get() != other
    def __lt__(self, other): return self.get() < other
    def __le__(self, other): return self.get() <= other
    def __gt__(self, other): return self.get() > other
    def __ge__(self, other): return self.get() >= other

    # Numeric
    def __int__(self): return int(self.get())
    def __float__(self): return float(self.get())
    def __complex__(self): return complex(self.get())
    def __index__(self): return int(self.get())

    # Arithmetic
    def __add__(self, other): return self.get() + other
    def __radd__(self, other): return other + self.get()
    def __sub__(self, other): return self.get() - other
    def __rsub__(self, other): return other - self.get()
    def __mul__(self, other): return self.get() * other
    def __rmul__(self, other): return other * self.get()
    def __truediv__(self, other): return self.get() / other
    def __rtruediv__(self, other): return other / self.get()
    def __floordiv__(self, other): return self.get() // other
    def __rfloordiv__(self, other): return other // self.get()
    def __mod__(self, other): return self.get() % other
    def __rmod__(self, other): return other % self.get()
    def __pow__(self, other): return self.get() ** other
    def __rpow__(self, other): return other ** self.get()
    def __neg__(self): return -self.get()
    def __pos__(self): return +self.get()
    def __abs__(self): return abs(self.get())

class _ProcessingVar(_BaseProxy):
    def set(self, value):
        if self._attr == "frameRate":
            # frameRate ist read-only wie in Processing
            return
        super().set(value)

class _DualProxy(_BaseProxy):
    def get(self):
        return getattr(self._state, self._attr)
    def set(self, value):
        setattr(self._state, self._attr, value)
    def __call__(self, *args, **kwargs):
        if self._callback:
            return self._callback(*args, **kwargs)
        return None

class _FunctionProxy(_BaseProxy):
    def get(self):
        return self._callback
    def __call__(self, *args, **kwargs):
        if self._callback:
            return self._callback(*args, **kwargs)
        return None

def _inject_var(name, dual=False):
    proxy_class = _DualProxy if dual else _ProcessingVar
    proxy = proxy_class(_P, name)
    snake = _camel_to_snake(name)

    # Callback falls schon vorhanden
    user_fn = getattr(__main__, name, None)
    if callable(user_fn) and dual:
        proxy.register_callback(user_fn)

    setattr(__main__, name, proxy)
    setattr(__main__, snake, proxy)
    return proxy

def _inject_fun(name):
    proxy_class = _FunctionProxy
    proxy = proxy_class(_P, name)
    snake = _camel_to_snake(name)

    # Callback falls schon vorhanden
    mod = sys.modules[__name__]  
    user_fn = getattr(mod, name, None) 
    if callable(user_fn):
        proxy.register_callback(user_fn)

    setattr(__main__, name, proxy)
    setattr(__main__, snake, proxy)
    return proxy


# -------------------------- Constants --------------------------

def _get_font(size):
    """Return a pygame Font object from cache (None -> default font)."""
    key = int(size)
    font = _P._text_font_cache.get(key)
    if font is None:
        # ensure pygame.font is initialized
        try:
            if not pygame.font.get_init():
                pygame.font.init()
        except Exception:
            pass
        font = pygame.font.Font(None, key)
        _P._text_font_cache[key] = font
    return font


def _apply_fill(surf,func,*args,**kwargs):
    if _P._use_fill and _P._fill_color:
        func(surf,_P._fill_color[:3],*args,**kwargs)

def _apply_stroke(surf,func,*args,**kwargs):
    if _P._use_stroke and _P._stroke_color:
        func(surf,_P._stroke_color[:3],*args,width=_P._stroke_weight,**kwargs)

def _update_mouse(event):
    # Handle movement
    if event.type == pygame.MOUSEMOTION:
        _P.pmouseX, _P.pmouseY = _P.mouseX, _P.mouseY
        _P.mouseX, _P.mouseY = event.pos

        if _P.mousePressed:
            # Dragging
            _P._mouse_dragged = True
            if callable(getattr(__main__, 'mouseDragged', None)):
                __main__.mouseDragged()
        else:
            # Moving without button
            if callable(getattr(__main__, 'mouseMoved', None)):
                __main__.mouseMoved()
        return

    # Handle mouse down
    if event.type == pygame.MOUSEBUTTONDOWN:
        # Wheel events should not trigger click logic
        if event.button in (4, 5):
            return

        _P.pmouseX, _P.pmouseY = _P.mouseX, _P.mouseY
        _P.mouseX, _P.mouseY = event.pos

        _P.mousePressed = True
        _P.mouseButton = {1:'LEFT', 2:'CENTER', 3:'RIGHT'}.get(event.button)
        _P._mouse_dragged = False
        _P._mouse_down_pos = event.pos
        _P._mouse_down_time = time.time()

        if callable(getattr(__main__, 'mousePressed', None)):
            __main__.mousePressed()
        return

    # Handle mouse up
    if event.type == pygame.MOUSEBUTTONUP:
        # Again: wheel events do not trigger click logic
        if event.button in (4, 5):
            return

        _P.pmouseX, _P.pmouseY = _P.mouseX, _P.mouseY
        _P.mouseX, _P.mouseY = event.pos

        was_dragged = _P._mouse_dragged
        down_pos = _P._mouse_down_pos
        down_time = _P._mouse_down_time

        _P.mousePressed = False
        _P.mouseButton = None

        # Always trigger mouseReleased
        if callable(getattr(__main__, 'mouseReleased', None)):
            __main__.mouseReleased()

        # Click detection: small movement + fast release
        if not was_dragged:
            dx = _P.mouseX - down_pos[0]
            dy = _P.mouseY - down_pos[1]
            dist_sq = dx*dx + dy*dy
            time_diff = time.time() - down_time

            if dist_sq < 9 and time_diff < 0.3:
                if callable(getattr(__main__, 'mouseClicked', None)):
                    __main__.mouseClicked()
        return


def _update_mouse_wheel(event):
    """
    Processing-kompatibles mouseWheel():
    - event.delta > 0  = nach oben
    - event.delta < 0  = nach unten

    Processing ruft mouseWheel(event) auf,
    wobei event.count die Scroll-Richtung enthält.
    """

    # Pygame: button 4 (up), button 5 (down)
    if event.type == pygame.MOUSEWHEEL:
        wheel = event.y
    elif event.type == pygame.MOUSEBUTTONDOWN:
        if event.button == 4:
            wheel = 1
        elif event.button == 5:
            wheel = -1
        else:
            return
    else:
        return


    class WheelEvent:
        def __init__(self, count):
            self.count = count

    if callable(getattr(__main__, "mouseWheel", None)):
        __main__.mouseWheel(WheelEvent(wheel))

def _update_keyboard(event):
    if event.type==pygame.KEYDOWN:
        _P.keyPressed=True
        _P.keyCode=event.key
        # If not a special name, try unicode
        if event.key in _keycode_map:
            key_name = _keycode_map[event.key]
            _P.key = key_name
            _P._keys_down.add(key_name)
        else:
            try:
                _P.key=event.unicode
                if _P.key:
                    _P._keys_down.add(_P.key)
            except:
                _P.key=None
        if callable(getattr(__main__,'keyPressed',None)):
            __main__.keyPressed()
        if callable(getattr(__main__,'keyTyped',None)) and _P.key:
            __main__.keyTyped()
    elif event.type==pygame.KEYUP:
        _P.keyPressed=False
        # Remove from pressed set
        if event.key in _keycode_map:
            _P._keys_down.discard(_keycode_map[event.key])
        else:
            try:
                if event.unicode:
                    _P._keys_down.discard(event.unicode)
            except:
                pass
        if callable(getattr(__main__,'keyReleased',None)):
            __main__.keyReleased()

# -------------------------- Run loop --------------------------
def run():
    pygame.init()
    
    # Apply startup cursor state
    if not _P._cursor_visible:
        pygame.mouse.set_visible(False)
    else:
        if _P._cursor_image:
            cursor(_P._cursor_image, *_P._cursor_hotspot)
        elif _P._cursor_mode:
            cursor(_P._cursor_mode)
        else:
            cursor()

    size(_P.width,_P.height)
    pygame.display.set_caption(_P._window_title)
    _P._clock = pygame.time.Clock()

    _P._running = True
    _P._last_time = time.time()
    _P._last_time_global = _P._last_time
    _P._last_frame_time = None  # Frame-Zeit initialisieren

    setup_fn = getattr(__main__,'setup',None)
    if callable(setup_fn):
        setup_fn()

    draw_fn = getattr(__main__,'draw',None)
    if callable(draw_fn):
        while _P._running:
            for event in pygame.event.get():
                if event.type==pygame.QUIT:
                    _P._running=False
                elif event.type in (pygame.MOUSEBUTTONDOWN, pygame.MOUSEWHEEL):
                    _update_mouse(event)
                    _update_mouse_wheel(event)
                elif event.type in (pygame.MOUSEMOTION, pygame.MOUSEBUTTONUP):
                    _update_mouse(event)
                elif event.type in (pygame.KEYDOWN,pygame.KEYUP):
                    _update_keyboard(event)

            if callable(draw_fn):
                # Wenn looping aktiv oder redraw angefordert
                if _P._looping or _P._draw_next_frame:
                    try:
                        draw_fn()
                    except Exception:
                        import traceback; traceback.print_exc()
                        _P._running=False
                        break
                    _P._draw_next_frame = False  # redraw zurücksetzen

            pygame.display.get_surface().blit(_P.surface,(0,0))
            pygame.display.flip()

            # --- Framerate-Messung (Processing-like) ---
            now = time.time()
            if _P._last_frame_time is None:
                _P._last_frame_time = now
            else:
                dt = now - _P._last_frame_time
                if dt > 0:
                    instant_fps = 1.0 / dt
                    # sanfte Glättung wie Processing
                    _P.frameRate = (_P.frameRate * 0.9) + (instant_fps * 0.1)
                _P._last_frame_time = now

            _P.frameCount += 1
            _P._clock.tick(_P._target_fps)


    pygame.quit()
    
# ---------------- now inject everything -------------------

# -------------------------- Inject all variables --------------------------
_PUBLIC_FUNCTIONS = [
    # control
    run, exit, loop, noLoop, redraw,

    # time
    millis, second, minute, hour, day, month, year, weekday, timestamp,

    # random
    random, randomSeed, randomGaussian, map,

    # input
    getPressed,

    # window
    size, windowTitle, smooth, noSmooth, cursor, noCursor,

    # text
    text, textSize, textAlign, textFont,

    # color
    color, colorMode, lerpColor,
    alpha, red, green, blue, hue, saturation, brightness,

    # style
    background, clear, fill, noFill, stroke, noStroke,
    strokeWeight, rectMode, ellipseMode,

    # shapes
    line, point, rect, ellipse, circle, triangle,
]

def _inject_all():
    # first those three variables for which a function with the same name (might) exist
    _inject_var("mousePressed", dual=True)
    _inject_var("keyPressed", dual=True)
    _inject_var("frameRate", dual=True)
#    _inject_var("frameRate")      # Variable (gemessene FPS)
#    _inject_fun("frameRate")      # Funktion (Setter)
    # all system variables
    for var in ["width","height","mouseX","mouseY","pmouseX","pmouseY","mouseButton",
                "key","keyCode","frameCount"]:
        _inject_var(var)

    # inject everything else that is callable and not
    # yet injected as functions in camelCase and snake_case
    for fn in _PUBLIC_FUNCTIONS:
        _inject_fun(fn.__name__)
    
#    for name, attr in globals().items():
#        if name.startswith("_"):
#           continue  # interne Dinge überspringen

#        if isinstance(attr, types.FunctionType) or isinstance(attr, types.BuiltinFunctionType):
#            # Wenn noch nicht im __main__ vorhanden
#            if not hasattr(__main__, name):
#                _inject_fun(name)

def _inject_key_constants_into_main():
    for name in _PROCESSING_KEY_CONSTANTS:
        setattr(__main__, name, name)
    for name in _CURSOR_CONSTANTS:
        setattr(__main__, name, name)
    for name,val in _CONSTANTS.items():
        # Typ prüfen: float/int → direkt, alles andere → String
        if isinstance(val, (int, float)):
            setattr(__main__, name, val)
        else:
            setattr(__main__, name, str(val))
    __main__.RETURN = "ENTER"


_inject_all()  
_inject_key_constants_into_main()

# -------------------------- Demo --------------------------
if __name__=='__main__':
    def setup():
        size(400,400)
        frameRate(60)        

    def draw():
        background(25,25,30)
        t = frameCount/60.0
        cx,cy = width/2,height/2
        c = color(255,180,0)
        fill(c)
        noStroke()
        circle(cx,cy,120)
        stroke(255)
        strokeWeight(2)
        for i in range(12):
            a = t + i*2*math.pi/12
            line(cx,cy,cx+math.cos(a)*140,cy+math.sin(a)*140)

        if getPressed(ESC):
            exit()

    run()

