import os
import random
import time

import board
import busio
import displayio
import framebufferio
import rgbmatrix
import wifi
import socketpool

from mini_text import draw_centered_text, draw_text

try:
    import adafruit_lis3dh
except ImportError:
    adafruit_lis3dh = None


WIDTH = 64
HEIGHT = 32
FPS = 30.0
FRAME_DELAY = 1.0 / FPS

CALIBRATION_SECONDS = 1.5
CALIBRATION_SAMPLES = 30
TILT_SCALE_G = 0.45
TILT_DEADZONE_G = 0.05
POSITION_SMOOTHING = 0.25
TRAVEL_X = 22.0
TRAVEL_Y = 11.0

TILT_SWAP_AXES = (os.getenv("TILT_SWAP_AXES") or "0") == "1"
TILT_INVERT_X = (os.getenv("TILT_INVERT_X") or "0") == "1"
TILT_INVERT_Y = (os.getenv("TILT_INVERT_Y") or "0") == "1"

MAX_HEALTH = 100
HIT_DAMAGE = 10
SHIELD_BONUS_HEALTH = 30
HEALTH_BAR_HEIGHT = 2
SHIP_FLASH_SECONDS = 0.18
SUCCESS_FLASH_SECONDS = 0.18
POPUP_SECONDS = 1.35

POWERUP_SECONDS = 5

CALIBRATION_CORNER_SECONDS = 4.0
CALIBRATION_UI_STEP_SECONDS = 0.05

BULLET_SPEED = 14

POWERUP_RESPAWN_MIN_SECONDS = 5.0
POWERUP_RESPAWN_MAX_SECONDS = 9.0
SHIELD_SPAWN_CHANCE = 0.08

PLAYER_FIRE_INTERVAL = 0.42
RAPID_SPEED_MULTIPLIER = 1.8
BIG_SHOT_RADIUS = 1

NETWORK_PORT = int(os.getenv("DUAL_PORT") or "5050")
NETWORK_BROADCAST_IP = os.getenv("DUAL_BROADCAST_IP") or "255.255.255.255"
BOARD_ID = os.getenv("BOARD_ID") or "B"
FRONT_FACING_MIRROR_X = (os.getenv("FRONT_FACING_MIRROR_X") or "1") == "1"
PEER_IP = os.getenv("DUAL_PEER_IP")
DEBUG_NETWORK = (os.getenv("DEBUG_NETWORK") or "1") == "1"

SHIP_BASE_COLOR = 0xFF5A5A
SHIP_HIGHLIGHT_COLOR = 0xFFD1D1

PACKET_WORD_HIT = "HIT"
PACKET_WORD_SUCCESS = "SCORE"
PACKET_WORD_DEAD = "DEAD"

LEFT_TEXT_X = 1
RIGHT_TEXT_X = 36
TEXT_Y = 8
POWERUP_TEXT_Y = 25
GAME_CENTER_X = WIDTH // 2
LEFT_EVENT_CENTER_X = 10
RIGHT_EVENT_CENTER_X = 54
TEXT_LINE_HEIGHT = 6

TEXT_COLOR_RED = 4
TEXT_COLOR_GREEN = 5
TEXT_COLOR_BLUE = 6
TEXT_COLOR_WHITE = 7
TEXT_COLOR_YELLOW = 8


class TextField:
    def __init__(self, text="", color=TEXT_COLOR_WHITE, x=0, y=0):
        self.text = text
        self.color = color
        self.x = x
        self.y = y


def clamp(value, minimum, maximum):
    if value < minimum:
        return minimum
    if value > maximum:
        return maximum
    return value


def fill_rect(bitmap, x, y, width, height, color_index):
    x0 = max(0, x)
    y0 = max(0, y)
    x1 = min(WIDTH, x + width)
    y1 = min(HEIGHT, y + height)
    for py in range(y0, y1):
        for px in range(x0, x1):
            bitmap[px, py] = color_index


def make_i2c_bus():
    if hasattr(board, "I2C"):
        try:
            return board.I2C()
        except Exception:
            pass

    scl = getattr(board, "ACCELEROMETER_SCL", None)
    sda = getattr(board, "ACCELEROMETER_SDA", None)
    if scl is None:
        scl = getattr(board, "SCL", None)
    if sda is None:
        sda = getattr(board, "SDA", None)

    if scl is None or sda is None:
        return None

    return busio.I2C(scl, sda)


def init_accelerometer():
    if adafruit_lis3dh is None:
        print("adafruit_lis3dh not available")
        return None

    i2c = make_i2c_bus()
    if i2c is None:
        print("No I2C bus found for accelerometer")
        return None

    last_error = None
    for address in (0x18, 0x19):
        try:
            sensor = adafruit_lis3dh.LIS3DH_I2C(i2c, address=address)
            sensor.range = adafruit_lis3dh.RANGE_4_G
            print("Accelerometer ready at", hex(address))
            return sensor
        except Exception as error:
            last_error = error

    print("Accelerometer init failed:", last_error)
    return None


def read_accel_g(sensor):
    if sensor is None:
        return 0.0, 0.0, 1.0

    try:
        ax, ay, az = sensor.acceleration
    except Exception:
        return 0.0, 0.0, 1.0

    return ax / 9.80665, ay / 9.80665, az / 9.80665


def orient_xy(x_value, y_value):
    if TILT_SWAP_AXES:
        x_value, y_value = y_value, x_value
    if TILT_INVERT_X:
        x_value = -x_value
    if TILT_INVERT_Y:
        y_value = -y_value
    return x_value, y_value


