import js
from pyodide.ffi import create_proxy
import random
import json
from datetime import datetime, timezone
from random import randint, choice
import time as _time

# ── Constants ──
CELL_SIZE = 20
MIN_SNAKE_SPEED = 0.05

DIFFICULTY_SETTINGS = {
    'EASY': {'speed_multiplier': 0.8, 'food_count': 3},
    'MEDIUM': {'speed_multiplier': 1.0, 'food_count': 1},
    'HARD': {'speed_multiplier': 3.0, 'food_count': 1}
}

SNAKE_SKINS = {
    'green': {'name': 'Green', 'head': 'assets/images/snake_head_green.png', 'body': 'assets/images/snake_body_green.png', 'tail': 'assets/images/snake_tail_green.png'},
    'red': {'name': 'Red', 'head': 'assets/images/snake_head_red.png', 'body': 'assets/images/snake_body_red.png', 'tail': 'assets/images/snake_tail_red.png'}
}

BACKGROUNDS = {
    'meadow': 'assets/wallpapers/wallpaper_meadow.gif',
    'fern forest': 'assets/wallpapers/wallpaper_fern_forest.gif',
    'sands and bones': 'assets/wallpapers/wallpaper_sands_and_bones.gif'
}

FOOD_SOURCES = ["assets/images/food_0.png", "assets/images/food_1.png", "assets/images/food_2.png", "assets/images/food_3.png", "assets/images/food_4.png", "assets/images/food_5.png", "assets/images/food_6.png", "assets/images/food_7.png"]

GRAPHICS_MODES = ['classic', 'modern']
CLASSIC_FOOD_COLOR = "#FF8800"
CLASSIC_SNAKE_COLORS = {
    'green': '#00FF00',
    'red': '#FF4444'
}

DEFAULT_SETTINGS = {
    "difficulty": "MEDIUM", "controls": "arrows", "snake_skin": "green",
    "background": "random", "snake_speed": 1.0, "show_grid": False,
    "sound_enabled": True, "graphics_mode": "modern"
}

