"""Emit `src/work/ArenaData.ts` from the extracted scene data.

    python3 tools/unity_extract.py "<scene>.unity" > tools/colliders.json
    python3 tools/unity_markers.py "<scene>.unity" > tools/markers.json
    python3 tools/build_parts.py
    python3 tools/write_arena_data.py "<Level Name>"   # defaults to levelName in unity-import.json
"""

import json
import os
import sys

SC = os.path.dirname(os.path.abspath(__file__))
from import_config import CFG as _CFG
OUT = os.path.join(os.path.dirname(SC), *_CFG['outputTs'].split('/'))


def n(v):
    return f'{round(v, 2)}'


def box_lines(items):
    out = []
    for b in items:
        out.append(f"    {{ cx: {n(b['cx'])}, cy: {n(b['cy'])}, hw: {n(b['hw'])}, hh: {n(b['hh'])}, rot: {n(b['rot'])}, tag: '{b['tag']}', name: '{b['name']}' }},")
    return '\n'.join(out)


def circle_lines(items):
    out = []
    for c in items:
        out.append(f"    {{ cx: {n(c['cx'])}, cy: {n(c['cy'])}, r: {n(c['r'])}, tag: '{c['tag']}', name: '{c['name']}' }},")
    return '\n'.join(out)


def rect_lines(items, z=False, tag=False):
    out = []
    for r in items:
        parts = [f"x0: {n(r['x0'])}", f"y0: {n(r['y0'])}", f"x1: {n(r['x1'])}", f"y1: {n(r['y1'])}"]
        if z:
            parts += [f"z0: {n(r['z0'])}", f"z1: {n(r['z1'])}"]
        if tag and r.get('tag'):
            parts.append(f"tag: '{r['tag']}'")
        if tag and r.get('name'):
            safe = str(r['name']).replace("\\", "").replace("'", "")
            parts.append(f"name: '{safe}'")
        out.append('    { ' + ', '.join(parts) + ' },')
    return '\n'.join(out)


def load_2d():
    """The designer-authored 2D colliders (see unity_colliders2d.py): the walls a ship can hit."""
    import math
    path = os.path.join(SC, 'colliders2d.json')
    if not os.path.exists(path):
        return [], [], []
    LIMIT = 80.0
    boxes, circles, aabbs = [], [], []
    for c in json.load(open(path)):
        if c['trigger'] or c['layer'] not in (0, 10):
            continue
        if abs(c['cx']) > LIMIT or abs(c['cy']) > LIMIT:
            continue
        tag = c.get('tag') or 'wall'; nm = str(c.get('name', '')).replace("\\", "").replace("'", "")
        if c['kind'] == 'box':
            rot = math.radians(c['rot'])
            boxes.append(dict(cx=c['cx'], cy=c['cy'], hw=c['hw'], hh=c['hh'], rot=round(rot, 5), tag=tag, name=nm))
            ca, sa = abs(math.cos(rot)), abs(math.sin(rot))
            ex = c['hw'] * ca + c['hh'] * sa; ey = c['hw'] * sa + c['hh'] * ca
            aabbs.append(dict(x0=round(c['cx'] - ex, 3), y0=round(c['cy'] - ey, 3), x1=round(c['cx'] + ex, 3), y1=round(c['cy'] + ey, 3), tag=tag, name=nm))
        else:
            circles.append(dict(cx=c['cx'], cy=c['cy'], r=c['r'], tag=tag, name=nm))
            aabbs.append(dict(x0=round(c['cx'] - c['r'], 3), y0=round(c['cy'] - c['r'], 3), x1=round(c['cx'] + c['r'], 3), y1=round(c['cy'] + c['r'], 3), tag=tag, name=nm))
    # Walls sliced from render meshes that carry no 2D collider in Unity (tools/mesh_walls.py):
    # the perimeter tunnel ring. Axis-aligned by construction.
    rw = os.path.join(SC, 'render_walls.json')
    if os.path.exists(rw):
        for r in json.load(open(rw)):
            cx, cy = (r['x0'] + r['x1']) / 2, (r['y0'] + r['y1']) / 2
            boxes.append(dict(cx=round(cx, 3), cy=round(cy, 3), hw=round((r['x1'] - r['x0']) / 2, 3), hh=round((r['y1'] - r['y0']) / 2, 3), rot=0.0, tag=r.get('tag', 'Tunnel'), name=r.get('name', 'Tunnel')))
            aabbs.append(dict(x0=r['x0'], y0=r['y0'], x1=r['x1'], y1=r['y1'], tag=r.get('tag', 'Tunnel'), name=r.get('name', 'Tunnel')))
    return boxes, circles, aabbs