def calibrate_center(sensor):
    total_x = 0.0
    total_y = 0.0
    samples = 0

    print("Calibrating accelerometer...")
    start = time.monotonic()
    while time.monotonic() - start < CALIBRATION_SECONDS:
        raw_x, raw_y, _raw_z = read_accel_g(sensor)
        raw_x, raw_y = orient_xy(raw_x, raw_y)
        total_x += raw_x
        total_y += raw_y
        samples += 1
        time.sleep(CALIBRATION_SECONDS / CALIBRATION_SAMPLES)

    if samples == 0:
        return 0.0, 0.0

    return total_x / samples, total_y / samples


def calibration_target_position(corner_name):
    if corner_name == "TR":
        return WIDTH - 4, 4
    if corner_name == "TL":
        return 3, 4
    if corner_name == "BL":
        return 3, HEIGHT - HEALTH_BAR_HEIGHT - 5
    return WIDTH - 4, HEIGHT - HEALTH_BAR_HEIGHT - 5


def corner_words(corner_name):
    if corner_name == "TR":
        return "TOP", "RIGHT"
    if corner_name == "TL":
        return "TOP", "LEFT"
    if corner_name == "BL":
        return "BOTTOM", "LEFT"
    return "BOTTOM", "RIGHT"


def shuffle_powerup_bag(items):
    # CircuitPython random module may not expose shuffle().
    for index in range(len(items) - 1, 0, -1):
        swap_index = random.randint(0, index)
        items[index], items[swap_index] = items[swap_index], items[index]


def run_guided_calibration(sensor, status_label, helper_label, detail_label, bitmap, display, rest_x, rest_y):
    if sensor is None:
        status_label.text = "NO ACCEL"
        helper_label.text = "FALLBACK"
        detail_label.text = ""
        display.refresh()
        time.sleep(0.5)
        return rest_x, rest_y, TILT_SCALE_G, TILT_SCALE_G

    corner_order = ["TR", "TL", "BL", "BR"]
    captured = {}

    ship_cal_x = WIDTH / 2
    ship_cal_y = (HEIGHT - HEALTH_BAR_HEIGHT) / 2
    last = time.monotonic()

    for corner_index, corner_name in enumerate(corner_order):
        target_orb_x, target_orb_y = calibration_target_position(corner_name)

        while True:
            now = time.monotonic()
            dt = now - last
            if dt <= 0:
                dt = FRAME_DELAY
            last = now

            raw_x, raw_y, _raw_z = read_accel_g(sensor)
            raw_x, raw_y = orient_xy(raw_x, raw_y)
            tilt_x = raw_x - rest_x
            tilt_y = raw_y - rest_y

            if abs(tilt_x) < TILT_DEADZONE_G:
                tilt_x = 0.0
            if abs(tilt_y) < TILT_DEADZONE_G:
                tilt_y = 0.0

            target_x = (WIDTH / 2) + clamp(tilt_x / TILT_SCALE_G, -1.0, 1.0) * TRAVEL_X
            target_y = ((HEIGHT - HEALTH_BAR_HEIGHT) / 2) + clamp(tilt_y / TILT_SCALE_G, -1.0, 1.0) * TRAVEL_Y
            ship_cal_x += (target_x - ship_cal_x) * 0.14
            ship_cal_y += (target_y - ship_cal_y) * 0.14

            hold_threshold = 0.08
            horizontal = "R" if tilt_x >= 0 else "L"
            vertical = "B" if tilt_y >= 0 else "T"
            held_corner = vertical + horizontal

            assist_step = 32.0 * dt
            if abs(tilt_x) > hold_threshold or abs(tilt_y) > hold_threshold:
                if held_corner == corner_name:
                    assist_step = 95.0 * dt

            if abs(target_orb_x - ship_cal_x) < 7 and abs(target_orb_y - ship_cal_y) < 7:
                assist_step = 140.0 * dt

            ship_cal_x += clamp(target_orb_x - ship_cal_x, -assist_step, assist_step)
            ship_cal_y += clamp(target_orb_y - ship_cal_y, -assist_step, assist_step)

            ship_cal_x = clamp(ship_cal_x, 3, WIDTH - 4)
            ship_cal_y = clamp(ship_cal_y, 5, HEIGHT - 4)

            word_top, word_bottom = corner_words(corner_name)
            status_label.text = "CAL " + str(corner_index + 1) + "/4"
            helper_label.text = word_top + " " + word_bottom
            detail_label.text = word_bottom
            detail_label.text = ""

            bitmap.fill(0)
            draw_powerup_orb(bitmap, {"kind": "shield", "x": target_orb_x, "y": target_orb_y})
            draw_ship(bitmap, ship_cal_x, ship_cal_y, tilt_x, tilt_y)
            draw_hud_text(bitmap)
            display.refresh()

            if abs(int(round(ship_cal_x)) - target_orb_x) <= 3 and abs(int(round(ship_cal_y)) - target_orb_y) <= 3:
                sample_x = 0.0
                sample_y = 0.0
                sample_n = 8
                for _ in range(sample_n):
                    corner_x, corner_y, _corner_z = read_accel_g(sensor)
                    corner_x, corner_y = orient_xy(corner_x, corner_y)
                    sample_x += corner_x
                    sample_y += corner_y
                    time.sleep(0.01)
                captured[corner_name] = (sample_x / sample_n, sample_y / sample_n)
                break

    min_x = min(point[0] for point in captured.values())
    max_x = max(point[0] for point in captured.values())
    min_y = min(point[1] for point in captured.values())
    max_y = max(point[1] for point in captured.values())

    center_x = (min_x + max_x) / 2.0
    center_y = (min_y + max_y) / 2.0
    range_x = (max_x - min_x) / 2.0
    range_y = (max_y - min_y) / 2.0

    if range_x < 0.12:
        range_x = TILT_SCALE_G
    if range_y < 0.12:
        range_y = TILT_SCALE_G

    status_label.text = "CALIBRATED"
    helper_label.text = "READY START"
    detail_label.text = ""
    bitmap.fill(0)
    display.refresh()
    time.sleep(0.4)

    detail_label.text = ""

    return center_x, center_y, range_x, range_y


