export type AssemblyPoint = { x: number; y: number; z: number }
export type AssemblyAxis = AssemblyPoint
export type AssemblyPlacement = AssemblyPoint & { yaw: number; pitch: number; roll: number }
export type AssemblyBounds = { min: AssemblyPoint; max: AssemblyPoint }
export type AssemblyComponent = { id: string; sourceObjectId: string; label: string; placement: AssemblyPlacement; grounded: boolean; bounds?: AssemblyBounds }
export type AssemblyConnector = { id: string; componentId: string; origin: AssemblyPoint; axis: AssemblyAxis }
export type AssemblyJointKind = 'fixed' | 'coincident' | 'distance' | 'angle' | 'concentric'
export type AssemblyJoint = { id: string; kind: AssemblyJointKind; first: string; second: string; value?: number; status: 'pending' | 'solved' | 'conflicting' }
export type AssemblyDiagnostic = { code: 'MISSING_CONNECTOR' | 'GROUND_CONFLICT' | 'UNSUPPORTED_CHAIN' | 'DISTANCE_CONFLICT' | 'ANGLE_CONFLICT'; jointId: string; message: string }
export type AssemblySnapshot = { id: string; label: string; components: AssemblyComponent[]; connectors: AssemblyConnector[]; joints: AssemblyJoint[]; diagnostics: AssemblyDiagnostic[]; version: number; solver: { status: 'unsolved' | 'solved' | 'conflicting'; iterations: number } }
export type AssemblyBomRow = { sourceObjectId: string; label: string; quantity: number; componentIds: string[] }
export type AssemblyCollision = { first: string; second: string; overlap: AssemblyBounds }
export type AssemblyVariant = { name: string; placements: AssemblyComponent[]; version: number }
export type AssemblyMotionFrame = { step: number; parameter: number; placement: AssemblyPlacement }

export type AssemblyApi = {
  snapshot(): AssemblySnapshot
  addComponent(input: Omit<AssemblyComponent, 'placement'> & { placement?: Partial<AssemblyPlacement> }): AssemblyComponent
  setPlacement(componentId: string, placement: Partial<AssemblyPlacement>): AssemblyComponent
  setGrounded(componentId: string, grounded: boolean): AssemblyComponent
  addConnector(input: { id: string; componentId: string; origin: AssemblyPoint; axis: AssemblyAxis }): AssemblyConnector
  addJoint(input: { id: string; kind: AssemblyJointKind; first: string; second: string; value?: number }): AssemblyJoint
  solve(): AssemblySnapshot
  bom(): AssemblyBomRow[]
  collisions(): AssemblyCollision[]
  exploded(distance?: number): AssemblyComponent[]
  variant(name: string, placements?: Record<string, Partial<AssemblyPlacement>>): AssemblyVariant
  motion(componentId: string, target: Partial<AssemblyPlacement>, steps: number): AssemblyMotionFrame[]
}