def main():
    level = sys.argv[1] if len(sys.argv) > 1 else _CFG['levelName']
    parts = json.load(open(os.path.join(SC, 'arena_parts.json')))
    boxes2d, circles2d, aabbs2d = load_2d()
    if aabbs2d:
        # Physics comes from the 2D colliders. The sliced 3D geometry is not what a ship hits.
        parts['walls'] = aabbs2d
    parts['boxes2d'] = boxes2d
    parts['circles2d'] = circles2d
    markers = json.load(open(os.path.join(SC, 'markers.json')))

    bases = [m for m in markers if m['kind'] == 'Base']
    # Sort into a stable order so pilot 1 is always the same base.
    bases.sort(key=lambda b: (round(b['pos'][1], 1), round(b['pos'][0], 1)))

    base_lines = []
    for i, b in enumerate(bases):
        x, y = b['pos'][0], b['pos'][1]
        # The origin game's camera script read each base's own cameraZRotation and cameraOffset. Where
        # the scene leaves the roll at its prefab default, the base's own facing is the fallback,
        # which is what `Spawner.Reset` does when a base has no override.
        roll = b.get('cameraZRotation', b.get('zrot', 0.0))
        ox = b.get('cameraOffset.x', 0.0)
        oy = b.get('cameraOffset.y', 0.0)
        base_lines.append(
            f"    {{ id: {i + 1}, x: {n(x)}, y: {n(y)}, rot: {n(roll)}, "
            f"offsetX: {n(ox)}, offsetY: {n(oy)} }},")

    b = parts['bounds']
    src = f'''/**
 * `{level}` — the level, extracted from the Unity scene of the same name by the scripts in
 * `tools/`, which compose the transform hierarchy (nested prefabs and prefab variants included)
 * and read mesh bounds straight out of the referenced FBX files.
 *
 * The sets are split by where each collider sits on Unity's Z axis relative to the gameplay plane.
 * Ships fly at z=0 with a 0.8-radius collider and their Z frozen, so only geometry crossing
 * z ∈ [-0.8, 0.8] can be hit; everything else is structure at its own depth, which is what gives
 * the level its sense of a place rather than a flat maze.
 *
 * - `ARENA_WALLS`      — solid, in-plane. These are what a ship collides with.
 * - `ARENA_BACKDROP`   — structure behind the plane: platform decks, towers, walls seen edge-on.
 * - `ARENA_FOREGROUND` — geometry in front of the plane, which the player moves behind.
 * - `ARENA_FLOORS`     — charging-plate trigger volumes, if the level has any.
 *
 * Depths are in UNITY's Z (positive = away from the camera). three.js looks down -Z, so the
 * renderer negates them; see Arena.ts.
 *
 * `ARENA_BASES` carries each base's placement plus the camera roll and offset
 * the origin game's camera script reads off the base script's `cameraZRotation` / `cameraOffset`.
 */

export interface Rect {{ x0: number; y0: number; x1: number; y1: number; tag?: string; name?: string }}
export interface OrientedBox {{ cx: number; cy: number; hw: number; hh: number; rot: number; tag?: string; name?: string }}
export interface Circle {{ cx: number; cy: number; r: number; tag?: string; name?: string }}
export interface Slab extends Rect {{ z0: number; z1: number }}
export interface BaseDef {{ id: number; x: number; y: number; rot: number; offsetX: number; offsetY: number }}
export interface Point {{ x: number; y: number }}

export const ARENA_NAME = '{level}';

export const ARENA_BOUNDS = {{ x0: {b['x0']}, y0: {b['y0']}, x1: {b['x1']}, y1: {b['y1']} }};

/** Thickness of the boundary wall that closes the level — the Unity scene bounds the playfield
 *  with art meshes that carry no collider, so the enclosure is added here. */
export const BOUNDARY_THICKNESS = 8;

/**
 * `PowerupManager.SpawnRandom`'s scatter box. This level places no PowerupSpawnPoints, so pickups
 * land at a random clear spot inside it, exactly as the Unity manager does.
 */
export const POWERUP_BOX = {{ x0: -32, y0: -32, x1: 32, y1: 32 }};

export const ARENA_WALLS: Rect[] = [
{rect_lines(parts['walls'], tag=True)}
];

/**
 * The colliders a ship actually hits: Unity's BoxCollider2D / CircleCollider2D components on the
 * level prefabs, composed through the scene. In a 2D-physics game the 3D MeshColliders serve
 * raycasts and shadows and never touch the player. Boxes may be rotated
 * (`rot`, radians about Z); `ARENA_WALLS` above holds their axis-aligned bounds for line-of-sight
 * and AI, which is a conservative envelope, not the physics shape.
 */
export const ARENA_BOXES: OrientedBox[] = [
{box_lines(parts.get('boxes2d', []))}
];

export const ARENA_CIRCLES: Circle[] = [
{circle_lines(parts.get('circles2d', []))}
];

export const ARENA_BACKDROP: Slab[] = [
{rect_lines(parts['back'], z=True)}
];

export const ARENA_FOREGROUND: Slab[] = [
{rect_lines(parts['fore'], z=True)}
];

export const ARENA_FLOORS: Rect[] = [
{rect_lines(parts['floors'])}
];

export const ARENA_BASES: BaseDef[] = [
{chr(10).join(base_lines)}
];
'''
    open(OUT, 'w').write(src)
    print(f'wrote {OUT}: {len(parts["walls"])} walls, {len(parts["back"])} backdrop, '
          f'{len(parts["fore"])} foreground, {len(parts["floors"])} floors, {len(bases)} bases')


if __name__ == '__main__':
    main()