def draw_background(bitmap, tick):
    bitmap.fill(0)


def draw_health_bar(bitmap, base_health_value, shield_health_value, flash_active):
    bitmap.fill(0)

    base_width = int((base_health_value * WIDTH) / MAX_HEALTH)
    if base_width < 0:
        base_width = 0
    if base_width > WIDTH:
        base_width = WIDTH

    shield_width = 0
    if MAX_HEALTH > 0:
        shield_width = int((shield_health_value * WIDTH) / MAX_HEALTH)
    if shield_width < 0:
        shield_width = 0
    if shield_width > WIDTH:
        shield_width = WIDTH

    bottom_y = HEIGHT - 1
    upper_y = HEIGHT - 2

    # 2px side rails in ship/base health color, kept above the health rows.
    for y in range(HEIGHT - HEALTH_BAR_HEIGHT):
        bitmap[0, y] = 2
        bitmap[1, y] = 2
        bitmap[WIDTH - 2, y] = 2
        bitmap[WIDTH - 1, y] = 2

    for x in range(WIDTH):
        bitmap[x, upper_y] = 1

    for x in range(base_width):
        bitmap[x, upper_y] = 2
        bitmap[x, bottom_y] = 2

    for x in range(shield_width):
        bitmap[x, upper_y] = 2 if flash_active else 3
        bitmap[x, bottom_y] = 2 if flash_active else 3

def draw_ship(bitmap, ship_x, ship_y, tilt_x, tilt_y):
    x = int(round(ship_x))
    y = int(round(ship_y))

    fill_rect(bitmap, x - 2, y - 1, 5, 3, 1)
    bitmap[x, y - 2] = 1
    bitmap[x - 1, y - 2] = 1
    bitmap[x + 1, y - 2] = 1
    bitmap[x, y - 3] = 2
    bitmap[x - 2, y] = 3
    bitmap[x + 2, y] = 3

    if tilt_x > 0.12 and y + 2 < HEIGHT:
        bitmap[x - 1, y + 2] = 3
        bitmap[x, y + 2] = 3
    elif tilt_x < -0.12 and y + 2 < HEIGHT:
        bitmap[x, y + 2] = 3
        bitmap[x + 1, y + 2] = 3

    if tilt_y > 0.12 and y + 3 < HEIGHT:
        bitmap[x, y + 3] = 3
    elif tilt_y < -0.12 and y + 3 < HEIGHT:
        bitmap[x, y + 3] = 1


def draw_hit_flash(bitmap, active):
    if not active:
        return

    for x in range(0, WIDTH, 2):
        bitmap[x, 0] = 4
    for x in range(1, WIDTH, 2):
        bitmap[x, 1] = 4


def draw_success_flash(bitmap, active):
    if not active:
        return

    for y in range(0, HEIGHT - HEALTH_BAR_HEIGHT, 2):
        bitmap[WIDTH - 1, y] = 5
    for y in range(1, HEIGHT - HEALTH_BAR_HEIGHT, 2):
        bitmap[WIDTH - 2, y] = 5


def draw_powerup_orb(bitmap, powerup):
    if not powerup:
        return

    x = int(powerup["x"])
    y = int(powerup["y"])
    kind = powerup["kind"]

    # Orb shell
    shell_color = 7
    if kind == "big":
        shell_color = 8
    elif kind == "rapid":
        shell_color = 4

    bitmap[x, y] = shell_color
    if x > 0:
        bitmap[x - 1, y] = shell_color
    if x < WIDTH - 1:
        bitmap[x + 1, y] = shell_color
    if y > 0:
        bitmap[x, y - 1] = shell_color
    if y < HEIGHT - HEALTH_BAR_HEIGHT - 1:
        bitmap[x, y + 1] = shell_color

    # Inner icon (shield / muscle / speed)
    if kind == "shield":
        if y > 0:
            bitmap[x, y - 1] = 7
        bitmap[x, y] = 7
        if x > 0:
            bitmap[x - 1, y] = 7
        if x < WIDTH - 1:
            bitmap[x + 1, y] = 7
        if y + 1 < HEIGHT - HEALTH_BAR_HEIGHT:
            bitmap[x - 1, y + 1] = 7
            bitmap[x + 1, y + 1] = 7
        if y + 2 < HEIGHT - HEALTH_BAR_HEIGHT:
            bitmap[x, y + 2] = 7
    elif kind == "big":
        bitmap[x - 1, y] = 8
        bitmap[x, y] = 8
        bitmap[x + 1, y] = 8
        if x - 2 >= 0:
            bitmap[x - 2, y + 1] = 8
        if x - 1 >= 0:
            bitmap[x - 1, y + 1] = 8
        bitmap[x, y + 1] = 8
        if x + 1 < WIDTH:
            bitmap[x + 1, y + 1] = 8
        if x + 2 < WIDTH:
            bitmap[x + 2, y + 1] = 8
        if y + 2 < HEIGHT - HEALTH_BAR_HEIGHT:
            bitmap[x - 1, y + 2] = 8
            bitmap[x, y + 2] = 8
            bitmap[x + 1, y + 2] = 8
    elif kind == "rapid":
        if y > 0:
            bitmap[x + 1, y - 1] = 4
        bitmap[x, y] = 4
        bitmap[x - 1, y] = 4
        bitmap[x + 1, y] = 4
        if x > 0:
            bitmap[x - 1, y + 1] = 4
        if x < WIDTH - 1:
            bitmap[x + 1, y + 1] = 4
        if y + 1 < HEIGHT - HEALTH_BAR_HEIGHT:
            bitmap[x, y + 1] = 4
        if y + 2 < HEIGHT - HEALTH_BAR_HEIGHT:
            bitmap[x - 1, y + 2] = 4
            bitmap[x, y + 2] = 4
            bitmap[x + 1, y + 2] = 4