const finite = (value: number, name: string) => { if (!Number.isFinite(value)) throw new RangeError(`${name} must be finite.`) }
const point = (value: AssemblyPoint, name: string) => { for (const axis of ['x', 'y', 'z'] as const) finite(value[axis], `${name}.${axis}`); return { x: value.x, y: value.y, z: value.z } }
const axis = (value: AssemblyAxis, name: string) => { const result = point(value, name); const length = Math.hypot(result.x, result.y, result.z); if (length <= 1e-12) throw new RangeError(`${name} must be non-zero.`); return { x: result.x / length, y: result.y / length, z: result.z / length } }
const placement = (value?: Partial<AssemblyPlacement>): AssemblyPlacement => ({ x: value?.x ?? 0, y: value?.y ?? 0, z: value?.z ?? 0, yaw: value?.yaw ?? 0, pitch: value?.pitch ?? 0, roll: value?.roll ?? 0 })
const clonePlacement = (value: AssemblyPlacement): AssemblyPlacement => ({ ...value })
const cloneComponent = (value: AssemblyComponent): AssemblyComponent => ({ ...value, placement: clonePlacement(value.placement), bounds: value.bounds ? { min: { ...value.bounds.min }, max: { ...value.bounds.max } } : undefined })
const cloneConnector = (value: AssemblyConnector): AssemblyConnector => ({ ...value, origin: { ...value.origin }, axis: { ...value.axis } })
const cloneJoint = (value: AssemblyJoint): AssemblyJoint => ({ ...value })
const rotate = (p: AssemblyPoint, transform: AssemblyPlacement): AssemblyPoint => {
  const cy = Math.cos(transform.yaw), sy = Math.sin(transform.yaw), cp = Math.cos(transform.pitch), sp = Math.sin(transform.pitch), cr = Math.cos(transform.roll), sr = Math.sin(transform.roll)
  const x1 = cy * p.x - sy * p.y; const y1 = sy * p.x + cy * p.y; const z1 = p.z
  const x2 = cp * x1 + sp * z1; const y2 = y1; const z2 = -sp * x1 + cp * z1
  return { x: cr * x2 - sr * y2, y: sr * x2 + cr * y2, z: z2 }
}
const worldPoint = (connector: AssemblyConnector, component: AssemblyComponent) => { const local = rotate(connector.origin, component.placement); return { x: local.x + component.placement.x, y: local.y + component.placement.y, z: local.z + component.placement.z } }
const worldAxis = (connector: AssemblyConnector, component: AssemblyComponent) => rotate(connector.axis, component.placement)
const distance = (a: AssemblyPoint, b: AssemblyPoint) => Math.hypot(a.x - b.x, a.y - b.y, a.z - b.z)
const dot = (a: AssemblyPoint, b: AssemblyPoint) => a.x * b.x + a.y * b.y + a.z * b.z
const clamp = (value: number, minimum: number, maximum: number) => Math.max(minimum, Math.min(maximum, value))
const bounds = (value: AssemblyBounds | undefined): AssemblyBounds => { const result = value ?? { min: { x: -0.5, y: -0.5, z: -0.5 }, max: { x: 0.5, y: 0.5, z: 0.5 } }; const min = point(result.min, 'Assembly bounds min'); const max = point(result.max, 'Assembly bounds max'); if (min.x > max.x || min.y > max.y || min.z > max.z) throw new RangeError('Assembly bounds min must not exceed max.'); return { min, max } }
const worldBounds = (component: AssemblyComponent): AssemblyBounds => { const local = component.bounds ?? bounds(undefined); return { min: { x: local.min.x + component.placement.x, y: local.min.y + component.placement.y, z: local.min.z + component.placement.z }, max: { x: local.max.x + component.placement.x, y: local.max.y + component.placement.y, z: local.max.z + component.placement.z } } }

