export type SurfacePoint = [number, number, number]
export type SurfaceKind = 'bezier' | 'bspline' | 'loft'
export type SurfacePatch = { id: string; kind: SurfaceKind; poles: SurfacePoint[][]; degreeU: number; degreeV: number; knotsU: number[]; knotsV: number[]; trim?: { u: [number, number]; v: [number, number] }; tolerance: number }
export type SurfaceQuality = { patches: number; poles: number; bounds: { min: SurfacePoint; max: SurfacePoint }; trimmed: number; openEdges: number }
export type SurfaceContinuity = { relation: 'C0' | 'disconnected'; gap: number; tolerance: number }
export type SurfaceTopoRef = { patchId: string; edge: 'u0' | 'u1' | 'v0' | 'v1'; persistentId: string; status: 'stable' }
export type SurfaceSnapshot = { id: string; label: string; patches: SurfacePatch[]; sewn: string[][]; version: number }
export type SurfaceApi = { snapshot(): SurfaceSnapshot; addBezier(id: string, poles: SurfacePoint[][], tolerance?: number): SurfacePatch; addBspline(id: string, poles: SurfacePoint[][], options?: { degreeU?: number; degreeV?: number; knotsU?: number[]; knotsV?: number[]; tolerance?: number }): SurfacePatch; loft(id: string, profiles: SurfacePoint[][], options?: { ruled?: boolean; tolerance?: number }): SurfacePatch; fill(id: string, boundary: SurfacePoint[], tolerance?: number): SurfacePatch; offset(sourceId: string, outputId: string, distance: number): SurfacePatch; trim(id: string, u: [number, number], v: [number, number]): SurfacePatch; sew(ids: string[], tolerance?: number): string[]; continuity(firstId: string, secondId: string, tolerance?: number): SurfaceContinuity; topologyRefs(id: string): SurfaceTopoRef[]; analyze(): SurfaceQuality; exportObj(samples?: number): string }

const clone = <T>(value: T): T => JSON.parse(JSON.stringify(value)) as T
const point = (value: SurfacePoint, label: string): SurfacePoint => { if (value.length !== 3 || value.some((entry) => !Number.isFinite(entry))) throw new RangeError(`${label} must contain three finite coordinates.`); return [value[0], value[1], value[2]] }
const poles = (input: SurfacePoint[][]) => { if (input.length < 2 || input.some((row) => row.length < 2 || row.length !== input[0].length)) throw new RangeError('Surface poles require a rectangular grid of at least 2x2 points.'); return input.map((row) => row.map((entry) => point(entry, 'Surface pole'))) }
const distance = (a: SurfacePoint, b: SurfacePoint) => Math.hypot(a[0] - b[0], a[1] - b[1], a[2] - b[2])
const edge = (patch: SurfacePatch, side: 'start' | 'end') => patch.poles[side === 'start' ? 0 : patch.poles.length - 1]