def draw_enemy_bullets(bitmap, enemy_bullets):
    for bullet in enemy_bullets:
        x = int(bullet["x"])
        y = int(bullet["y"])
        radius = int(bullet.get("radius", 0))
        for dy in range(-radius, radius + 1):
            for dx in range(-radius, radius + 1):
                if abs(dx) + abs(dy) > radius + 1:
                    continue
                px = x + dx
                py = y + dy
                if 0 <= px < WIDTH and 0 <= py < HEIGHT - HEALTH_BAR_HEIGHT:
                    bitmap[px, py] = 8


def draw_player_bullets(bitmap, player_bullets):
    for bullet in player_bullets:
        x = int(bullet["x"])
        y = int(bullet["y"])
        radius = bullet["radius"]
        for dy in range(-radius, radius + 1):
            for dx in range(-radius, radius + 1):
                if abs(dx) + abs(dy) > radius + 1:
                    continue
                px = x + dx
                py = y + dy
                if 0 <= px < WIDTH and 0 <= py < HEIGHT - HEALTH_BAR_HEIGHT:
                    bitmap[px, py] = 5


def draw_opponent_target(bitmap, active):
    if not active:
        return
    tx = WIDTH // 2
    ty = 3
    bitmap[tx, ty] = 4
    if tx > 0:
        bitmap[tx - 1, ty] = 4
    if tx < WIDTH - 1:
        bitmap[tx + 1, ty] = 4
    if ty + 1 < HEIGHT - HEALTH_BAR_HEIGHT:
        bitmap[tx, ty + 1] = 4


def draw_text_field(bitmap, field):
    if field.text:
        draw_centered_text(bitmap, field.text, field.x, field.y, field.color)


def draw_firework_burst(bitmap, center_x, center_y, radius, color_index):
    for offset in range(-radius, radius + 1):
        if center_x + offset >= 0 and center_x + offset < WIDTH and center_y >= 0 and center_y < HEIGHT - HEALTH_BAR_HEIGHT:
            bitmap[center_x + offset, center_y] = color_index
        if center_y + offset >= 0 and center_y + offset < HEIGHT - HEALTH_BAR_HEIGHT and center_x >= 0 and center_x < WIDTH:
            bitmap[center_x, center_y + offset] = color_index
        if center_x + offset >= 0 and center_x + offset < WIDTH and center_y + offset >= 0 and center_y + offset < HEIGHT - HEALTH_BAR_HEIGHT:
            bitmap[center_x + offset, center_y + offset] = color_index
        if center_x + offset >= 0 and center_x + offset < WIDTH and center_y - offset >= 0 and center_y - offset < HEIGHT - HEALTH_BAR_HEIGHT:
            bitmap[center_x + offset, center_y - offset] = color_index


def draw_fireworks(bitmap, now):
    burst_seed = int(now * 8)
    colors = (TEXT_COLOR_YELLOW, TEXT_COLOR_GREEN, TEXT_COLOR_BLUE, 3)
    for index in range(7):
        center_x = (burst_seed * 11 + index * 9 + 7) % WIDTH
        center_y = (burst_seed * 5 + index * 7 + 3) % (HEIGHT - HEALTH_BAR_HEIGHT - 1)
        radius = 1 + ((burst_seed + index) % 3)
        color_index = colors[(burst_seed + index) % len(colors)]
        draw_firework_burst(bitmap, center_x, center_y, radius, color_index)


def draw_boo_effects(bitmap, now):
    pattern_seed = int(now * 6)
    for index in range(8):
        x = (pattern_seed * 13 + index * 11) % max(1, WIDTH - 12)
        y = (pattern_seed * 7 + index * 5) % max(1, HEIGHT - HEALTH_BAR_HEIGHT - 6)
        color_index = TEXT_COLOR_RED if index % 2 == 0 else TEXT_COLOR_WHITE
        draw_text(bitmap, "BOO", x, y, color_index)


def draw_game_over_effects(bitmap, now):
    if game_state == "win":
        draw_fireworks(bitmap, now)
    elif game_state == "lose":
        draw_boo_effects(bitmap, now)


def schedule_game_over(next_state, now):
    global game_state, game_restart_at
    if game_state != next_state:
        game_state = next_state
        game_restart_at = now + 20.0


def draw_hud_text(bitmap):
    draw_text_field(bitmap, left_event_label)
    draw_text_field(bitmap, right_event_label)
    draw_text_field(bitmap, powerup_label)
    draw_text_field(bitmap, end_title_label)
    draw_text_field(bitmap, end_subtitle_label)
    draw_text_field(bitmap, calibration_status_label)
    draw_text_field(bitmap, calibration_helper_label)
    draw_text_field(bitmap, calibration_detail_label)


displayio.release_displays()
matrix = rgbmatrix.RGBMatrix(
    width=WIDTH,
    height=HEIGHT,
    bit_depth=4,
    addr_pins=board.MTX_ADDRESS[:4],
    **board.MTX_COMMON,
)
display = framebufferio.FramebufferDisplay(matrix, auto_refresh=False)