export const createAssembly = (id = 'assembly', label = 'Assembly'): AssemblyApi => {
  const components = new Map<string, AssemblyComponent>()
  const connectors = new Map<string, AssemblyConnector>()
  const joints = new Map<string, AssemblyJoint>()
  let diagnostics: AssemblyDiagnostic[] = []
  let version = 0
  let solver: AssemblySnapshot['solver'] = { status: 'unsolved', iterations: 0 }
  const snapshot = (): AssemblySnapshot => ({ id, label, components: [...components.values()].map(cloneComponent), connectors: [...connectors.values()].map(cloneConnector), joints: [...joints.values()].map(cloneJoint), diagnostics: diagnostics.map((entry) => ({ ...entry })), version, solver: { ...solver } })
  const componentById = (componentId: string) => { const component = components.get(componentId); if (!component) throw new RangeError(`Assembly component does not exist: ${componentId}`); return component }
  const connectorById = (connectorId: string) => { const connector = connectors.get(connectorId); if (!connector) throw new RangeError(`Assembly connector does not exist: ${connectorId}`); return connector }
  const addComponent = (input: Omit<AssemblyComponent, 'placement'> & { placement?: Partial<AssemblyPlacement> }) => { if (!input.id.trim() || components.has(input.id)) throw new RangeError(`Assembly component already exists: ${input.id}`); if (!input.sourceObjectId.trim()) throw new RangeError('Assembly component sourceObjectId is required.'); const value: AssemblyComponent = { id: input.id, sourceObjectId: input.sourceObjectId, label: input.label, placement: placement(input.placement), grounded: Boolean(input.grounded), bounds: input.bounds ? bounds(input.bounds) : undefined }; components.set(value.id, value); version += 1; return cloneComponent(value) }
  const setPlacement = (componentId: string, update: Partial<AssemblyPlacement>) => { const component = componentById(componentId); const next = { ...component.placement, ...update }; component.placement = placement(next); version += 1; solver = { status: 'unsolved', iterations: 0 }; return cloneComponent(component) }
  const setGrounded = (componentId: string, grounded: boolean) => { const component = componentById(componentId); component.grounded = grounded; version += 1; solver = { status: 'unsolved', iterations: 0 }; return cloneComponent(component) }
  const addConnector = (input: { id: string; componentId: string; origin: AssemblyPoint; axis: AssemblyAxis }) => { componentById(input.componentId); if (!input.id.trim() || connectors.has(input.id)) throw new RangeError(`Assembly connector already exists: ${input.id}`); const value: AssemblyConnector = { id: input.id, componentId: input.componentId, origin: point(input.origin, 'Assembly connector origin'), axis: axis(input.axis, 'Assembly connector axis') }; connectors.set(value.id, value); version += 1; return cloneConnector(value) }
  const addJoint = (input: { id: string; kind: AssemblyJointKind; first: string; second: string; value?: number }) => { if (!input.id.trim() || joints.has(input.id)) throw new RangeError(`Assembly joint already exists: ${input.id}`); connectorById(input.first); connectorById(input.second); if (input.first === input.second) throw new RangeError('Assembly joint requires two distinct connectors.'); if (input.value !== undefined) finite(input.value, 'Assembly joint value'); if ((input.kind === 'distance' && (input.value === undefined || input.value < 0)) || (input.kind === 'angle' && (input.value === undefined || input.value < -Math.PI || input.value > Math.PI))) throw new RangeError(`Assembly ${input.kind} joint value is outside the supported range.`); const value: AssemblyJoint = { id: input.id, kind: input.kind, first: input.first, second: input.second, value: input.value, status: 'pending' }; joints.set(value.id, value); version += 1; solver = { status: 'unsolved', iterations: 0 }; return cloneJoint(value) }
  const solve = () => {
    diagnostics = []
    let iterations = 0
    let conflicting = false
    for (const joint of joints.values()) {
      const firstConnector = connectors.get(joint.first); const secondConnector = connectors.get(joint.second)
      if (!firstConnector || !secondConnector) { joint.status = 'conflicting'; diagnostics.push({ code: 'MISSING_CONNECTOR', jointId: joint.id, message: 'Joint references a missing connector.' }); conflicting = true; continue }
      const firstComponent = componentById(firstConnector.componentId); const secondComponent = componentById(secondConnector.componentId)
      if (firstComponent.id === secondComponent.id) { joint.status = 'conflicting'; diagnostics.push({ code: 'UNSUPPORTED_CHAIN', jointId: joint.id, message: 'A joint cannot connect two connectors on the same component.' }); conflicting = true; continue }
      const firstPoint = worldPoint(firstConnector, firstComponent); const secondPoint = worldPoint(secondConnector, secondComponent)
      const movable = firstComponent.grounded ? (secondComponent.grounded ? null : secondComponent) : firstComponent
      if (joint.kind === 'fixed' || joint.kind === 'coincident' || joint.kind === 'concentric') {
        if (!movable) { if (distance(firstPoint, secondPoint) > 1e-7) { joint.status = 'conflicting'; diagnostics.push({ code: 'GROUND_CONFLICT', jointId: joint.id, message: 'Grounded components cannot satisfy the joint.' }); conflicting = true; continue } }
        else { const target = movable.id === secondComponent.id ? firstPoint : secondPoint; const current = movable.id === secondComponent.id ? secondPoint : firstPoint; movable.placement.x += target.x - current.x; movable.placement.y += target.y - current.y; movable.placement.z += target.z - current.z }
      }
      const afterFirst = worldPoint(firstConnector, firstComponent); const afterSecond = worldPoint(secondConnector, secondComponent)
      const actualDistance = distance(afterFirst, afterSecond)
      if (joint.kind === 'distance') {
        const desired = joint.value ?? 0; if (firstComponent.grounded && secondComponent.grounded) { if (Math.abs(actualDistance - desired) > 1e-7) { joint.status = 'conflicting'; diagnostics.push({ code: 'DISTANCE_CONFLICT', jointId: joint.id, message: `Distance ${actualDistance} does not match ${desired}.` }); conflicting = true; continue } }
        else { const target = movable?.id === secondComponent.id ? afterFirst : afterSecond; const current = movable?.id === secondComponent.id ? afterSecond : afterFirst; const raw = { x: current.x - target.x, y: current.y - target.y, z: current.z - target.z }; const fallback = movable?.id === secondComponent.id ? worldAxis(secondConnector, secondComponent) : worldAxis(firstConnector, firstComponent); const direction = Math.hypot(raw.x, raw.y, raw.z) > 1e-12 ? raw : fallback; const length = Math.hypot(direction.x, direction.y, direction.z) || 1; const scale = (desired - actualDistance) / length; if (movable) { movable.placement.x += direction.x * scale; movable.placement.y += direction.y * scale; movable.placement.z += direction.z * scale } }
      }
      if (joint.kind === 'angle') {
        const actual = Math.acos(clamp(dot(worldAxis(firstConnector, firstComponent), worldAxis(secondConnector, secondComponent)), -1, 1))
        const desired = joint.value ?? 0
        if (firstComponent.grounded && secondComponent.grounded && Math.abs(actual - desired) > 1e-7) { joint.status = 'conflicting'; diagnostics.push({ code: 'ANGLE_CONFLICT', jointId: joint.id, message: `Angle ${actual} does not match ${desired}.` }); conflicting = true; continue }
        if (movable && Math.abs(actual - desired) > 1e-7) movable.placement.yaw += (movable.id === secondComponent.id ? 1 : -1) * (desired - actual)
      }
      joint.status = 'solved'; iterations += 1
    }
    solver = { status: conflicting ? 'conflicting' : 'solved', iterations }
    version += 1
    return snapshot()
  }
  const bom = (): AssemblyBomRow[] => { const rows = new Map<string, AssemblyBomRow>(); for (const component of components.values()) { const row = rows.get(component.sourceObjectId) ?? { sourceObjectId: component.sourceObjectId, label: component.label, quantity: 0, componentIds: [] }; row.quantity += 1; row.componentIds.push(component.id); rows.set(component.sourceObjectId, row) } return [...rows.values()].map((row) => ({ ...row, componentIds: [...row.componentIds].sort() })).sort((first, second) => first.sourceObjectId.localeCompare(second.sourceObjectId)) }
  const collisions = (): AssemblyCollision[] => { const output: AssemblyCollision[] = []; const list = [...components.values()]; for (let first = 0; first < list.length; first += 1) for (let second = first + 1; second < list.length; second += 1) { const a = worldBounds(list[first]); const b = worldBounds(list[second]); const min = { x: Math.max(a.min.x, b.min.x), y: Math.max(a.min.y, b.min.y), z: Math.max(a.min.z, b.min.z) }; const max = { x: Math.min(a.max.x, b.max.x), y: Math.min(a.max.y, b.max.y), z: Math.min(a.max.z, b.max.z) }; if (min.x < max.x && min.y < max.y && min.z < max.z) output.push({ first: list[first].id, second: list[second].id, overlap: { min, max } }) } return output }
  const exploded = (offset = 1): AssemblyComponent[] => { finite(offset, 'Assembly explode distance'); if (offset < 0) throw new RangeError('Assembly explode distance must be non-negative.'); const center = [...components.values()].reduce((sum, component) => ({ x: sum.x + component.placement.x, y: sum.y + component.placement.y, z: sum.z + component.placement.z }), { x: 0, y: 0, z: 0 }); const count = components.size || 1; return [...components.values()].map((component, index) => { const dx = component.placement.x - center.x / count; const dy = component.placement.y - center.y / count; const dz = component.placement.z - center.z / count; const length = Math.hypot(dx, dy, dz) || 1; return cloneComponent({ ...component, placement: { ...component.placement, x: component.placement.x + offset * dx / length, y: component.placement.y + offset * dy / length, z: component.placement.z + offset * dz / length } }) }) }
  const variant = (name: string, updates: Record<string, Partial<AssemblyPlacement>> = {}): AssemblyVariant => { if (!name.trim()) throw new RangeError('Assembly variant name is required.'); const placements = [...components.values()].map((component) => cloneComponent({ ...component, placement: placement({ ...component.placement, ...(updates[component.id] ?? {}) }) })); return { name, placements, version }
  }
  const motion = (componentId: string, target: Partial<AssemblyPlacement>, steps: number): AssemblyMotionFrame[] => {
    const component = componentById(componentId)
    if (!Number.isSafeInteger(steps) || steps < 1 || steps > 10_000) throw new RangeError('Assembly motion steps must be between 1 and 10000.')
    const end = placement({ ...component.placement, ...target })
    return Array.from({ length: steps + 1 }, (_, step) => { const parameter = step / steps; return { step, parameter, placement: { x: component.placement.x + (end.x - component.placement.x) * parameter, y: component.placement.y + (end.y - component.placement.y) * parameter, z: component.placement.z + (end.z - component.placement.z) * parameter, yaw: component.placement.yaw + (end.yaw - component.placement.yaw) * parameter, pitch: component.placement.pitch + (end.pitch - component.placement.pitch) * parameter, roll: component.placement.roll + (end.roll - component.placement.roll) * parameter } } })
  }
  return { snapshot, addComponent, setPlacement, setGrounded, addConnector, addJoint, solve, bom, collisions, exploded, variant, motion }
}