export const createSurfaceDocument = (id = 'surface', label = 'Surface'): SurfaceApi => {
  const patches = new Map<string, SurfacePatch>(); const sewn: string[][] = []; let version = 0
  const add = (patch: SurfacePatch) => { if (patches.has(patch.id)) throw new RangeError(`Surface patch already exists: ${patch.id}`); patches.set(patch.id, patch); version += 1; return clone(patch) }
  const addBezier = (patchId: string, input: SurfacePoint[][], tolerance = 1e-6) => { if (!Number.isFinite(tolerance) || tolerance <= 0) throw new RangeError('Surface tolerance must be positive.'); const grid = poles(input); return add({ id: patchId, kind: 'bezier', poles: grid, degreeU: grid.length - 1, degreeV: grid[0].length - 1, knotsU: [], knotsV: [], tolerance }) }
  const addBspline = (patchId: string, input: SurfacePoint[][], options: { degreeU?: number; degreeV?: number; knotsU?: number[]; knotsV?: number[]; tolerance?: number } = {}) => { const grid = poles(input); const degreeU = options.degreeU ?? Math.min(3, grid.length - 1); const degreeV = options.degreeV ?? Math.min(3, grid[0].length - 1); if (!Number.isSafeInteger(degreeU) || degreeU < 1 || degreeU >= grid.length || !Number.isSafeInteger(degreeV) || degreeV < 1 || degreeV >= grid[0].length) throw new RangeError('BSpline degree must fit the pole grid.'); const knotsU = options.knotsU ? [...options.knotsU] : Array.from({ length: grid.length + degreeU + 1 }, (_, index) => index); const knotsV = options.knotsV ? [...options.knotsV] : Array.from({ length: grid[0].length + degreeV + 1 }, (_, index) => index); if (knotsU.length !== grid.length + degreeU + 1 || knotsV.length !== grid[0].length + degreeV + 1) throw new RangeError('BSpline knot vectors have an invalid length.'); return add({ id: patchId, kind: 'bspline', poles: grid, degreeU, degreeV, knotsU, knotsV, tolerance: options.tolerance ?? 1e-6 }) }
  const loft = (patchId: string, profiles: SurfacePoint[][], options: { ruled?: boolean; tolerance?: number } = {}) => { if (profiles.length < 2 || profiles.some((profile) => profile.length < 2 || profile.length !== profiles[0].length)) throw new RangeError('Surface loft requires two or more profiles with equal point counts.'); const grid = poles(profiles); return add({ id: patchId, kind: 'loft', poles: grid, degreeU: options.ruled ? 1 : Math.min(3, grid.length - 1), degreeV: 1, knotsU: [], knotsV: [], tolerance: options.tolerance ?? 1e-6 }) }
  const fill = (patchId: string, boundary: SurfacePoint[], tolerance = 1e-6) => { if (boundary.length !== 4) throw new RangeError('Surface fill requires four boundary corner points.'); const corners = boundary.map((entry) => point(entry, 'Surface fill boundary')); return addBezier(patchId, [[corners[0], corners[1]], [corners[3], corners[2]]], tolerance) }
  const offset = (sourceId: string, outputId: string, distanceValue: number) => { const source = patches.get(sourceId); if (!source) throw new RangeError(`Surface patch does not exist: ${sourceId}`); if (!Number.isFinite(distanceValue) || distanceValue === 0) throw new RangeError('Surface offset distance must be finite and non-zero.'); return add({ ...clone(source), id: outputId, poles: source.poles.map((row) => row.map((entry) => [entry[0], entry[1], entry[2] + distanceValue] as SurfacePoint)) }) }
  const trim = (patchId: string, u: [number, number], v: [number, number]) => { const patch = patches.get(patchId); if (!patch) throw new RangeError(`Surface patch does not exist: ${patchId}`); if (![...u, ...v].every((entry) => Number.isFinite(entry)) || u[0] < 0 || u[1] > 1 || u[0] >= u[1] || v[0] < 0 || v[1] > 1 || v[0] >= v[1]) throw new RangeError('Surface trim bounds must be ordered within [0, 1].'); patch.trim = { u: [...u], v: [...v] }; version += 1; return clone(patch) }
  const sew = (ids: string[], tolerance = 1e-6) => { if (ids.length < 2 || ids.some((patchId) => !patches.has(patchId))) throw new RangeError('Surface sewing requires at least two existing patches.'); const group = [...new Set(ids)]; for (const previous of sewn) if (previous.join('|') === group.join('|')) return [...previous]; for (let index = 1; index < group.length; index += 1) { const relation = continuity(group[index - 1], group[index], tolerance); if (relation.relation === 'disconnected') throw new RangeError(`Surface patches are outside sewing tolerance: ${relation.gap}`) } sewn.push(group); version += 1; return [...group] }
  const continuity = (firstId: string, secondId: string, tolerance = 1e-6): SurfaceContinuity => { const first = patches.get(firstId); const second = patches.get(secondId); if (!first || !second) throw new RangeError('Surface continuity requires existing patches.'); const a = edge(first, 'end'); const b = edge(second, 'start'); const gap = Math.max(...a.map((entry, index) => distance(entry, b[Math.min(index, b.length - 1)]))); return { relation: gap <= tolerance ? 'C0' : 'disconnected', gap, tolerance } }
  const topologyRefs = (patchId: string): SurfaceTopoRef[] => { const patch = patches.get(patchId); if (!patch) throw new RangeError(`Surface patch does not exist: ${patchId}`); return (['u0', 'u1', 'v0', 'v1'] as const).map((edgeName) => ({ patchId, edge: edgeName, persistentId: `${patchId}:${edgeName}`, status: 'stable' as const })) }
  const analyze = (): SurfaceQuality => { const min: SurfacePoint = [Infinity, Infinity, Infinity]; const max: SurfacePoint = [-Infinity, -Infinity, -Infinity]; let poleCount = 0; for (const patch of patches.values()) for (const row of patch.poles) for (const entry of row) { poleCount += 1; for (let axis = 0; axis < 3; axis += 1) { min[axis] = Math.min(min[axis], entry[axis]); max[axis] = Math.max(max[axis], entry[axis]) } } return { patches: patches.size, poles: poleCount, bounds: { min, max }, trimmed: [...patches.values()].filter((patch) => patch.trim !== undefined).length, openEdges: Math.max(0, patches.size * 4 - sewn.reduce((total, group) => total + (group.length - 1) * 2, 0)) } }
  const sample = (patch: SurfacePatch, u: number, v: number): SurfacePoint => { const row = patch.poles[Math.min(patch.poles.length - 1, Math.round(u * (patch.poles.length - 1)))]; const next = patch.poles[Math.min(patch.poles.length - 1, Math.min(patch.poles.length - 1, Math.round(u * (patch.poles.length - 1)) + 1))]; const first = row[Math.min(row.length - 1, Math.round(v * (row.length - 1)))]; const second = next[Math.min(next.length - 1, Math.round(v * (next.length - 1)))]; return [(first[0] + second[0]) / 2, (first[1] + second[1]) / 2, (first[2] + second[2]) / 2] }
  const exportObj = (samples = 2) => { if (!Number.isSafeInteger(samples) || samples < 2 || samples > 16) throw new RangeError('Surface export samples must be between 2 and 16.'); const vertices: SurfacePoint[] = []; const faces: string[] = []; for (const patch of patches.values()) { const offset = vertices.length; for (let u = 0; u < samples; u += 1) for (let v = 0; v < samples; v += 1) vertices.push(sample(patch, u / (samples - 1), v / (samples - 1))); for (let u = 0; u < samples - 1; u += 1) for (let v = 0; v < samples - 1; v += 1) { const base = offset + u * samples + v; faces.push(`f ${base + 1} ${base + samples + 1} ${base + samples + 2} ${base + 2}`) } } return `${vertices.map((entry) => `v ${entry.map((value) => value.toFixed(6)).join(' ')}`).concat(faces).join('\n')}\n` }
  const snapshot = (): SurfaceSnapshot => ({ id, label, patches: [...patches.values()].map(clone), sewn: clone(sewn), version })
  return { snapshot, addBezier, addBspline, loft, fill, offset, trim, sew, continuity, topologyRefs, analyze, exportObj }
}