# ── Game State ──
class GameState:
    def __init__(self):
        self.cell_size = CELL_SIZE
        self.cols = 60
        self.rows = 40
        self.snake = [(5, 5)]
        self.foods = []
        self.food_sources = []
        self.food_count = 1
        self.direction = (1, 0)
        self.last_direction = (1, 0)
        self.score = 0
        self.snake_speed = 0.2
        self.snake_skin = "green"
        self.is_paused = False
        self.game_over_active = False

    def update_grid_size(self, canvas_width, canvas_height):
        self.cols = max(10, int(canvas_width // self.cell_size))
        self.rows = max(10, int(canvas_height // self.cell_size))

    def _pick_food_source(self, used_sources):
        available = [s for s in FOOD_SOURCES if s not in used_sources]
        if not available:
            available = list(FOOD_SOURCES)
        return choice(available)

    def spawn_food(self):
        self.update_grid_size(self.cols * self.cell_size, self.rows * self.cell_size)
        self.foods = []
        self.food_sources = []
        used_sources = []
        max_y = max(0, self.rows - 4)
        for _ in range(self.food_count):
            food_pos = (randint(0, self.cols - 1), randint(0, max_y))
            while food_pos in self.snake or food_pos in self.foods:
                food_pos = (randint(0, self.cols - 1), randint(0, max_y))
            self.foods.append(food_pos)
            source = self._pick_food_source(used_sources)
            used_sources.append(source)
            self.food_sources.append(source)

    def spawn_single_food(self):
        max_y = max(0, self.rows - 4)
        food_pos = (randint(0, self.cols - 1), randint(0, max_y))
        while food_pos in self.snake or food_pos in self.foods:
            food_pos = (randint(0, self.cols - 1), randint(0, max_y))
        self.foods.append(food_pos)
        self.food_sources.append(self._pick_food_source(self.food_sources))

    def update(self):
        if self.is_paused or self.game_over_active:
            return None
        head_x, head_y = self.snake[-1]
        new_head = (head_x + self.direction[0], head_y + self.direction[1])
        if not (0 <= new_head[0] < self.cols and 0 <= new_head[1] < self.rows):
            return self.game_over()
        if new_head in self.snake[:-1]:
            return self.game_over()
        ate = False
        if new_head in self.foods:
            idx = self.foods.index(new_head)
            self.foods.pop(idx)
            self.food_sources.pop(idx)
            self.score += 1
            self.snake_speed = max(MIN_SNAKE_SPEED, 0.2 - (self.score * 0.01))
            ate = True
            if len(self.foods) < self.food_count:
                self.spawn_single_food()
        else:
            self.snake.pop(0)
        self.snake.append(new_head)
        self.last_direction = self.direction
        return ate

    def game_over(self):
        self.game_over_active = True
        return "game_over"

    def reset_game(self):
        self.snake = [(5, 5)]
        self.foods = []
        self.food_sources = []
        self.direction = (1, 0)
        self.last_direction = (1, 0)
        self.score = 0
        self.snake_speed = 0.2
        self.is_paused = False
        self.game_over_active = False

    def set_direction(self, new_dir):
        if new_dir == (0, 1) and self.last_direction != (0, -1):
            self.direction = (0, 1)
        elif new_dir == (0, -1) and self.last_direction != (0, 1):
            self.direction = (0, -1)
        elif new_dir == (1, 0) and self.last_direction != (-1, 0):
            self.direction = (1, 0)
        elif new_dir == (-1, 0) and self.last_direction != (1, 0):
            self.direction = (-1, 0)

    def get_angle(self, direction):
        if direction == (1, 0): return 0
        elif direction == (0, 1): return 90
        elif direction == (-1, 0): return 180
        elif direction == (0, -1): return 270
        return 0

    def get_current_skin_assets(self):
        skin = SNAKE_SKINS.get(self.snake_skin, SNAKE_SKINS['green'])
        return skin['head'], skin['body'], skin['tail']

# ── Settings Manager ──
class SettingsManager:
    def __init__(self):
        self.settings = self.load_settings()

    def load_settings(self):
        try:
            data = js.localStorage.getItem("snake_settings")
            if data:
                return json.loads(str(data))
        except Exception:
            pass
        return dict(DEFAULT_SETTINGS)

    def save_settings(self):
        try:
            js.localStorage.setItem("snake_settings", json.dumps(self.settings))
            return True
        except Exception:
            return False

    def get_current_settings(self):
        return self.settings

    def set_difficulty(self, d):
        if d in DIFFICULTY_SETTINGS:
            self.settings["difficulty"] = d
            self.save_settings()
            return True
        return False

    def set_controls(self, c):
        if c in ["arrows", "wasd", "both"]:
            self.settings["controls"] = c
            self.save_settings()
            return True
        return False

    def set_snake_skin(self, s):
        if s in ["green", "red"]:
            self.settings["snake_skin"] = s
            self.save_settings()
            return True
        return False

    def set_background(self, w):
        if w in ["random", "meadow", "fern forest", "sands and bones"]:
            self.settings["background"] = w
            self.save_settings()
            return True
        return False

    def set_sound_enabled(self, val):
        enabled = val == "True"
        self.settings["sound_enabled"] = enabled
        self.save_settings()
        AudioManager().set_enabled(enabled)
        return True

    def set_graphics_mode(self, mode):
        if mode in ["classic", "modern"]:
            self.settings["graphics_mode"] = mode
            self.save_settings()
            return True
        return False

    def get_graphics_mode(self):
        return self.settings.get("graphics_mode", "modern")

    def reset_to_defaults(self):
        self.settings = dict(DEFAULT_SETTINGS)
        self.save_settings()

DEFAULT_HIGH_SCORES = [
    {"score":100,"player_name":"SNAKE","difficulty":"MEDIUM","date":"2026-01-01 12:00","timestamp":1735744800.0},
    {"score":75,"player_name":"VIPER","difficulty":"EASY","date":"2026-01-02 14:30","timestamp":1735830600.0},
    {"score":60,"player_name":"COBRA","difficulty":"MEDIUM","date":"2026-01-03 09:15","timestamp":1735904100.0},
    {"score":50,"player_name":"PYTHON","difficulty":"HARD","date":"2026-01-04 18:45","timestamp":1736019900.0},
    {"score":40,"player_name":"ADDER","difficulty":"MEDIUM","date":"2026-01-05 11:00","timestamp":1736079600.0},
    {"score":30,"player_name":"MAMBA","difficulty":"EASY","date":"2026-01-06 16:20","timestamp":1736181600.0},
    {"score":25,"player_name":"BOA","difficulty":"MEDIUM","date":"2026-01-07 13:40","timestamp":1736264400.0},
    {"score":20,"player_name":"RATTLER","difficulty":"HARD","date":"2026-01-08 20:00","timestamp":1736376000.0},
    {"score":15,"player_name":"TAIPAN","difficulty":"EASY","date":"2026-01-09 10:30","timestamp":1736416800.0},
    {"score":10,"player_name":"KRAIT","difficulty":"MEDIUM","date":"2026-01-10 15:50","timestamp":1736526600.0}
]

# ── High Score Manager ──
class HighScoreManager:
    def __init__(self):
        self.high_scores = []
        self.max_scores = 10
        self.load_scores()

    def load_scores(self):
        try:
            data = js.localStorage.getItem("snake_high_scores")
            if data:
                parsed = json.loads(str(data))
                self.high_scores = parsed.get('scores', [])
                return
        except Exception:
            pass
        self.high_scores = [dict(d) for d in DEFAULT_HIGH_SCORES]

    def save_scores(self):
        try:
            data = {'scores': self.high_scores, 'last_updated': datetime.now().isoformat()}
            js.localStorage.setItem("snake_high_scores", json.dumps(data))
        except Exception as e:
            print(f"Error saving high scores: {e}")

    def add_score(self, score, player_name="Player", difficulty="MEDIUM"):
        ts = datetime.now(timezone.utc).timestamp()
        entry = {'score': score, 'player_name': player_name, 'difficulty': difficulty,
                 'date': datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M") + " UTC", 'timestamp': ts}
        inserted = False
        for i, ex in enumerate(self.high_scores):
            if score > ex['score']:
                self.high_scores.insert(i, entry)
                inserted = True
                break
        if not inserted and len(self.high_scores) < self.max_scores:
            self.high_scores.append(entry)
            inserted = True
        elif inserted and len(self.high_scores) > self.max_scores:
            self.high_scores = self.high_scores[:self.max_scores]
        if inserted:
            self.save_scores()
        return inserted

    def get_top_scores(self, limit=10):
        return self.high_scores[:limit]

    def is_high_score(self, score):
        if not self.high_scores or len(self.high_scores) < self.max_scores:
            return True
        return score > self.high_scores[-1]['score']

    def get_best_score(self):
        return self.high_scores[0] if self.high_scores else None

    def get_score_count(self):
        return len(self.high_scores)

    def clear_all_scores(self):
        self.high_scores = []
        self.save_scores()

    def reset_to_default(self):
        self.high_scores = [dict(d) for d in DEFAULT_HIGH_SCORES]
        self.save_scores()
        return True

# ── Music Track Lists ──
MENU_TRACKS = [
    "assets/audio/paradise-by-next-route.mp3"
]
GAME_TRACKS = [
    "assets/audio/remote-location-by-filo-starquez.mp3",
    "assets/audio/retroverse-pt1-by-lucjo.mp3",
    "assets/audio/dreamscape-by-audio-library-beats.mp3",
    "assets/audio/dirt-rhodes-by-kevin-macleod.mp3"
]

# ── Audio Manager ──
class AudioManager:
    _instance = None
    def __new__(cls):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
            cls._instance._initialized = False
        return cls._instance

    def __init__(self):
        if self._initialized:
            return
        self._initialized = True
        self.bgm_volume = 0.5
        self.bgm_enabled = True
        self.current_context = None
        self._current_audio = None

    def set_volume(self, v):
        self.bgm_volume = max(0.0, min(1.0, v))
        if self._current_audio:
            self._current_audio.volume = self.bgm_volume

    def set_enabled(self, e):
        self.bgm_enabled = e
        if not e:
            self.stop_bgm()

    def play_bgm(self, track='menu'):
        if not self.bgm_enabled:
            return
        self.stop_bgm()
        tracks = MENU_TRACKS if track == 'menu' else GAME_TRACKS
        src = random.choice(tracks)
        try:
            js.window.__audioPlay(src, self.bgm_volume)
            self._current_audio = js.window.__audioBGM
        except Exception as e:
            print(f"[audio] JS bridge failed: {e}, falling back")
            if self._current_audio is None:
                a = js.document.createElement("audio")
                a.id = "bgm-player"
                a.loop = True
                js.document.body.appendChild(a)
                self._current_audio = a
            a = self._current_audio
            a.src = src
            a.volume = self.bgm_volume
            a.currentTime = 0
            p = a.play()
            if p is not None:
                def _on_err(e):
                    print(f"[audio] playback error: {e}")
                p.catch(_on_err)

    def set_context(self, ctx):
        if ctx == self.current_context:
            return
        self.play_bgm('game' if ctx == 'game' else 'menu')
        self.current_context = ctx

    def stop_bgm(self):
        try:
            js.window.__audioStop()
        except Exception:
            pass
        if self._current_audio:
            self._current_audio.pause()
            self._current_audio.currentTime = 0

# ── Canvas Renderer ──
class CanvasRenderer:
    def __init__(self, canvas_id="game-canvas"):
        self.canvas = js.document.getElementById(canvas_id)
        self.ctx = self.canvas.getContext("2d") if self.canvas else None
        self.images = {}
        self.graphics_mode = "modern"
        self.current_skin = "green"

    def preload_images(self, paths, callback=None):
        loaded = []
        total = len(paths)
        for path in paths:
            img = js.Image.new()
            img.addEventListener("load", lambda e, p=path: (
                loaded.append(p),
                callback() if callback and len(loaded) >= total else None
            ))
            img.src = path
            self.images[path] = img

    def update_size(self, w, h):
        if self.canvas:
            self.canvas.width = w
            self.canvas.height = h

    def clear(self, color=(0, 0, 0, 0)):
        if not self.ctx:
            return
        if self.graphics_mode == "classic":
            self.ctx.fillStyle = "#000000"
            self.ctx.fillRect(0, 0, self.canvas.width, self.canvas.height)
            self._draw_boundary()
        else:
            self.ctx.clearRect(0, 0, self.canvas.width, self.canvas.height)

    def _draw_boundary(self):
        cs = CELL_SIZE
        cols = self.canvas.width // cs
        rows = self.canvas.height // cs
        self.ctx.strokeStyle = "#ffffff"
        self.ctx.lineWidth = 2
        self.ctx.strokeRect(0, 0, cols * cs, rows * cs)

    def draw_grid(self, cols, rows, cs):
        if not self.ctx:
            return
        self.ctx.strokeStyle = "rgba(50,50,50,0.3)"
        self.ctx.lineWidth = 0.5
        for x in range(0, cols * cs + 1, cs):
            self.ctx.beginPath()
            self.ctx.moveTo(x, 0)
            self.ctx.lineTo(x, rows * cs)
            self.ctx.stroke()
        for y in range(0, rows * cs + 1, cs):
            self.ctx.beginPath()
            self.ctx.moveTo(0, y)
            self.ctx.lineTo(cols * cs, y)
            self.ctx.stroke()

    def _draw_segment(self, img_path, x, y, cs, angle=0):
        img = self.images.get(img_path)
        if not img:
            self.ctx.fillStyle = "#00ff00"
            self.ctx.fillRect(x * cs, y * cs, cs, cs)
            return
        cx = x * cs + cs / 2
        cy = y * cs + cs / 2
        self.ctx.save()
        self.ctx.translate(cx, cy)
        self.ctx.rotate(angle * 3.14159 / 180)
        self.ctx.drawImage(img, -cs / 2, -cs / 2, cs, cs)
        self.ctx.restore()

    def draw_snake(self, snake, direction, cs, skin_assets):
        if not self.ctx or not snake:
            return
        if self.graphics_mode == "classic":
            return self._draw_snake_classic(snake, cs)
        head, body, tail = skin_assets
        for i, (sx, sy) in enumerate(snake):
            if i == len(snake) - 1:
                a = self._angle(direction)
                self._draw_segment(head, sx, sy, cs, a)
            elif i == 0:
                td = (snake[1][0] - sx, snake[1][1] - sy) if len(snake) > 1 else direction
                self._draw_segment(tail, sx, sy, cs, self._angle(td))
            else:
                bd = (snake[i-1][0] - sx, snake[i-1][1] - sy)
                self._draw_segment(body, sx, sy, cs, self._angle(bd))

    def _draw_snake_classic(self, snake, cs):
        color = CLASSIC_SNAKE_COLORS.get(self.current_skin, "#00FF00")
        self.ctx.fillStyle = color
        for sx, sy in snake:
            self.ctx.fillRect(sx * cs, sy * cs, cs, cs)

    def _angle(self, d):
        if d == (1, 0): return 0
        elif d == (0, 1): return 90
        elif d == (-1, 0): return 180
        elif d == (0, -1): return 270
        return 0

    def draw_food(self, foods, sources, cs):
        if not self.ctx:
            return
        if self.graphics_mode == "classic":
            return self._draw_food_classic(foods, cs)
        for i, (fx, fy) in enumerate(foods):
            src = sources[i] if i < len(sources) else None
            img = self.images.get(src) if src else None
            if img:
                self.ctx.drawImage(img, fx * cs, fy * cs, cs, cs)
            else:
                self.ctx.fillStyle = "#ff0000"
                self.ctx.beginPath()
                self.ctx.arc(fx * cs + cs/2, fy * cs + cs/2, cs/3, 0, 6.28318)
                self.ctx.fill()

    def _draw_food_classic(self, foods, cs):
        for fx, fy in foods:
            cx = fx * cs + cs // 2
            cy = fy * cs + cs // 2
            self.ctx.beginPath()
            self.ctx.arc(cx, cy, cs // 3, 0, 6.28318)
            self.ctx.fillStyle = CLASSIC_FOOD_COLOR
            self.ctx.fill()

    def draw_score(self, text, cw, ch):
        if not self.ctx:
            return
        self.ctx.fillStyle = "white"
        self.ctx.font = "bold 24px Arial"
        self.ctx.textAlign = "right"
        self.ctx.textBaseline = "top"
        self.ctx.fillText(text, cw - 15, 15)

# ── Screen Helpers ──
def show_screen(name):
    screens = ["loading", "dev_splash", "splash", "game", "options", "highscores", "help", "credits", "thankyou"]
    for s in screens:
        el = js.document.getElementById(f"screen-{s}")
        if el:
            el.style.display = "flex" if s == name else "none"

def get(id):
    return js.document.getElementById(id)

def set_text(id, text):
    el = get(id)
    if el:
        el.textContent = text

def show_pause():
    el = get("pause-menu")
    if el: el.style.display = "flex"

def hide_pause():
    el = get("pause-menu")
    if el: el.style.display = "none"

def show_go(data):
    el = get("game-over-overlay")
    if not el: return
    el.style.display = "flex"
    is_h = data.get("is_high_score", False)
    t = get("game-over-title")
    if t:
        t.textContent = "NEW HIGH SCORE!" if is_h else "Game Over!"
        t.style.color = "gold" if is_h else "#ff4444"
    s = get("game-over-score")
    if s: s.textContent = f"Your Score: {data.get('score', 0)}"
    b = get("game-over-best")
    if b: b.textContent = f"Highest Score Ever: {data.get('best_score', 0)}"
    na = get("game-over-name-area")
    if na: na.style.display = "block" if is_h else "none"
    nd = get("name-display")
    if nd: nd.textContent = "_"

def hide_go():
    el = get("game-over-overlay")
    if el: el.style.display = "none"

def update_name(text):
    el = get("name-display")
    if el: el.textContent = text if text else "_"

_notify_proxies = []
def notify(msg):
    el = get("notification")
    if el:
        el.textContent = msg
        el.style.display = "block"
        p = create_proxy(lambda: setattr(el.style, 'display', 'none'))
        _notify_proxies.append(p)
        js.setTimeout(p, 3000)

# ── Main App ──
class SnakeApp:
    def __init__(self):
        self.settings = SettingsManager()
        self.high_scores = HighScoreManager()
        self.audio = AudioManager()
        self.renderer = CanvasRenderer("game-canvas")
        self.gs = GameState()
        self.game_interval = None
        self.render_interval = None
        self._gt = None
        self._rt = None
        self.player_name = ""
        self.current_score = 0
        self.is_high = False
        self._dev_timer_ids = []
        self._dev_proxies = []
        self._proxies = []
        self._setup()

    def _setup(self):
        self._bind()
        show_screen("loading")
        self._preload_all()

    def _preload_all(self):
        paths = []
        for s in SNAKE_SKINS.values():
            paths.extend([s['head'], s['body'], s['tail']])
        paths.extend(FOOD_SOURCES)
        paths += [
            "assets/images/snake_logo.png",
            "assets/images/dev_logos_1.png",
            "assets/images/title_text.png",
            "assets/images/main_bkgd.png",
        ]
        for i in range(1, 4):
            paths.append(f"assets/images/medal_{i}.png")
        paths.extend(BACKGROUNDS.values())

        self._load_total = len(paths)
        self._load_count = 0

        def on_loaded(event=None):
            self._load_count += 1
            pct = int(self._load_count / self._load_total * 100)
            bar = get("loading-bar-fill")
            if bar: bar.style.width = f"{pct}%"
            txt = get("loading-text")
            if txt: txt.textContent = f"Loading... {pct}%"
            if self._load_count >= self._load_total:
                self._on_preload_done()

        proxy = create_proxy(on_loaded)
        self._proxies.append(proxy)

        for path in paths:
            img = js.Image.new()
            img.addEventListener("load", proxy)
            img.addEventListener("error", proxy)
            img.src = path
            self.renderer.images[path] = img

    def _on_preload_done(self):
        show_screen("dev_splash")
        self._dev_start()

    def _dev_start(self):
        logo = get("dev-logo")
        if logo:
            logo.style.opacity = "0"
            logo.style.transition = "opacity 2s"
            p = create_proxy(lambda: self._dev_fade_in())
            self._dev_proxies.append(p)
            self._dev_timer_ids.append(js.setTimeout(p, 50))

    def _dev_fade_in(self):
        logo = get("dev-logo")
        if logo: logo.style.opacity = "1"
        p = create_proxy(self._dev_hold)
        self._dev_proxies.append(p)
        self._dev_timer_ids.append(js.setTimeout(p, 2000))

    def _dev_hold(self):
        p = create_proxy(self._dev_fade_out)
        self._dev_proxies.append(p)
        self._dev_timer_ids.append(js.setTimeout(p, 2000))

    def _dev_fade_out(self):
        logo = get("dev-logo")
        if logo: logo.style.opacity = "0"
        p = create_proxy(lambda: self._show_menu())
        self._dev_proxies.append(p)
        self._dev_timer_ids.append(js.setTimeout(p, 2000))

    def _dev_skip(self, e):
        for tid in self._dev_timer_ids:
            js.clearTimeout(tid)
        self._dev_timer_ids = []
        self._dev_proxies = []
        logo = get("dev-logo")
        if logo:
            logo.style.transition = "none"
            logo.style.opacity = "0"
        d = get("screen-dev_splash")
        if d and d.style.display != "none":
            self._show_menu()

    def _bind(self):
        self._clk("btn-start", lambda e: self._start_game())
        self._clk("btn-highscores", lambda e: self._show_hs())
        self._clk("btn-options", lambda e: self._show_opts())
        self._clk("btn-help", lambda e: self._show_help())
        self._clk("btn-credits", lambda e: self._show_credits())
        self._clk("screen-dev_splash", self._dev_skip)
        self._clk("btn-play-again", lambda e: self._play_again())
        self._clk("btn-game-over-menu", lambda e: self._go_menu())
        self._clk("btn-pause-resume", lambda e: self._resume())
        self._clk("btn-pause-restart", lambda e: self._restart())
        self._clk("btn-pause-menu", lambda e: self._pause_menu())

        self._chg("opt-difficulty", lambda e: self._opt("difficulty", e.target.value))
        self._chg("opt-background", lambda e: self._opt("background", e.target.value))
        self._chg("opt-controls", lambda e: self._opt("controls", e.target.value))
        self._chg("opt-skin", lambda e: self._opt("snake_skin", e.target.value))
        self._chg("opt-sound", lambda e: self._opt("sound", e.target.value))
        self._chg("opt-graphics-mode", lambda e: self._on_graphics_mode_change(e))
        self._clk("btn-options-back", lambda e: self._show_menu())
        self._clk("btn-hs-back", lambda e: self._show_menu())
        self._clk("btn-hs-clear", lambda e: (self.high_scores.clear_all_scores(), self._pop_hs()))
        self._clk("btn-hs-reset", lambda e: (self.high_scores.reset_to_default(), self._pop_hs()))
        self._clk("btn-help-back", lambda e: self._show_menu())
        self._clk("btn-credits-back", lambda e: show_screen("splash"))
        self._clk("btn-thx-play", lambda e: self._start_game())
        self._clk("btn-thx-menu", lambda e: show_screen("splash"))

        canvas = self.renderer.canvas
        if canvas:
            tp = create_proxy(self._handle_tap)
            self._proxies.append(tp)
            canvas.addEventListener("click", tp)
            canvas.addEventListener("touchstart", tp)

        def on_key(event):
            key = event.key.lower()
            d = get("screen-dev_splash")
            if d and js.window.getComputedStyle(d).display != "none":
                self._dev_skip(event); return
            if self.gs.game_over_active:
                if self.is_high:
                    if key == "enter": self._submit()
                    elif key == "backspace": self.player_name = self.player_name[:-1]; update_name(self.player_name)
                    elif len(key) == 1 and key.isalnum() and len(self.player_name) < 10:
                        self.player_name += key.upper(); update_name(self.player_name)
                elif key in ("enter", " "):
                    self._play_again()
                return
            g = get("screen-game")
            if g and js.window.getComputedStyle(g).display != "none":
                if key in ("escape", "p"): self._toggle_pause(); return
                if not self.gs.is_paused:
                    ctrl = self.settings.get_current_settings().get("controls", "arrows")
                    dm = {"arrowup": (0,-1), "arrowdown": (0,1), "arrowright": (1,0), "arrowleft": (-1,0),
                          "w": (0,-1), "s": (0,1), "d": (1,0), "a": (-1,0)}
                    if key in dm:
                        if (ctrl in ("arrows","both") and key.startswith("arrow")) or \
                           (ctrl in ("wasd","both") and not key.startswith("arrow")):
                            self.gs.set_direction(dm[key])

        self._kp = create_proxy(on_key)
        js.document.addEventListener("keydown", self._kp)

    def _handle_tap(self, event):
        if self.gs.is_paused or self.gs.game_over_active:
            return
        screen_g = get("screen-game")
        if not screen_g or js.window.getComputedStyle(screen_g).display == "none":
            return
        rect = self.renderer.canvas.getBoundingClientRect()
        client_x = event.clientX if hasattr(event, 'clientX') else event.touches[0].clientX
        client_y = event.clientY if hasattr(event, 'clientY') else event.touches[0].clientY
        tap_x = int((client_x - rect.left) / self.gs.cell_size)
        tap_y = int((client_y - rect.top) / self.gs.cell_size)
        head = self.gs.snake[-1]
        dx = tap_x - head[0]
        dy = tap_y - head[1]
        if dx == 0 and dy == 0:
            return

        current = self.gs.direction
        if abs(dx) > abs(dy):
            intended = (1, 0) if dx > 0 else (-1, 0)
        else:
            intended = (0, 1) if dy > 0 else (0, -1)

        if intended == current and dx != 0 and dy != 0:
            if abs(dx) > abs(dy):
                new_dir = (0, 1) if dy > 0 else (0, -1)
            else:
                new_dir = (1, 0) if dx > 0 else (-1, 0)
        elif intended == (-current[0], -current[1]):
            cross = current[0] * dy - current[1] * dx
            if cross > 0:
                new_dir = (-current[1], current[0])
            elif cross < 0:
                new_dir = (current[1], -current[0])
            else:
                new_dir = (current[1], -current[0])
        else:
            new_dir = intended

        self.gs.set_direction(new_dir)
        if hasattr(event, 'preventDefault'):
            event.preventDefault()

    def _clk(self, id, handler):
        el = get(id)
        if el:
            p = create_proxy(handler)
            self._proxies.append(p)
            el.addEventListener("click", p)

    def _chg(self, id, handler):
        el = get(id)
        if el:
            p = create_proxy(handler)
            self._proxies.append(p)
            el.addEventListener("change", p)

    def _start_game(self):
        s = self.settings.get_current_settings()
        if s.get("graphics_mode") == "classic":
            self.audio.stop_bgm()
        else:
            self.audio.set_context('game')
        show_screen("game")
        self._init_game()

    def _init_game(self):
        s = self.settings.get_current_settings()
        diff = s.get("difficulty", "MEDIUM")
        d = DIFFICULTY_SETTINGS.get(diff, DIFFICULTY_SETTINGS["MEDIUM"])
        self.gs = GameState()
        self.gs.snake_skin = s.get("snake_skin", "green")
        self.gs.snake_speed = 0.2 / d["speed_multiplier"]
        self.gs.food_count = d.get("food_count", 1)
        self.player_name = ""

        graphics_mode = s.get("graphics_mode", "modern")
        self.renderer.graphics_mode = graphics_mode
        self.renderer.current_skin = s.get("snake_skin", "green")

        if graphics_mode == "classic":
            ga = get("game-area")
            if ga:
                ga.style.backgroundImage = ""
            canvas = js.document.getElementById("game-canvas")
            if canvas:
                canvas.style.backgroundImage = ""
                w = canvas.clientWidth or 1180
                h = canvas.clientHeight or 700
                self.renderer.update_size(w, h)
                self.gs.update_grid_size(w, h)
            print("[background] classic mode - no wallpaper")
        else:
            bg = s.get("background", "random")
            if bg == "random":
                bg = random.choice(list(BACKGROUNDS.keys()))
            bg_path = BACKGROUNDS.get(bg, "")
            cache_buster = int(_time.time() * 1000)
            ga = get("game-area")
            if ga:
                ga.style.backgroundImage = f"url('{bg_path}?v={cache_buster}')" if bg_path else ""
                ga.style.backgroundSize = "cover"
                ga.style.backgroundPosition = "center"
            canvas = js.document.getElementById("game-canvas")
            if canvas:
                canvas.style.backgroundImage = f"url('{bg_path}?v={cache_buster}')" if bg_path else ""
                canvas.style.backgroundSize = "cover"
                canvas.style.backgroundPosition = "center"
                w = canvas.clientWidth or 1180
                h = canvas.clientHeight or 700
                self.renderer.update_size(w, h)
                self.gs.update_grid_size(w, h)
            print(f"[background] setting='{s.get('background')}' resolved='{bg}' path='{bg_path}'")

        self.gs.spawn_food()
        self._start_loop()

    def _start_loop(self):
        self._stop_loop()
        self.gs.is_paused = False
        self.gs.game_over_active = False

        def tick():
            if self.gs.is_paused or self.gs.game_over_active:
                return
            os = self.gs.snake_speed
            r = self.gs.update()
            if r == "game_over":
                self._on_go()
            elif r is True and self.gs.snake_speed != os:
                self._resched()

        def render():
            if self.gs.game_over_active:
                return
            canvas = js.document.getElementById("game-canvas")
            if not canvas:
                return
            self.renderer.clear((0, 0, 0, 0))
            cs = self.gs.cell_size
            sk = self.gs.get_current_skin_assets()
            self.renderer.draw_snake(self.gs.snake, self.gs.direction, cs, sk)
            self.renderer.draw_food(self.gs.foods, self.gs.food_sources, cs)
            self.renderer.draw_score(f"Score: {self.gs.score}", canvas.width, canvas.height)

        interval = int(self.gs.snake_speed * 1000)
        self._gt = create_proxy(tick)
        self._rt = create_proxy(render)
        self.game_interval = js.setInterval(self._gt, interval)
        self.render_interval = js.setInterval(self._rt, 33)

    def _stop_loop(self):
        if self.game_interval:
            js.clearInterval(self.game_interval)
            self.game_interval = None
        if self.render_interval:
            js.clearInterval(self.render_interval)
            self.render_interval = None
        self._gt = None
        self._rt = None

    def _resched(self):
        if self.game_interval and self._gt:
            js.clearInterval(self.game_interval)
        self.game_interval = js.setInterval(self._gt, int(self.gs.snake_speed * 1000))

    def _on_go(self):
        self._stop_loop()
        score = self.gs.score
        self.current_score = score
        self.is_high = self.high_scores.is_high_score(score)
        best = self.high_scores.get_best_score()
        highest = best['score'] if best else 0
        show_go({"score": score, "is_high_score": self.is_high, "best_score": highest})

    def _submit(self):
        name = self.player_name if self.player_name else "ANONYMOUS"
        if len(name) > 10:
            name = name[:10]
        s = self.settings.get_current_settings()
        self.high_scores.add_score(self.current_score, name, s.get("difficulty", "MEDIUM"))
        c = get("score-saved-confirmation")
        if c:
            c.textContent = f"Score saved: {name} - {self.current_score}"
            c.style.display = "block"
        self.is_high = False

    def _play_again(self):
        hide_go()
        self._init_game()
        show_screen("game")

    def _go_menu(self):
        hide_go()
        self._stop_loop()
        self._show_menu()

    def _toggle_pause(self):
        if self.gs.game_over_active:
            return
        if self.gs.is_paused:
            self._resume()
        else:
            self.gs.is_paused = True
            show_pause()

    def _resume(self):
        if self.gs.is_paused:
            self.gs.is_paused = False
            hide_pause()

    def _restart(self):
        hide_pause()
        self._init_game()

    def _pause_menu(self):
        hide_pause()
        self._stop_loop()
        self.gs.is_paused = False
        self._show_menu()

    def _pause_exit(self):
        hide_pause()
        self._stop_loop()
        self.gs.is_paused = False
        if self.settings.get_current_settings().get("graphics_mode") == "classic":
            self.audio.stop_bgm()
        else:
            self.audio.set_context('menu')
        show_screen("thankyou")

    def _show_hs(self):
        if self.settings.get_current_settings().get("graphics_mode") != "classic":
            self.audio.set_context('menu')
        self._pop_hs()
        show_screen("highscores")

    def _pop_hs(self):
        self.high_scores.load_scores()
        scores = self.high_scores.get_top_scores()
        c = get("hs-list")
        if not c:
            return
        c.innerHTML = ""
        if not scores:
            el = js.document.createElement("div")
            el.className = "hs-empty"
            el.textContent = "No high scores yet!\nPlay some games to set records!"
            c.appendChild(el)
        else:
            h = js.document.createElement("div")
            h.className = "hs-row hs-header"
            h.innerHTML = "<span>Rank</span><span>Player</span><span>Score</span><span>Difficulty</span><span>Date</span>"
            c.appendChild(h)
            for i, sd in enumerate(scores, 1):
                row = js.document.createElement("div")
                row.className = "hs-row"
                rc = {1: "gold", 2: "silver", 3: "#cd7f32"}.get(i, "white")
                dc = {"EASY": "#00ff00", "MEDIUM": "gold", "HARD": "#ff4444"}.get(sd.get("difficulty", ""), "white")
                rh = f"<span style='color:{rc};font-weight:bold'>"
                if i <= 3:
                    rh += f"<img src='assets/images/medal_{i}.png' style='width:24px;height:24px;vertical-align:middle'>"
                else:
                    rh += f"#{i}"
                rh += "</span>"
                local_dt = datetime.fromtimestamp(sd['timestamp']).strftime("%Y-%m-%d %H:%M") if sd.get('timestamp') else sd.get('date','Unknown')
                row.innerHTML = (f"{rh}<span>{sd.get('player_name','Player')}</span>"
                                 f"<span style='color:#00ff00'>{sd.get('score',0)}</span>"
                                 f"<span style='color:{dc}'>{sd.get('difficulty','MEDIUM')}</span>"
                                 f"<span>{local_dt}</span>")
                c.appendChild(row)
        set_text("hs-total", f"Total Scores: {self.high_scores.get_score_count()}")
        best = self.high_scores.get_best_score()
        set_text("hs-best", f"Best Score: {best['score']} by {best['player_name']}" if best else "Best Score: 0")

    def _show_menu(self):
        s = self.settings.get_current_settings()
        if s.get("graphics_mode") == "classic":
            self.audio.stop_bgm()
        else:
            self.audio.set_context('menu')
        show_screen("splash")

    def _show_opts(self):
        s = self.settings.get_current_settings()
        if s.get("graphics_mode") != "classic":
            self.audio.set_context('menu')
        bg_map = {"random": "Random", "meadow": "Meadow", "fern forest": "Fern Forest", "sands and bones": "Sands & Bones"}
        ctrl_map = {"arrows": "Arrows", "wasd": "WASD", "both": "Both"}
        skin_map = {"green": "Green", "red": "Red"}
        self._sel("opt-difficulty", s.get("difficulty","MEDIUM").capitalize())
        self._sel("opt-background", bg_map.get(s.get("background","random"), "Random"))
        self._sel("opt-controls", ctrl_map.get(s.get("controls","arrows"), "Arrows"))
        self._sel("opt-skin", skin_map.get(s.get("snake_skin","green"), "Green"))
        self._sel("opt-sound", "On" if s.get("sound_enabled", True) else "Off")
        self._sel("opt-graphics-mode", "Classic" if s.get("graphics_mode") == "classic" else "Modern")
        bg_row = get("opt-background-row")
        if bg_row:
            bg_row.style.display = "none" if s.get("graphics_mode") == "classic" else "flex"
        show_screen("options")

    def _on_graphics_mode_change(self, e):
        self._opt("graphics_mode", e.target.value)
        bg_row = get("opt-background-row")
        if bg_row:
            bg_row.style.display = "none" if e.target.value == "Classic" else "flex"

    def _sel(self, id, val):
        el = get(id)
        if el:
            el.value = val

    def _opt(self, group, value):
        maps = {"difficulty": {"Easy":"EASY","Medium":"MEDIUM","Hard":"HARD"},
                "background": {"Random":"random","Meadow":"meadow","Fern Forest":"fern forest","Sands & Bones":"sands and bones"},
                "controls": {"Arrows":"arrows","WASD":"wasd","Both":"both"},
                "snake_skin": {"Green":"green","Red":"red"},
                "sound": {"On":"True","Off":"False"},
                "graphics_mode": {"Classic":"classic","Modern":"modern"}}
        m = maps.get(group, {})
        fn = {"difficulty": self.settings.set_difficulty,
              "background": self.settings.set_background,
              "controls": self.settings.set_controls,
              "snake_skin": self.settings.set_snake_skin,
              "sound": self.settings.set_sound_enabled,
              "graphics_mode": self.settings.set_graphics_mode}
        fn.get(group, lambda x: None)(m.get(value, value))
        rev = {v: k for k, v in m.items()}
        notify(f"{group.replace('_',' ').title()} set to {rev.get(value, value)}")

    def _show_help(self):
        if self.settings.get_current_settings().get("graphics_mode") != "classic":
            self.audio.set_context('menu')
        show_screen("help")

    def _show_credits(self):
        if self.settings.get_current_settings().get("graphics_mode") != "classic":
            self.audio.set_context('menu')
        show_screen("credits")


app = SnakeApp()