background_bitmap = displayio.Bitmap(WIDTH, HEIGHT, 4)
background_palette = displayio.Palette(4)
background_palette[0] = 0x000000
background_palette[1] = 0x000000
background_palette[2] = 0x000000
background_palette[3] = 0x000000

ship_bitmap = displayio.Bitmap(WIDTH, HEIGHT, 9)
ship_palette = displayio.Palette(9)
ship_palette[0] = 0x000000
ship_palette[1] = SHIP_BASE_COLOR
ship_palette[2] = SHIP_HIGHLIGHT_COLOR
ship_palette[3] = 0xF59E0B
ship_palette[4] = 0xFF4242
ship_palette[5] = 0x49FF7A
ship_palette[6] = 0x7BDFF2
ship_palette[7] = 0xEDEDED
ship_palette[8] = 0xFFEB3B
ship_palette.make_transparent(0)

health_bitmap = displayio.Bitmap(WIDTH, HEIGHT, 4)
health_palette = displayio.Palette(4)
health_palette[0] = 0x000000
health_palette[1] = 0x460000
health_palette[2] = 0xD11C1C
health_palette[3] = 0xC0C6CC
health_palette.make_transparent(0)

group = displayio.Group()
group.append(displayio.TileGrid(background_bitmap, pixel_shader=background_palette))
group.append(displayio.TileGrid(health_bitmap, pixel_shader=health_palette))
group.append(displayio.TileGrid(ship_bitmap, pixel_shader=ship_palette))

display.root_group = group

left_event_label = TextField("", TEXT_COLOR_RED, LEFT_EVENT_CENTER_X, TEXT_Y)
right_event_label = TextField("", TEXT_COLOR_GREEN, RIGHT_EVENT_CENTER_X, TEXT_Y)
powerup_label = TextField("", TEXT_COLOR_WHITE, GAME_CENTER_X, POWERUP_TEXT_Y)
end_title_label = TextField("", TEXT_COLOR_WHITE, GAME_CENTER_X, 10)
end_subtitle_label = TextField("", TEXT_COLOR_BLUE, GAME_CENTER_X, 18)
calibration_status_label = TextField("", TEXT_COLOR_WHITE, GAME_CENTER_X, 10)
calibration_helper_label = TextField("", TEXT_COLOR_BLUE, GAME_CENTER_X, 18)
calibration_detail_label = TextField("", TEXT_COLOR_BLUE, GAME_CENTER_X, 26)

accelerometer = init_accelerometer()

ship_x = WIDTH / 2
ship_y = HEIGHT / 2 + 4
frame_tick = 0
last_debug = time.monotonic()
current_tilt_x = 0.0
current_tilt_y = 0.0
player_health = MAX_HEALTH
shield_health = 0
ship_flash_until = 0.0
hit_flash_until = 0.0
success_flash_until = 0.0

rapid_fire_until = 0.0
big_shot_until = 0.0

left_text_until = 0.0
right_text_until = 0.0
powerup_text_until = 0.0
left_text_phase = 0.0
right_text_phase = 0.0

active_powerup = None
powerup_spawned_at = 0.0
next_powerup_spawn_at = 0.0
powerup_bag = []
last_powerup_kind = ""

enemy_bullets = []
player_bullets = []
last_player_fire_time = 0.0

last_frame_time = time.monotonic()

udp_send = None
udp_recv = None
network_buffer = bytearray(96)
game_state = "playing"
game_over_sent = False
game_restart_at = 0.0
ignore_bullets_until = 0.0


def max_player_health():
    return MAX_HEALTH + SHIELD_BONUS_HEALTH


def current_player_health():
    return player_health + shield_health


def damage_player(amount):
    global player_health, shield_health, ship_flash_until, hit_flash_until
    global game_state, game_over_sent
    damage_remaining = amount
    if shield_health > 0:
        shield_hit = shield_health
        if shield_hit > damage_remaining:
            shield_hit = damage_remaining
        shield_health -= shield_hit
        damage_remaining -= shield_hit
    if damage_remaining > 0:
        player_health -= damage_remaining
        if player_health < 0:
            player_health = 0
    ship_flash_until = time.monotonic() + SHIP_FLASH_SECONDS
    hit_flash_until = time.monotonic() + SHIP_FLASH_SECONDS

    if current_player_health() <= 0 and game_state == "playing":
        schedule_game_over("lose", time.monotonic())
        if not game_over_sent:
            send_text_packet("DEAD", PACKET_WORD_DEAD)
            game_over_sent = True


def award_shield():
    global shield_health
    if shield_health > 0:
        return
    if current_player_health() >= max_player_health():
        return
    shield_health = SHIELD_BONUS_HEALTH


def award_rapid_fire():
    global rapid_fire_until
    rapid_fire_until = time.monotonic() + POWERUP_SECONDS


def award_big_shot():
    global big_shot_until
    big_shot_until = time.monotonic() + POWERUP_SECONDS


def send_text_packet(event_name, one_word):
    packet = BOARD_ID + "|" + event_name + "|" + one_word
    if DEBUG_NETWORK:
        print("TX", packet)
    if udp_send is None:
        if DEBUG_NETWORK:
            print("TX skip: udp_send not ready")
        return
    try:
        udp_send.sendto(packet.encode("utf-8"), (NETWORK_BROADCAST_IP, NETWORK_PORT))
        if PEER_IP:
            udp_send.sendto(packet.encode("utf-8"), (PEER_IP, NETWORK_PORT))
    except Exception as error:
        print("TX fail:", error)


