/**
 * SURFACES — the material framework (GDD §5.5). Friction / restitution / tags
 * per material so honey, ice, oil, mud and water are *data*, not code. The blob
 * controller (lane A) queries `surfaceAt(point)` to react to what it is standing
 * on; contraptions and courses stamp materials onto colliders via `applySurface`.
 *
 * Self-contained: no imports from other lanes. Colliders are tagged in a local
 * registry keyed by collider handle so `surfaceAt` can resolve a world point back
 * to its material with a short downward ray.
 */
import * as THREE from 'three'
import type RAPIER from '@dimforge/rapier3d-compat'

export type SurfaceName = 'normal' | 'ice' | 'oil' | 'honey' | 'mud' | 'water'

export interface SurfaceMaterial {
  name: SurfaceName
  /** Rapier collider friction coefficient. */
  friction: number
  /** Rapier collider restitution (bounciness). */
  restitution: number
  /**
   * How friction combines with the *other* collider (the blob). `Min` lets ice
   * stay slippery even under a grippy/green blob; `Max` lets honey stay sticky.
   */
  combine: 'average' | 'min' | 'max' | 'multiply'
  /** Extra per-frame linear damping the controller may apply while on it. */
  drag: number
  /** Behaviour flags read by gameplay (grip/slip/climb/rinse/anti-paint...). */
  tags: string[]
  /** Debug/VFX tint. */
  color: string
}

export const SURFACES: Record<SurfaceName, SurfaceMaterial> = {
  // baseline grippy course material
  normal: { name: 'normal', friction: 1.0, restitution: 0.15, combine: 'average', drag: 0.0, tags: [], color: '#c9b98f' },
  // near-frictionless: a rolling ball keeps going for ages
  ice: { name: 'ice', friction: 0.02, restitution: 0.05, combine: 'min', drag: 0.0, tags: ['slip'], color: '#bfe9ff' },
  // slick + flowy luge; no steering (GDD §5.5 oil)
  oil: { name: 'oil', friction: 0.04, restitution: 0.0, combine: 'min', drag: 0.02, tags: ['slip', 'flow', 'flammable'], color: '#3a2f4a' },
  // sticky: kills momentum fast, lets you climb (GDD §5.5 honey)
  honey: { name: 'honey', friction: 2.0, restitution: 0.0, combine: 'max', drag: 0.12, tags: ['sticky', 'climb'], color: '#e7a72b' },
  // anti-paint sludge: sticky and masks colour (GDD §5.5 mud)
  mud: { name: 'mud', friction: 1.6, restitution: 0.0, combine: 'max', drag: 0.18, tags: ['sticky', 'antipaint'], color: '#6b4a2b' },
  // gradual rinse lane
  water: { name: 'water', friction: 0.45, restitution: 0.0, combine: 'average', drag: 0.08, tags: ['flow', 'rinse'], color: '#2f7fd0' },
}

function combineRule(rapier: typeof RAPIER, combine: SurfaceMaterial['combine']) {
  switch (combine) {
    case 'min': return rapier.CoefficientCombineRule.Min
    case 'max': return rapier.CoefficientCombineRule.Max
    case 'multiply': return rapier.CoefficientCombineRule.Multiply
    default: return rapier.CoefficientCombineRule.Average
  }
}

/** Registry mapping collider.handle -> surface, so points can be resolved back. */
export class SurfaceRegistry {
  private byHandle = new Map<number, SurfaceMaterial>()
  constructor(private rapier: typeof RAPIER) {}

  /** Stamp a material onto a live collider and remember it. */
  apply(collider: RAPIER.Collider, name: SurfaceName): SurfaceMaterial {
    const surf = SURFACES[name]
    collider.setFriction(surf.friction)
    collider.setRestitution(surf.restitution)
    const rule = combineRule(this.rapier, surf.combine)
    collider.setFrictionCombineRule(rule)
    collider.setRestitutionCombineRule(rule)
    this.byHandle.set(collider.handle, surf)
    return surf
  }

  /** Look up the material for a specific collider (or undefined). */
  forCollider(collider: RAPIER.Collider): SurfaceMaterial | undefined {
    return this.byHandle.get(collider.handle)
  }

  /**
   * Resolve the surface under a world point (e.g. beneath the blob) by casting a
   * short ray straight down. Returns `normal` if the hit collider is untagged,
   * `undefined` only when nothing is below.
   */
  surfaceAt(physics: RAPIER.World, point: THREE.Vector3, reach = 1.5): SurfaceMaterial | undefined {
    const ray = new this.rapier.Ray(
      { x: point.x, y: point.y + 0.05, z: point.z },
      { x: 0, y: -1, z: 0 },
    )
    const hit = physics.castRay(ray, reach, true)
    if (!hit) return undefined
    return this.byHandle.get(hit.collider.handle) ?? SURFACES.normal
  }
}

/**
 * Convenience for callers that don't hold a registry: stamp friction/restitution
 * directly onto a collider. (Prefer `SurfaceRegistry.apply` so `surfaceAt` works.)
 */
export function applySurface(
  rapier: typeof RAPIER,
  collider: RAPIER.Collider,
  name: SurfaceName,
): SurfaceMaterial {
  const surf = SURFACES[name]
  collider.setFriction(surf.friction)
  collider.setRestitution(surf.restitution)
  const rule = combineRule(rapier, surf.combine)
  collider.setFrictionCombineRule(rule)
  collider.setRestitutionCombineRule(rule)
  return surf
}