def receive_text_packet(packet):
    global success_flash_until, left_text_until, right_text_until
    global powerup_text_until, left_text_phase, right_text_phase
    global enemy_bullets
    global game_state, ignore_bullets_until

    parts = packet.split("|", 2)
    if len(parts) == 3:
        sender_id, event_name, word = parts
    elif len(parts) == 2:
        sender_id = "?"
        event_name, word = parts
    else:
        sender_id = "?"
        event_name = packet
        word = ""

    if sender_id == BOARD_ID:
        return

    now = time.monotonic()

    if event_name == "HIT":
        damage_player(HIT_DAMAGE)
        left_event_label.text = word or PACKET_WORD_HIT
        left_event_label.color = TEXT_COLOR_RED
        left_text_until = now + POPUP_SECONDS
        left_text_phase = now
    elif event_name == "SUCCESS":
        success_flash_until = now + SUCCESS_FLASH_SECONDS
        right_event_label.text = word or PACKET_WORD_SUCCESS
        right_event_label.color = TEXT_COLOR_GREEN
        right_text_until = now + POPUP_SECONDS
        right_text_phase = now
    elif event_name == "SHIELD":
        powerup_label.text = word or "SHIELD"
        powerup_label.color = TEXT_COLOR_WHITE
        powerup_text_until = now + POPUP_SECONDS
    elif event_name == "RAPID":
        powerup_label.text = word or "RAPID"
        powerup_label.color = TEXT_COLOR_BLUE
        powerup_text_until = now + POPUP_SECONDS
    elif event_name == "BIG":
        powerup_label.text = word or "MUSCLE"
        powerup_label.color = TEXT_COLOR_YELLOW
        powerup_text_until = now + POPUP_SECONDS
    elif event_name == "DEAD":
        schedule_game_over("win", now)
        success_flash_until = now + SUCCESS_FLASH_SECONDS
        right_event_label.text = "WIN"
        right_event_label.color = TEXT_COLOR_GREEN
        right_text_until = now + POPUP_SECONDS
        right_text_phase = now
    elif event_name == "BULLET":
        if now < ignore_bullets_until:
            return
        try:
            parts = word.split(",")
            bullet_x = int(parts[0])
            bullet_speed = float(parts[1]) if len(parts) > 1 else BULLET_SPEED
            bullet_radius = int(parts[2]) if len(parts) > 2 else 0
        except Exception:
            return

        if FRONT_FACING_MIRROR_X:
            bullet_x = (WIDTH - 1) - bullet_x

        bullet_x = clamp(bullet_x, 0, WIDTH - 1)
        enemy_bullets.append(
            {
                "x": float(bullet_x),
                "y": 0.0,
                "speed": bullet_speed,
                "radius": clamp(bullet_radius, 0, BIG_SHOT_RADIUS),
                "owner": sender_id,
            }
        )


def init_network():
    global udp_send, udp_recv

    ssid = os.getenv("CIRCUITPY_WIFI_SSID")
    password = os.getenv("CIRCUITPY_WIFI_PASSWORD")
    if not ssid or not password:
        print("No Wi-Fi credentials in settings.toml")
        return

    try:
        print("Wi-Fi connect start:", ssid)
        wifi.radio.connect(ssid, password)
        pool = socketpool.SocketPool(wifi.radio)

        udp_send = pool.socket(pool.AF_INET, pool.SOCK_DGRAM)
        try:
            udp_send.setsockopt(pool.SOL_SOCKET, pool.SO_BROADCAST, 1)
        except Exception:
            pass
        udp_recv = pool.socket(pool.AF_INET, pool.SOCK_DGRAM)
        udp_recv.bind(("0.0.0.0", NETWORK_PORT))
        udp_recv.settimeout(0)

        print("Wi-Fi connected:", wifi.radio.ipv4_address)
        print("UDP listen on:", NETWORK_PORT, "board:", BOARD_ID)
        print("UDP broadcast target:", NETWORK_BROADCAST_IP)
        if PEER_IP:
            print("UDP peer target:", PEER_IP)
    except Exception as error:
        print("Wi-Fi init failed:", error)
        udp_send = None
        udp_recv = None


def poll_network_packets():
    if udp_recv is None:
        return

    while True:
        try:
            size, _addr = udp_recv.recvfrom_into(network_buffer)
        except Exception:
            break
        if size <= 0:
            break
        try:
            packet = bytes(network_buffer[:size]).decode("utf-8")
            if DEBUG_NETWORK:
                print("RX", _addr, packet)
            receive_text_packet(packet)
        except Exception:
            continue


def flush_pending_bullet_packets():
    if udp_recv is None:
        return

    while True:
        try:
            size, _addr = udp_recv.recvfrom_into(network_buffer)
        except Exception:
            break
        if size <= 0:
            break


def update_text_animations(now):
    if now >= left_text_until:
        left_event_label.text = ""
        left_event_label.x = LEFT_EVENT_CENTER_X
    else:
        wiggle = int((now - left_text_phase) * 20) % 2
        left_event_label.x = LEFT_EVENT_CENTER_X + wiggle

    if now >= right_text_until:
        right_event_label.text = ""
        right_event_label.x = RIGHT_EVENT_CENTER_X
    else:
        wiggle = int((now - right_text_phase) * 20) % 2
        right_event_label.x = RIGHT_EVENT_CENTER_X + wiggle

    if now >= powerup_text_until:
        powerup_label.text = ""


def update_end_screen_labels(now):
    if game_state == "win":
        end_title_label.text = "YOU WIN"
        end_title_label.color = TEXT_COLOR_GREEN
        blink = int(now * 3) % 2
        end_subtitle_label.text = "OPPONENT DOWN" if blink == 0 else "GAME OVER"
        end_subtitle_label.color = TEXT_COLOR_WHITE
    elif game_state == "lose":
        end_title_label.text = "YOU LOSE"
        end_title_label.color = TEXT_COLOR_RED
        blink = int(now * 3) % 2
        end_subtitle_label.text = "YOU ARE DOWN" if blink == 0 else "GAME OVER"
        end_subtitle_label.color = TEXT_COLOR_WHITE
    else:
        end_title_label.text = ""
        end_subtitle_label.text = ""


def spawn_powerup(kind, now):
    global active_powerup, powerup_spawned_at
    active_powerup = {
        "kind": kind,
        "x": random.randint(8, WIDTH - 9),
        "y": random.randint(7, HEIGHT - HEALTH_BAR_HEIGHT - 8),
    }
    powerup_spawned_at = now


def collect_powerup(kind):
    if kind == "shield":
        award_shield()
        receive_text_packet("SHIELD|SHIELD UP")
    elif kind == "big":
        award_big_shot()
        receive_text_packet("BIG|BULK UP")
    elif kind == "rapid":
        award_rapid_fire()
        receive_text_packet("RAPID|XLR8")


def current_player_bullet_speed(now):
    speed = BULLET_SPEED
    if now < rapid_fire_until:
        speed *= RAPID_SPEED_MULTIPLIER
    return speed


def current_player_bullet_radius(now):
    if now < big_shot_until:
        return BIG_SHOT_RADIUS
    return 0


def verify_powerup_effect(kind, now):
    if kind == "shield":
        return shield_health > 0
    if kind == "big":
        return current_player_bullet_radius(now) > 0
    if kind == "rapid":
        return current_player_bullet_speed(now) > BULLET_SPEED
    return False


def choose_random_powerup_kind():
    global powerup_bag, last_powerup_kind

    if shield_health <= 0 and current_player_health() < max_player_health():
        if random.random() < SHIELD_SPAWN_CHANCE:
            last_powerup_kind = "shield"
            return "shield"

    if len(powerup_bag) == 0:
        powerup_bag = ["big", "rapid"]
        shuffle_powerup_bag(powerup_bag)
        # Avoid obvious repeats at bag boundaries.
        if last_powerup_kind and powerup_bag[-1] == last_powerup_kind:
            powerup_bag.reverse()

    picked = powerup_bag.pop()
    last_powerup_kind = picked
    return picked


def update_powerups(now):
    global next_powerup_spawn_at

    if next_powerup_spawn_at == 0.0:
        next_powerup_spawn_at = now + random.uniform(POWERUP_RESPAWN_MIN_SECONDS, POWERUP_RESPAWN_MAX_SECONDS)

    if active_powerup is None and now >= next_powerup_spawn_at:
        spawn_powerup(choose_random_powerup_kind(), now)
        next_powerup_spawn_at = now + random.uniform(POWERUP_RESPAWN_MIN_SECONDS, POWERUP_RESPAWN_MAX_SECONDS)

    if active_powerup is None:
        return

    px = int(active_powerup["x"])
    py = int(active_powerup["y"])
    if abs(int(round(ship_x)) - px) <= 3 and abs(int(round(ship_y)) - py) <= 3:
        collect_powerup(active_powerup["kind"])
        next_powerup_spawn_at = now + random.uniform(POWERUP_RESPAWN_MIN_SECONDS, POWERUP_RESPAWN_MAX_SECONDS)
        clear_powerup()


def update_player_bullets(now, dt):
    global player_bullets, last_player_fire_time

    if now - last_player_fire_time >= PLAYER_FIRE_INTERVAL:
        player_bullets.append(
            {
                "x": ship_x,
                "y": ship_y - 4,
                "speed": current_player_bullet_speed(now),
                "radius": current_player_bullet_radius(now),
            }
        )
        last_player_fire_time = now

    live = []
    for bullet in player_bullets:
        bullet["y"] -= bullet["speed"] * dt
        if bullet["y"] >= 0:
            live.append(bullet)
        else:
            payload = (
                str(int(round(bullet["x"])))
                + ","
                + str(round(bullet["speed"], 2))
                + ","
                + str(int(bullet["radius"]))
            )
            send_text_packet("BULLET", payload)
    player_bullets = live


def update_enemy_bullets(dt):
    global enemy_bullets
    global left_text_until, left_text_phase

    ship_px = int(round(ship_x))
    ship_py = int(round(ship_y))

    live = []
    for bullet in enemy_bullets:
        bullet["y"] += bullet.get("speed", BULLET_SPEED) * dt
        bx = int(round(bullet["x"]))
        by = int(round(bullet["y"]))
        radius = int(bullet.get("radius", 0))

        if abs(bx - ship_px) <= 2 + radius and abs(by - ship_py) <= 2 + radius:
            damage_player(HIT_DAMAGE)
            left_event_label.text = PACKET_WORD_HIT
            left_event_label.color = TEXT_COLOR_RED
            left_text_phase = time.monotonic()
            left_text_until = left_text_phase + POPUP_SECONDS
            owner = bullet.get("owner", "")
            if owner:
                send_text_packet("SUCCESS", PACKET_WORD_SUCCESS)
            continue

        if by < HEIGHT - HEALTH_BAR_HEIGHT:
            live.append(bullet)

    enemy_bullets = live


def clear_powerup():
    global active_powerup
    active_powerup = None


def reset_round_state():
    global ship_x, ship_y, frame_tick, last_debug, current_tilt_x, current_tilt_y
    global player_health, shield_health, ship_flash_until, hit_flash_until, success_flash_until
    global rapid_fire_until, big_shot_until
    global left_text_until, right_text_until, powerup_text_until, left_text_phase, right_text_phase
    global active_powerup, powerup_spawned_at, next_powerup_spawn_at, powerup_bag, last_powerup_kind
    global enemy_bullets, player_bullets, last_player_fire_time
    global game_over_sent, game_restart_at, ignore_bullets_until

    ship_x = WIDTH / 2
    ship_y = HEIGHT / 2 + 4
    frame_tick = 0
    last_debug = time.monotonic()
    current_tilt_x = 0.0
    current_tilt_y = 0.0
    player_health = MAX_HEALTH
    shield_health = 0
    ship_flash_until = 0.0
    hit_flash_until = 0.0
    success_flash_until = 0.0
    rapid_fire_until = 0.0
    big_shot_until = 0.0
    left_text_until = 0.0
    right_text_until = 0.0
    powerup_text_until = 0.0
    left_text_phase = 0.0
    right_text_phase = 0.0
    active_powerup = None
    powerup_spawned_at = 0.0
    next_powerup_spawn_at = 0.0
    powerup_bag = []
    last_powerup_kind = ""
    enemy_bullets = []
    player_bullets = []
    last_player_fire_time = 0.0
    game_over_sent = False
    game_restart_at = 0.0
    ignore_bullets_until = 0.0
    left_event_label.text = ""
    right_event_label.text = ""
    powerup_label.text = ""
    end_title_label.text = ""
    end_subtitle_label.text = ""
    calibration_status_label.text = ""
    calibration_helper_label.text = ""
    calibration_detail_label.text = ""


def run_calibration_sequence():
    global rest_x, rest_y, calibrated_range_x, calibrated_range_y, game_state, last_frame_time
    global ignore_bullets_until

    calibration_status_label.text = "CALIBRATING"
    calibration_helper_label.text = "HOLD STILL"
    calibration_detail_label.text = ""
    rest_x, rest_y = calibrate_center(accelerometer)
    rest_x, rest_y, calibrated_range_x, calibrated_range_y = run_guided_calibration(
        accelerometer,
        calibration_status_label,
        calibration_helper_label,
        calibration_detail_label,
        ship_bitmap,
        display,
        rest_x,
        rest_y,
    )
    calibration_status_label.text = ""
    calibration_helper_label.text = ""
    calibration_detail_label.text = ""
    flush_pending_bullet_packets()
    ignore_bullets_until = time.monotonic() + 0.75
    game_state = "playing"
    last_frame_time = time.monotonic()


def prepare_new_round():
    global game_state
    game_state = "calibrating"
    reset_round_state()
    run_calibration_sequence()


init_network()
prepare_new_round()


while True:
    frame_start = time.monotonic()
    now = time.monotonic()
    dt = now - last_frame_time
    if dt <= 0:
        dt = FRAME_DELAY
    last_frame_time = now

    if game_state in ("win", "lose") and game_restart_at and now >= game_restart_at:
        prepare_new_round()
        continue

    poll_network_packets()

    if game_state == "playing":
        raw_x, raw_y, _raw_z = read_accel_g(accelerometer)
        raw_x, raw_y = orient_xy(raw_x, raw_y)

        current_tilt_x = raw_x - rest_x
        current_tilt_y = raw_y - rest_y

        normalized_tilt_x = current_tilt_x / calibrated_range_x
        normalized_tilt_y = current_tilt_y / calibrated_range_y

        deadzone_x = TILT_DEADZONE_G / calibrated_range_x
        deadzone_y = TILT_DEADZONE_G / calibrated_range_y

        if abs(normalized_tilt_x) < deadzone_x:
            normalized_tilt_x = 0.0
        if abs(normalized_tilt_y) < deadzone_y:
            normalized_tilt_y = 0.0

        normalized_tilt_x = clamp(normalized_tilt_x * 1.12, -1.0, 1.0)
        normalized_tilt_y = clamp(normalized_tilt_y * 1.12, -1.0, 1.0)

        target_x = (WIDTH / 2) + normalized_tilt_x * TRAVEL_X
        target_y = (HEIGHT / 2) + normalized_tilt_y * TRAVEL_Y

        smooth_factor = clamp(POSITION_SMOOTHING - 0.08, 0.05, 0.95)
        ship_x += (target_x - ship_x) * smooth_factor
        ship_y += (target_y - ship_y) * smooth_factor

        ship_x = clamp(ship_x, 3, WIDTH - 4)
        ship_y = clamp(ship_y, 5, HEIGHT - 4)

        update_powerups(now)
        update_player_bullets(now, dt)
        update_enemy_bullets(dt)

    draw_background(background_bitmap, frame_tick)
    draw_health_bar(health_bitmap, player_health, shield_health, now < ship_flash_until)
    ship_bitmap.fill(0)
    draw_powerup_orb(ship_bitmap, active_powerup)
    draw_enemy_bullets(ship_bitmap, enemy_bullets)
    draw_player_bullets(ship_bitmap, player_bullets)
    draw_ship(ship_bitmap, ship_x, ship_y, current_tilt_x, current_tilt_y)
    draw_hit_flash(ship_bitmap, now < hit_flash_until)
    draw_success_flash(ship_bitmap, now < success_flash_until)
    update_text_animations(now)
    update_end_screen_labels(now)
    draw_game_over_effects(ship_bitmap, now)
    draw_hud_text(ship_bitmap)

    display.refresh()

    if now - last_debug > 1.0:
        print(
            "tilt_x:",
            round(current_tilt_x, 3),
            "tilt_y:",
            round(current_tilt_y, 3),
            "ship_x:",
            round(ship_x, 2),
            "ship_y:",
            round(ship_y, 2),
            "health:",
            current_player_health(),
            "/",
            max_player_health(),
            "shield:",
            shield_health,
        )
        last_debug = now

    frame_tick += 1
    elapsed = time.monotonic() - frame_start
    if elapsed < FRAME_DELAY:
        time.sleep(FRAME_DELAY - elapsed)
