import type { TopoRefValue } from './types'

export type SketchPoint = { x: number; y: number }

export type SketchGeometry =
  | { id: string; type: 'point'; position: SketchPoint; construction?: boolean }
  | { id: string; type: 'line'; start: SketchPoint; end: SketchPoint; construction?: boolean }
  | { id: string; type: 'circle'; center: SketchPoint; radius: number; construction?: boolean }
  | { id: string; type: 'arc'; center: SketchPoint; radius: number; startAngle: number; endAngle: number; construction?: boolean }
  | { id: string; type: 'ellipse'; center: SketchPoint; majorRadius: number; minorRadius: number; rotation: number; construction?: boolean }
  | { id: string; type: 'arcEllipse'; center: SketchPoint; majorRadius: number; minorRadius: number; rotation: number; startAngle: number; endAngle: number; construction?: boolean }
  | { id: string; type: 'arcHyperbola'; center: SketchPoint; majorRadius: number; minorRadius: number; rotation: number; startAngle: number; endAngle: number; construction?: boolean }
  | { id: string; type: 'arcParabola'; center: SketchPoint; focal: number; rotation: number; startAngle: number; endAngle: number; construction?: boolean }
  | { id: string; type: 'bspline'; degree: number; controlPoints: SketchPoint[]; weights?: number[]; knots?: number[]; periodic?: boolean; construction?: boolean }

export type SketchPointRef = { geometryId: string; point: 'start' | 'end' | 'center' | 'position' }

export type SketchExternalMode = 'projection' | 'intersection' | 'both'

export type SketchExternalGeometry = {
  id: string
  source: TopoRefValue
  projection: SketchGeometry
  construction: true
  mode?: SketchExternalMode
  defining?: boolean
  frozen?: boolean
  detached?: boolean
  missing?: boolean
  sync?: boolean
}

export type SketchConstraint =
  | { id: string; type: 'coincident'; first: SketchPointRef; second: SketchPointRef; driving?: boolean }
  | { id: string; type: 'horizontal' | 'vertical'; geometryId: string; driving?: boolean }
  | { id: string; type: 'distance' | 'distanceX' | 'distanceY'; first: SketchPointRef; second: SketchPointRef; value: number; driving?: boolean }
  | { id: string; type: 'radius'; geometryId: string; value: number; driving?: boolean }
  | { id: string; type: 'diameter'; geometryId: string; value: number; driving?: boolean }
  | { id: string; type: 'angle'; geometryId: string; value: number; driving?: boolean }
  | { id: string; type: 'equal'; firstGeometryId: string; secondGeometryId: string; driving?: boolean }
  | { id: string; type: 'parallel' | 'perpendicular'; firstGeometryId: string; secondGeometryId: string; driving?: boolean }
  | { id: string; type: 'pointOnObject'; point: SketchPointRef; geometryId: string; driving?: boolean }
  | { id: string; type: 'symmetric'; first: SketchPointRef; second: SketchPointRef; center: SketchPointRef; driving?: boolean }
  | { id: string; type: 'tangent'; firstGeometryId: string; secondGeometryId: string; driving?: boolean }
  | { id: string; type: 'block'; geometryId: string; driving?: boolean }
  | { id: string; type: 'weight'; geometryId: string; controlPointIndex: number; value: number; driving?: boolean }
  | { id: string; type: 'snellsLaw'; first: SketchPointRef; second: SketchPointRef; boundaryGeometryId: string; value: number; driving?: boolean }
  | { id: string; type: 'snellsLaw'; firstGeometryId: string; secondGeometryId: string; value: number; driving?: boolean }
  | { id: string; type: 'internalAlignment'; geometryId: string; internalGeometryIndex: number; alignmentType: 'ellipse-major' | 'ellipse-minor' | 'ellipse-focus' | 'hyperbola-major' | 'hyperbola-minor' | 'hyperbola-focus' | 'parabola-focus' | 'parabola-focal-axis' | 'bspline-control-point' | 'bspline-knot'; driving?: boolean }

export const FREECAD_SKETCHER_GEOMETRY_TYPES = Object.freeze(['point', 'line', 'arc', 'circle', 'ellipse', 'arcEllipse', 'arcHyperbola', 'arcParabola', 'bspline'] as const)
export const FREECAD_SKETCHER_CONSTRAINT_TYPES = Object.freeze(['coincident', 'horizontal', 'vertical', 'parallel', 'tangent', 'distance', 'distanceX', 'distanceY', 'angle', 'perpendicular', 'radius', 'equal', 'pointOnObject', 'symmetric', 'internalAlignment', 'snellsLaw', 'block', 'diameter', 'weight'] as const)
export const FREECAD_SKETCHER_INTERNAL_ALIGNMENT_TYPES = Object.freeze(['ellipse-major', 'ellipse-minor', 'ellipse-focus', 'hyperbola-major', 'hyperbola-minor', 'hyperbola-focus', 'parabola-focus', 'parabola-focal-axis', 'bspline-control-point', 'bspline-knot'] as const)

export type SketchSolverStatus = 'solved' | 'under-constrained' | 'conflicting' | 'invalid'

export type SketchDiagnostic = {
  code: 'UNKNOWN_GEOMETRY' | 'UNKNOWN_POINT' | 'INVALID_VALUE' | 'UNSUPPORTED_GEOMETRY' | 'UNSUPPORTED_CONSTRAINT' | 'REFERENCE_DIMENSION' | 'CONSTRAINT_CONFLICT' | 'REDUNDANT_CONSTRAINT' | 'SOLVER_NOT_CONVERGED'
  geometryId?: string
  constraintId?: string
  message: string
}

export type SketchSnapshot = {
  id: string
  geometry: SketchGeometry[]
  externalGeometry: SketchExternalGeometry[]
  constraints: SketchConstraint[]
  solver: {
    status: SketchSolverStatus
    degreesOfFreedom: number
    residual: number
    iterations: number
    diagnostics: SketchDiagnostic[]
  }
}

export type SketchSolveOptions = {
  tolerance?: number
  maxIterations?: number
}

export type SketchAutoConstraintSuggestion = { id: string; constraint: SketchConstraint; reason: 'horizontal' | 'vertical' | 'coincident' }

export type SketchSolveResult = {
  snapshot: SketchSnapshot
  status: SketchSolverStatus
  degreesOfFreedom: number
  residual: number
  iterations: number
  diagnostics: SketchDiagnostic[]
}

const assertFinitePoint = (point: SketchPoint, label: string) => {
  if (!Number.isFinite(point.x) || !Number.isFinite(point.y)) throw new RangeError(`${label} coordinates must be finite.`)
}

export const validateSketchGeometry = (geometry: SketchGeometry): void => {
  if (!geometry.id.trim()) throw new TypeError('Sketch geometry IDs must be non-empty.')
  if (geometry.type === 'point') assertFinitePoint(geometry.position, `${geometry.id}.position`)
  if (geometry.type === 'line') { assertFinitePoint(geometry.start, `${geometry.id}.start`); assertFinitePoint(geometry.end, `${geometry.id}.end`) }
  if (geometry.type === 'circle' || geometry.type === 'arc') {
    assertFinitePoint(geometry.center, `${geometry.id}.center`)
    if (!Number.isFinite(geometry.radius) || geometry.radius <= 0) throw new RangeError(`${geometry.id} radius must be finite and greater than zero.`)
    if (geometry.type === 'arc' && (!Number.isFinite(geometry.startAngle) || !Number.isFinite(geometry.endAngle))) throw new RangeError(`${geometry.id} angles must be finite.`)
  }
  if (geometry.type === 'ellipse' || geometry.type === 'arcEllipse' || geometry.type === 'arcHyperbola') {
    assertFinitePoint(geometry.center, `${geometry.id}.center`)
    if (!Number.isFinite(geometry.majorRadius) || geometry.majorRadius <= 0 || !Number.isFinite(geometry.minorRadius) || geometry.minorRadius <= 0) throw new RangeError(`${geometry.id} radii must be finite and greater than zero.`)
    if ((geometry.type === 'ellipse' || geometry.type === 'arcEllipse') && geometry.majorRadius < geometry.minorRadius) throw new RangeError(`${geometry.id} has invalid axis parameters.`)
    if (!Number.isFinite(geometry.rotation)) throw new RangeError(`${geometry.id} has invalid axis parameters.`)
    if (geometry.type !== 'ellipse' && (!Number.isFinite(geometry.startAngle) || !Number.isFinite(geometry.endAngle) || geometry.startAngle === geometry.endAngle)) throw new RangeError(`${geometry.id} parameter range must contain two distinct finite values.`)
  }
  if (geometry.type === 'arcParabola') {
    assertFinitePoint(geometry.center, `${geometry.id}.center`)
    if (!Number.isFinite(geometry.focal) || geometry.focal <= 0 || !Number.isFinite(geometry.rotation)) throw new RangeError(`${geometry.id} has invalid parabola parameters.`)
    if (!Number.isFinite(geometry.startAngle) || !Number.isFinite(geometry.endAngle) || geometry.startAngle === geometry.endAngle) throw new RangeError(`${geometry.id} parameter range must contain two distinct finite values.`)
  }
  if (geometry.type === 'bspline') {
    if (!Number.isSafeInteger(geometry.degree) || geometry.degree < 1) throw new RangeError(`${geometry.id} degree must be a positive integer.`)
    if (geometry.controlPoints.length < geometry.degree + 1) throw new RangeError(`${geometry.id} requires at least degree + 1 control points.`)
    geometry.controlPoints.forEach((point, index) => assertFinitePoint(point, `${geometry.id}.controlPoints[${index}]`))
    if (geometry.weights && (geometry.weights.length !== geometry.controlPoints.length || geometry.weights.some((weight) => !Number.isFinite(weight) || weight <= 0))) throw new RangeError(`${geometry.id} weights must be positive finite values matching control points.`)
    if (geometry.knots && (geometry.knots.length !== geometry.controlPoints.length + geometry.degree + 1 || geometry.knots.some((knot, index) => !Number.isFinite(knot) || (index > 0 && knot < geometry.knots![index - 1])))) throw new RangeError(`${geometry.id} knots must be non-decreasing and have controlPointCount + degree + 1 entries.`)
    if (geometry.periodic && geometry.controlPoints.length <= geometry.degree) throw new RangeError(`${geometry.id} periodic curves require more control points than degree.`)
  }
}

export const validateSketchSnapshot = (sketch: SketchSnapshot): void => {
  if (!sketch.id.trim()) throw new TypeError('Sketch IDs must be non-empty.')
  const geometryIds = new Set<string>()
  for (const geometry of sketch.geometry) { validateSketchGeometry(geometry); if (geometryIds.has(geometry.id)) throw new TypeError(`Duplicate sketch geometry ID '${geometry.id}'.`); geometryIds.add(geometry.id) }
  const constraintIds = new Set<string>()
  for (const constraint of sketch.constraints) { if (!constraint.id.trim()) throw new TypeError('Sketch constraint IDs must be non-empty.'); if (constraintIds.has(constraint.id)) throw new TypeError(`Duplicate sketch constraint ID '${constraint.id}'.`); constraintIds.add(constraint.id) }
  const externalIds = new Set<string>()
  const projectionIds = new Set(geometryIds)
  const externalGroupState = new Map<string, string>()
  for (const external of sketch.externalGeometry) {
    if (!external.id.trim()) throw new TypeError('External geometry IDs must be non-empty.')
    if (externalIds.has(external.id)) throw new TypeError(`Duplicate external geometry ID '${external.id}'.`)
    externalIds.add(external.id)
    if (external.construction !== true) throw new TypeError(`External geometry '${external.id}' must be construction geometry.`)
    if (external.mode !== undefined && external.mode !== 'projection' && external.mode !== 'intersection' && external.mode !== 'both') throw new TypeError(`External geometry '${external.id}' has an invalid mode.`)
    for (const flag of ['defining', 'frozen', 'detached', 'missing', 'sync'] as const) if (external[flag] !== undefined && typeof external[flag] !== 'boolean') throw new TypeError(`External geometry '${external.id}' ${flag} flag must be boolean.`)
    if (external.sync && !external.frozen) throw new TypeError(`External geometry '${external.id}' can only synchronize while frozen.`)
    validateSketchGeometry(external.projection)
    if (projectionIds.has(external.projection.id)) throw new TypeError(`Duplicate sketch projection geometry ID '${external.projection.id}'.`)
    projectionIds.add(external.projection.id)
    const groupKey = `${external.source.objectId}\0${external.source.kind}\0${external.source.persistentId}`
    const stateKey = JSON.stringify({ mode: external.mode ?? 'projection', defining: external.defining ?? false, frozen: external.frozen ?? false, detached: external.detached ?? false, missing: external.missing ?? false, sync: external.sync ?? false })
    const existingState = externalGroupState.get(groupKey)
    if (existingState !== undefined && existingState !== stateKey) throw new TypeError(`External geometry group '${external.source.objectId}.${external.source.persistentId}' has conflicting mode or state flags.`)
    externalGroupState.set(groupKey, stateKey)
  }
}

export const sketchGeometrySignature = (geometry: SketchGeometry): string => {
  validateSketchGeometry(geometry)
  return JSON.stringify(geometry)
}

export const cloneSketchGeometry = (geometry: SketchGeometry): SketchGeometry => {
  validateSketchGeometry(geometry)
  if (geometry.type === 'line') return { ...geometry, start: { ...geometry.start }, end: { ...geometry.end } }
  if (geometry.type === 'circle' || geometry.type === 'arc' || geometry.type === 'ellipse' || geometry.type === 'arcEllipse' || geometry.type === 'arcHyperbola' || geometry.type === 'arcParabola') return { ...geometry, center: { ...geometry.center } }
  if (geometry.type === 'bspline') return { ...geometry, controlPoints: geometry.controlPoints.map((point) => ({ ...point })), weights: geometry.weights ? [...geometry.weights] : undefined, knots: geometry.knots ? [...geometry.knots] : undefined }
  return { ...geometry, position: { ...geometry.position } }
}

export const projectSketchGeometry = (geometry: SketchGeometry, projectedId = `projection-${geometry.id}`): SketchGeometry => ({ ...cloneSketchGeometry(geometry), id: projectedId, construction: true })

export const carbonCopySketchGeometry = (geometry: SketchGeometry, idPrefix = 'carboncopy'): SketchGeometry => ({ ...cloneSketchGeometry(geometry), id: `${idPrefix}-${geometry.id}` })

export type BsplineGeometryPatch = Partial<Pick<Extract<SketchGeometry, { type: 'bspline' }>, 'degree' | 'controlPoints' | 'weights' | 'knots' | 'periodic' | 'construction'>>

export const editBsplineGeometry = (geometry: Extract<SketchGeometry, { type: 'bspline' }>, patch: BsplineGeometryPatch): Extract<SketchGeometry, { type: 'bspline' }> => {
  const edited: Extract<SketchGeometry, { type: 'bspline' }> = {
    ...geometry,
    ...patch,
    id: geometry.id,
    controlPoints: (patch.controlPoints ?? geometry.controlPoints).map((point) => ({ ...point })),
    weights: patch.weights ? [...patch.weights] : patch.weights === undefined && geometry.weights ? [...geometry.weights] : undefined,
    knots: patch.knots ? [...patch.knots] : patch.knots === undefined && geometry.knots ? [...geometry.knots] : undefined,
  }
  validateSketchGeometry(edited)
  return edited
}

export const editSketchBspline = (sketch: SketchSnapshot, geometryId: string, patch: BsplineGeometryPatch): SketchSnapshot => {
  const edited = cloneSketch(sketch)
  const index = edited.geometry.findIndex((geometry) => geometry.id === geometryId)
  if (index < 0) throw new RangeError(`Sketch B-spline '${geometryId}' does not exist.`)
  const geometry = edited.geometry[index]
  if (geometry.type !== 'bspline') throw new TypeError(`Sketch geometry '${geometryId}' is not a B-spline.`)
  edited.geometry[index] = editBsplineGeometry(geometry, patch)
  validateSketchSnapshot(edited)
  return edited
}

export const cloneSketchConstraint = (constraint: SketchConstraint): SketchConstraint => {
  if (constraint.type === 'coincident' || constraint.type === 'distance' || constraint.type === 'distanceX' || constraint.type === 'distanceY') return { ...constraint, first: { ...constraint.first }, second: { ...constraint.second } }
  if (constraint.type === 'snellsLaw' && 'boundaryGeometryId' in constraint) return { ...constraint, first: { ...constraint.first }, second: { ...constraint.second } }
  if (constraint.type === 'pointOnObject') return { ...constraint, point: { ...constraint.point } }
  if (constraint.type === 'symmetric') return { ...constraint, first: { ...constraint.first }, second: { ...constraint.second }, center: { ...constraint.center } }
  return { ...constraint }
}

export const cloneSketch = (sketch: SketchSnapshot): SketchSnapshot => {
  validateSketchSnapshot(sketch)
  return {
    ...sketch,
    geometry: sketch.geometry.map(cloneSketchGeometry),
    externalGeometry: (sketch.externalGeometry ?? []).map((external) => ({ ...external, source: { ...external.source, candidates: external.source.candidates ? [...external.source.candidates] : undefined }, projection: cloneSketchGeometry(external.projection), construction: true })),
    constraints: sketch.constraints.map(cloneSketchConstraint),
    solver: { ...sketch.solver, diagnostics: sketch.solver.diagnostics.map((diagnostic) => ({ ...diagnostic })) },
  }
}

export const createSketch = (id: string, geometry: SketchGeometry[] = [], constraints: SketchConstraint[] = []): SketchSnapshot => {
  const sketch: SketchSnapshot = { id, geometry: geometry.map(cloneSketchGeometry), externalGeometry: [], constraints: constraints.map(cloneSketchConstraint), solver: { status: geometry.length === 0 ? 'solved' : 'under-constrained', degreesOfFreedom: 0, residual: 0, iterations: 0, diagnostics: [] } }
  validateSketchSnapshot(sketch)
  return sketch
}

export const dragSketchPoint = (sketch: SketchSnapshot, pointRef: SketchPointRef, target: SketchPoint, options: SketchSolveOptions = {}): SketchSolveResult => {
  assertFinitePoint(target, 'Sketch drag target')
  const edited = cloneSketch(sketch)
  const geometry = edited.geometry.find((candidate) => candidate.id === pointRef.geometryId)
  if (!geometry) throw new RangeError(`Sketch geometry '${pointRef.geometryId}' does not exist.`)
  const current = pointFor(geometry, pointRef.point, 'drag', [])
  if (!current) throw new RangeError(`Point '${pointRef.point}' is not valid for ${geometry.type} '${geometry.id}'.`)
  adjustPoint(geometry, pointRef.point, target, new Set())
  if (geometry.type === 'line' && (pointRef.point === 'start' || pointRef.point === 'end')) {
    const other = pointRef.point === 'start' ? geometry.end : geometry.start
    if (edited.constraints.some((constraint) => constraint.type === 'horizontal' && constraint.geometryId === geometry.id)) other.y = target.y
    if (edited.constraints.some((constraint) => constraint.type === 'vertical' && constraint.geometryId === geometry.id)) other.x = target.x
  }
  return solveSketch(edited, options)
}

const constraintReferencesGeometry = (constraint: SketchConstraint, geometryId: string) => {
  if (constraint.type === 'snellsLaw' && 'boundaryGeometryId' in constraint && constraint.boundaryGeometryId === geometryId) return true
  if ('geometryId' in constraint && constraint.geometryId === geometryId) return true
  if ('firstGeometryId' in constraint && constraint.firstGeometryId === geometryId) return true
  if ('secondGeometryId' in constraint && constraint.secondGeometryId === geometryId) return true
  if ('first' in constraint && constraint.first.geometryId === geometryId) return true
  if ('second' in constraint && constraint.second.geometryId === geometryId) return true
  if ('center' in constraint && constraint.center.geometryId === geometryId) return true
  if ('point' in constraint && constraint.point.geometryId === geometryId) return true
  return false
}

export const splitSketchLine = (sketch: SketchSnapshot, geometryId: string, splitPoint: SketchPoint, newGeometryId: string, tolerance = 1e-7): SketchSnapshot => {
  assertFinitePoint(splitPoint, 'Sketch line split point')
  if (!newGeometryId.trim()) throw new TypeError('Split geometry ID must be non-empty.')
  if (!Number.isFinite(tolerance) || tolerance <= 0) throw new RangeError('Split tolerance must be finite and greater than zero.')
  const edited = cloneSketch(sketch)
  if (edited.geometry.some((geometry) => geometry.id === newGeometryId)) throw new RangeError(`Sketch geometry '${newGeometryId}' already exists.`)
  const index = edited.geometry.findIndex((geometry) => geometry.id === geometryId)
  if (index < 0) throw new RangeError(`Sketch geometry '${geometryId}' does not exist.`)
  const geometry = edited.geometry[index]
  if (geometry.type !== 'line') throw new TypeError(`Sketch geometry '${geometryId}' is not a line.`)
  if (edited.constraints.some((constraint) => constraintReferencesGeometry(constraint, geometryId))) throw new Error(`Cannot split constrained sketch line '${geometryId}' without an explicit constraint migration.`)
  const dx = geometry.end.x - geometry.start.x
  const dy = geometry.end.y - geometry.start.y
  const lengthSquared = dx * dx + dy * dy
  if (lengthSquared <= tolerance * tolerance) throw new RangeError(`Sketch line '${geometryId}' is degenerate.`)
  const parameter = ((splitPoint.x - geometry.start.x) * dx + (splitPoint.y - geometry.start.y) * dy) / lengthSquared
  const projected = { x: geometry.start.x + parameter * dx, y: geometry.start.y + parameter * dy }
  if (distance(projected, splitPoint) > tolerance || parameter <= tolerance || parameter >= 1 - tolerance) throw new RangeError('Split point must lie strictly inside the sketch line.')
  const originalEnd = { ...geometry.end }
  geometry.end = projected
  edited.geometry.splice(index + 1, 0, { id: newGeometryId, type: 'line', start: { ...projected }, end: originalEnd, construction: geometry.construction })
  edited.constraints.push({ id: `split-coincident-${geometryId}-${newGeometryId}`, type: 'coincident', first: { geometryId, point: 'end' }, second: { geometryId: newGeometryId, point: 'start' } })
  validateSketchSnapshot(edited)
  return edited
}

export const extendSketchLine = (sketch: SketchSnapshot, geometryId: string, endpoint: 'start' | 'end', target: SketchPoint, tolerance = 1e-7): SketchSnapshot => {
  assertFinitePoint(target, 'Sketch line extension target')
  if (!Number.isFinite(tolerance) || tolerance <= 0) throw new RangeError('Extension tolerance must be finite and greater than zero.')
  const edited = cloneSketch(sketch)
  const geometry = edited.geometry.find((candidate) => candidate.id === geometryId)
  if (!geometry) throw new RangeError(`Sketch geometry '${geometryId}' does not exist.`)
  if (geometry.type !== 'line') throw new TypeError(`Sketch geometry '${geometryId}' is not a line.`)
  if (edited.constraints.some((constraint) => constraintReferencesGeometry(constraint, geometryId))) throw new Error(`Cannot extend constrained sketch line '${geometryId}' without an explicit constraint migration.`)
  const dx = geometry.end.x - geometry.start.x
  const dy = geometry.end.y - geometry.start.y
  const lengthSquared = dx * dx + dy * dy
  if (lengthSquared <= tolerance * tolerance) throw new RangeError(`Sketch line '${geometryId}' is degenerate.`)
  const parameter = ((target.x - geometry.start.x) * dx + (target.y - geometry.start.y) * dy) / lengthSquared
  const projected = { x: geometry.start.x + parameter * dx, y: geometry.start.y + parameter * dy }
  const extendsEndpoint = endpoint === 'start' ? parameter < -tolerance : parameter > 1 + tolerance
  if (distance(projected, target) > tolerance || !extendsEndpoint) throw new RangeError(`Extension target must be collinear and beyond the line ${endpoint}.`)
  geometry[endpoint] = { ...target }
  validateSketchSnapshot(edited)
  return edited
}

export const trimSketchLine = (sketch: SketchSnapshot, geometryId: string, endpoint: 'start' | 'end', target: SketchPoint, tolerance = 1e-7): SketchSnapshot => {
  assertFinitePoint(target, 'Sketch line trim target')
  if (!Number.isFinite(tolerance) || tolerance <= 0) throw new RangeError('Trim tolerance must be finite and greater than zero.')
  const edited = cloneSketch(sketch)
  const geometry = edited.geometry.find((candidate) => candidate.id === geometryId)
  if (!geometry) throw new RangeError(`Sketch geometry '${geometryId}' does not exist.`)
  if (geometry.type !== 'line') throw new TypeError(`Sketch geometry '${geometryId}' is not a line.`)
  if (edited.constraints.some((constraint) => constraintReferencesGeometry(constraint, geometryId))) throw new Error(`Cannot trim constrained sketch line '${geometryId}' without an explicit constraint migration.`)
  const dx = geometry.end.x - geometry.start.x
  const dy = geometry.end.y - geometry.start.y
  const lengthSquared = dx * dx + dy * dy
  if (lengthSquared <= tolerance * tolerance) throw new RangeError(`Sketch line '${geometryId}' is degenerate.`)
  const parameter = ((target.x - geometry.start.x) * dx + (target.y - geometry.start.y) * dy) / lengthSquared
  const projected = { x: geometry.start.x + parameter * dx, y: geometry.start.y + parameter * dy }
  if (distance(projected, target) > tolerance || parameter <= tolerance || parameter >= 1 - tolerance) throw new RangeError('Trim target must lie strictly inside the sketch line.')
  geometry[endpoint] = { ...target }
  const cuttingGeometry = edited.geometry.find((candidate) => candidate.id !== geometryId && candidate.type === 'line' && (() => {
    const cuttingDx = candidate.end.x - candidate.start.x
    const cuttingDy = candidate.end.y - candidate.start.y
    const cuttingLengthSquared = cuttingDx * cuttingDx + cuttingDy * cuttingDy
    if (cuttingLengthSquared <= tolerance * tolerance) return false
    const cuttingParameter = ((target.x - candidate.start.x) * cuttingDx + (target.y - candidate.start.y) * cuttingDy) / cuttingLengthSquared
    const cuttingProjection = { x: candidate.start.x + cuttingParameter * cuttingDx, y: candidate.start.y + cuttingParameter * cuttingDy }
    return cuttingParameter >= -tolerance && cuttingParameter <= 1 + tolerance && distance(cuttingProjection, target) <= tolerance
  })())
  if (cuttingGeometry) edited.constraints.push({ id: `trim-point-on-object-${geometryId}-${endpoint}-${cuttingGeometry.id}`, type: 'pointOnObject', point: { geometryId, point: endpoint }, geometryId: cuttingGeometry.id })
  validateSketchSnapshot(edited)
  return edited
}

export const deleteSketchGeometry = (sketch: SketchSnapshot, geometryId: string): SketchSnapshot => {
  const edited = cloneSketch(sketch)
  const index = edited.geometry.findIndex((geometry) => geometry.id === geometryId)
  if (index < 0) throw new RangeError(`Sketch geometry '${geometryId}' does not exist.`)
  if (edited.constraints.some((constraint) => constraintReferencesGeometry(constraint, geometryId))) throw new Error(`Cannot delete constrained sketch geometry '${geometryId}'.`)
  if (edited.externalGeometry.some((external) => external.projection.id === geometryId || external.source.persistentId === geometryId)) throw new Error(`Cannot delete sketch geometry '${geometryId}' referenced by external geometry.`)
  edited.geometry.splice(index, 1)
  validateSketchSnapshot(edited)
  return edited
}

export const setSketchConstruction = (sketch: SketchSnapshot, geometryId: string, construction: boolean): SketchSnapshot => {
  const edited = cloneSketch(sketch)
  const geometry = edited.geometry.find((candidate) => candidate.id === geometryId)
  if (!geometry) throw new RangeError(`Sketch geometry '${geometryId}' does not exist.`)
  geometry.construction = construction
  validateSketchSnapshot(edited)
  return edited
}

export const suggestSketchAutoConstraints = (sketch: SketchSnapshot, geometryId: string, tolerance = 1e-3): SketchAutoConstraintSuggestion[] => {
  validateSketchSnapshot(sketch)
  if (!Number.isFinite(tolerance) || tolerance <= 0) throw new RangeError('Autoconstraint tolerance must be finite and greater than zero.')
  const geometry = sketch.geometry.find((candidate) => candidate.id === geometryId)
  if (!geometry) throw new RangeError(`Sketch geometry '${geometryId}' does not exist.`)
  const suggestions: SketchAutoConstraintSuggestion[] = []
  const existing = new Set(sketch.constraints.map(constraintSignature))
  const add = (reason: SketchAutoConstraintSuggestion['reason'], constraint: SketchConstraint) => {
    if (!existing.has(constraintSignature(constraint))) suggestions.push({ id: constraint.id, constraint, reason })
  }
  if (geometry.type === 'line') {
    if (Math.abs(geometry.end.y - geometry.start.y) <= tolerance) add('horizontal', { id: `auto-horizontal-${geometry.id}`, type: 'horizontal', geometryId: geometry.id })
    if (Math.abs(geometry.end.x - geometry.start.x) <= tolerance) add('vertical', { id: `auto-vertical-${geometry.id}`, type: 'vertical', geometryId: geometry.id })
    for (const point of ['start', 'end'] as const) {
      const current = geometry[point]
      for (const other of sketch.geometry) {
        if (other.id === geometry.id || other.type !== 'line') continue
        for (const otherPoint of ['start', 'end'] as const) {
          if (distance(current, other[otherPoint]) > tolerance) continue
          const ids = [`${geometry.id}.${point}`, `${other.id}.${otherPoint}`].sort()
          add('coincident', { id: `auto-coincident-${ids.join('-')}`, type: 'coincident', first: { geometryId: geometry.id, point }, second: { geometryId: other.id, point: otherPoint } })
        }
      }
    }
  }
  return suggestions.sort((left, right) => left.id.localeCompare(right.id))
}

export const applySketchAutoConstraints = (sketch: SketchSnapshot, suggestions: readonly SketchAutoConstraintSuggestion[]): SketchSnapshot => {
  const edited = cloneSketch(sketch)
  const ids = new Set(edited.constraints.map((constraint) => constraint.id))
  const signatures = new Set(edited.constraints.map(constraintSignature))
  for (const suggestion of suggestions) {
    if (!suggestion.id.trim() || suggestion.id !== suggestion.constraint.id) throw new TypeError('Autoconstraint suggestion ID must match its constraint ID.')
    if (ids.has(suggestion.id)) throw new RangeError(`Sketch constraint '${suggestion.id}' already exists.`)
    const signature = constraintSignature(suggestion.constraint)
    if (signatures.has(signature)) throw new RangeError(`Sketch already contains the suggested ${suggestion.reason} constraint.`)
    ids.add(suggestion.id)
    signatures.add(signature)
    edited.constraints.push(cloneSketchConstraint(suggestion.constraint))
  }
  validateSketchSnapshot(edited)
  return edited
}

export type SketchEditorEvent =
  | { type: 'drag'; point: SketchPointRef; target: SketchPoint; options?: SketchSolveOptions }
  | { type: 'split'; geometryId: string; point: SketchPoint; newGeometryId: string; tolerance?: number }
  | { type: 'extend' | 'trim'; geometryId: string; endpoint: 'start' | 'end'; target: SketchPoint; tolerance?: number }
  | { type: 'delete'; geometryId: string }
  | { type: 'construction'; geometryId: string; construction: boolean }
  | { type: 'autoconstraint'; geometryId: string; tolerance?: number; suggestionIds?: string[] }
  | { type: 'undo' | 'redo' }

export type SketchEditorReplayResult = { snapshot: SketchSnapshot; applied: number; undone: number; redone: number }

export const replaySketchEditorEvents = (initial: SketchSnapshot, events: readonly SketchEditorEvent[]): SketchEditorReplayResult => {
  const history: SketchSnapshot[] = [cloneSketch(initial)]
  let cursor = 0
  let applied = 0
  let undone = 0
  let redone = 0
  for (const event of events) {
    if (event.type === 'undo') { if (cursor > 0) { cursor -= 1; undone += 1 } continue }
    if (event.type === 'redo') { if (cursor < history.length - 1) { cursor += 1; redone += 1 } continue }
    const current = history[cursor]
    let next: SketchSnapshot
    if (event.type === 'drag') next = dragSketchPoint(current, event.point, event.target, event.options).snapshot
    else if (event.type === 'split') next = splitSketchLine(current, event.geometryId, event.point, event.newGeometryId, event.tolerance)
    else if (event.type === 'extend') next = extendSketchLine(current, event.geometryId, event.endpoint, event.target, event.tolerance)
    else if (event.type === 'trim') next = trimSketchLine(current, event.geometryId, event.endpoint, event.target, event.tolerance)
    else if (event.type === 'delete') next = deleteSketchGeometry(current, event.geometryId)
    else if (event.type === 'construction') next = setSketchConstruction(current, event.geometryId, event.construction)
    else if (event.type === 'autoconstraint') {
      const suggestions = suggestSketchAutoConstraints(current, event.geometryId, event.tolerance)
      next = applySketchAutoConstraints(current, event.suggestionIds ? suggestions.filter((suggestion) => event.suggestionIds?.includes(suggestion.id)) : suggestions)
    } else throw new TypeError(`Unsupported sketch editor event: ${(event as { type: string }).type}`)
    history.splice(cursor + 1)
    history.push(next)
    cursor += 1
    applied += 1
  }
  return { snapshot: cloneSketch(history[cursor]), applied, undone, redone }
}

export type SketchEditorTool = 'select' | 'drag' | 'split' | 'extend' | 'trim'
export type SketchEditorPointerTarget = { geometryId: string; point?: SketchPointRef['point']; endpoint?: 'start' | 'end'; newGeometryId?: string }
export type SketchEditorPointerInput = { position: SketchPoint; target?: SketchEditorPointerTarget }
export type SketchEditorKeyboardInput = { key: string; ctrlKey?: boolean; metaKey?: boolean; shiftKey?: boolean }

export class SketchEditorInteractionSession {
  private history: SketchSnapshot[]
  private cursor = 0
  private activePointer: SketchEditorPointerInput | null = null
  private selectedGeometryId: string | null = null
  private splitSequence = 0
  private appliedCount = 0
  private undoneCount = 0
  private redoneCount = 0
  private tool: SketchEditorTool = 'select'

  constructor(initial: SketchSnapshot) {
    this.history = [cloneSketch(initial)]
  }

  setTool(tool: SketchEditorTool) {
    this.tool = tool
    this.activePointer = null
  }

  getTool() { return this.tool }

  getSnapshot() { return cloneSketch(this.history[this.cursor]) }

  getState() {
    return {
      snapshot: this.getSnapshot(),
      tool: this.tool,
      pointerActive: this.activePointer !== null,
      selectedGeometryId: this.selectedGeometryId,
      applied: this.appliedCount,
      undone: this.undoneCount,
      redone: this.redoneCount,
    }
  }

  pointerDown(input: SketchEditorPointerInput) {
    assertFinitePoint(input.position, 'Sketch editor pointer position')
    if (input.target && !this.history[this.cursor].geometry.some((geometry) => geometry.id === input.target?.geometryId)) throw new RangeError(`Sketch geometry '${input.target.geometryId}' does not exist.`)
    this.activePointer = { position: { ...input.position }, target: input.target ? { ...input.target } : undefined }
    this.selectedGeometryId = input.target?.geometryId ?? null
  }

  pointerUp(input: SketchEditorPointerInput) {
    assertFinitePoint(input.position, 'Sketch editor pointer position')
    const started = this.activePointer
    this.activePointer = null
    if (!started?.target || this.tool === 'select') return false
    const target = started.target
    if (this.tool === 'drag') {
      if (!target.point) throw new TypeError('Drag pointer target requires a sketch point reference.')
      this.apply({ type: 'drag', point: { geometryId: target.geometryId, point: target.point }, target: input.position })
    } else if (this.tool === 'split') {
      this.apply({ type: 'split', geometryId: target.geometryId, point: input.position, newGeometryId: target.newGeometryId ?? `${target.geometryId}-split-${++this.splitSequence}` })
    } else if (this.tool === 'extend' || this.tool === 'trim') {
      const endpoint = target.endpoint ?? (target.point === 'start' || target.point === 'end' ? target.point : undefined)
      if (!endpoint) throw new TypeError(`${this.tool} pointer target requires a line endpoint.`)
      this.apply({ type: this.tool, geometryId: target.geometryId, endpoint, target: input.position })
    }
    return true
  }

  keyDown(input: SketchEditorKeyboardInput) {
    const key = input.key.toLowerCase()
    if ((input.ctrlKey || input.metaKey) && key === 'z') return input.shiftKey ? this.redo() : this.undo()
    if ((input.ctrlKey || input.metaKey) && key === 'y') return this.redo()
    if (key === 'escape') { this.activePointer = null; this.tool = 'select'; return true }
    if ((key === 'delete' || key === 'backspace') && this.selectedGeometryId) {
      this.apply({ type: 'delete', geometryId: this.selectedGeometryId })
      this.selectedGeometryId = null
      return true
    }
    return false
  }

  apply(event: Exclude<SketchEditorEvent, { type: 'undo' | 'redo' }>) {
    const result = replaySketchEditorEvents(this.history[this.cursor], [event])
    this.history.splice(this.cursor + 1)
    this.history.push(result.snapshot)
    this.cursor += 1
    this.appliedCount += 1
  }

  undo() {
    if (this.cursor === 0) return false
    this.cursor -= 1
    this.undoneCount += 1
    return true
  }

  redo() {
    if (this.cursor >= this.history.length - 1) return false
    this.cursor += 1
    this.redoneCount += 1
    return true
  }
}

const findGeometry = (geometry: SketchGeometry[], id: string, constraintId: string, diagnostics: SketchDiagnostic[]) => {
  const result = geometry.find((candidate) => candidate.id === id)
  if (!result) diagnostics.push({ code: 'UNKNOWN_GEOMETRY', constraintId, message: `Sketch geometry '${id}' does not exist.` })
  return result
}

const pointFor = (geometry: SketchGeometry, point: SketchPointRef['point'], constraintId: string, diagnostics: SketchDiagnostic[]): SketchPoint | null => {
  if (geometry.type === 'point' && point === 'position') return geometry.position
  if (geometry.type === 'line' && (point === 'start' || point === 'end')) return point === 'start' ? geometry.start : geometry.end
  if ((geometry.type === 'circle' || geometry.type === 'arc' || geometry.type === 'ellipse' || geometry.type === 'arcEllipse' || geometry.type === 'arcHyperbola' || geometry.type === 'arcParabola') && point === 'center') return geometry.center
  diagnostics.push({ code: 'UNKNOWN_POINT', constraintId, message: `Point '${point}' is not valid for ${geometry.type} '${geometry.id}'.` })
  return null
}

const pointKey = (ref: SketchPointRef) => `${ref.geometryId}.${ref.point}`
const distance = (left: SketchPoint, right: SketchPoint) => Math.hypot(left.x - right.x, left.y - right.y)
const lineLength = (geometry: SketchGeometry) => geometry.type === 'line' ? distance(geometry.start, geometry.end) : geometry.type === 'circle' || geometry.type === 'arc' ? geometry.radius : 0

const lineCircleTangentResidual = (line: SketchGeometry, circle: SketchGeometry) => {
  if (line.type !== 'line' || (circle.type !== 'circle' && circle.type !== 'arc')) return Infinity
  const dx = line.end.x - line.start.x
  const dy = line.end.y - line.start.y
  const length = Math.hypot(dx, dy)
  if (length === 0) return Infinity
  return Math.abs(((circle.center.x - line.start.x) * dy - (circle.center.y - line.start.y) * dx) / length) - circle.radius
}

const lineRelationResidual = (first: SketchGeometry, second: SketchGeometry, relation: 'parallel' | 'perpendicular') => {
  if (first.type !== 'line' || second.type !== 'line') return Infinity
  const firstDx = first.end.x - first.start.x
  const firstDy = first.end.y - first.start.y
  const secondDx = second.end.x - second.start.x
  const secondDy = second.end.y - second.start.y
  const scale = Math.hypot(firstDx, firstDy) * Math.hypot(secondDx, secondDy)
  if (scale === 0) return Infinity
  return relation === 'parallel' ? Math.abs(firstDx * secondDy - firstDy * secondDx) / scale : Math.abs(firstDx * secondDx + firstDy * secondDy) / scale
}

const pointOnObjectProjection = (point: SketchPoint, object: SketchGeometry): SketchPoint | null => {
  if (object.type === 'line') {
    const dx = object.end.x - object.start.x
    const dy = object.end.y - object.start.y
    const lengthSquared = dx * dx + dy * dy
    if (lengthSquared === 0) return null
    const parameter = ((point.x - object.start.x) * dx + (point.y - object.start.y) * dy) / lengthSquared
    return { x: object.start.x + parameter * dx, y: object.start.y + parameter * dy }
  }
  if (object.type === 'circle' || object.type === 'arc') {
    const dx = point.x - object.center.x
    const dy = point.y - object.center.y
    const length = Math.hypot(dx, dy)
    return length === 0 ? { x: object.center.x + object.radius, y: object.center.y } : { x: object.center.x + dx / length * object.radius, y: object.center.y + dy / length * object.radius }
  }
  return null
}

const isBlocked = (geometryId: string, blocked: Set<string>) => blocked.has(geometryId)

const adjustPoint = (geometry: SketchGeometry, point: SketchPointRef['point'], next: SketchPoint, blocked: Set<string>) => {
  if (isBlocked(geometry.id, blocked)) return
  if (geometry.type === 'point') { geometry.position = { ...next }; return }
  if (geometry.type === 'line') { if (point === 'start') geometry.start = { ...next }; else if (point === 'end') geometry.end = { ...next }; return }
  if (point === 'center' && (geometry.type === 'circle' || geometry.type === 'arc' || geometry.type === 'ellipse' || geometry.type === 'arcEllipse' || geometry.type === 'arcHyperbola' || geometry.type === 'arcParabola')) geometry.center = { ...next }
}

const validateConstraintValues = (constraint: SketchConstraint, diagnostics: SketchDiagnostic[]) => {
  if ('value' in constraint && (!Number.isFinite(constraint.value) || constraint.value < 0)) diagnostics.push({ code: 'INVALID_VALUE', constraintId: constraint.id, message: `Constraint '${constraint.id}' requires a finite non-negative value.` })
  if (constraint.type === 'weight' && constraint.value <= 0) diagnostics.push({ code: 'INVALID_VALUE', constraintId: constraint.id, message: `Weight constraint '${constraint.id}' requires a positive value.` })
  if ((constraint.type === 'weight' || constraint.type === 'internalAlignment') && (!Number.isSafeInteger(constraint.type === 'weight' ? constraint.controlPointIndex : constraint.internalGeometryIndex) || (constraint.type === 'weight' ? constraint.controlPointIndex : constraint.internalGeometryIndex) < 0)) diagnostics.push({ code: 'INVALID_VALUE', constraintId: constraint.id, message: `Constraint '${constraint.id}' requires a non-negative integer index.` })
}

const constraintSignature = (constraint: SketchConstraint) => {
  if (constraint.type === 'coincident' || constraint.type === 'distance' || constraint.type === 'distanceX' || constraint.type === 'distanceY') return `${constraint.type}:${pointKey(constraint.first)}:${pointKey(constraint.second)}:${'value' in constraint ? constraint.value : ''}`
  if (constraint.type === 'symmetric') return `${constraint.type}:${pointKey(constraint.first)}:${pointKey(constraint.second)}:${pointKey(constraint.center)}`
  if (constraint.type === 'equal' || constraint.type === 'tangent' || constraint.type === 'parallel' || constraint.type === 'perpendicular') return `${constraint.type}:${[constraint.firstGeometryId, constraint.secondGeometryId].sort().join(':')}`
  if (constraint.type === 'pointOnObject') return `${constraint.type}:${pointKey(constraint.point)}:${constraint.geometryId}`
  if (constraint.type === 'weight') return `${constraint.type}:${constraint.geometryId}:${constraint.controlPointIndex}:${constraint.value}`
  if (constraint.type === 'snellsLaw') return 'boundaryGeometryId' in constraint
    ? `${constraint.type}:${pointKey(constraint.first)}:${pointKey(constraint.second)}:${constraint.boundaryGeometryId}:${constraint.value}`
    : `${constraint.type}:${[constraint.firstGeometryId, constraint.secondGeometryId].sort().join(':')}:${constraint.value}`
  if (constraint.type === 'internalAlignment') return `${constraint.type}:${constraint.geometryId}:${constraint.internalGeometryIndex}:${constraint.alignmentType}`
  if (constraint.type === 'horizontal' || constraint.type === 'vertical' || constraint.type === 'block') return `${constraint.type}:${constraint.geometryId}`
  return `${constraint.type}:${'geometryId' in constraint ? constraint.geometryId : ''}:${'value' in constraint ? constraint.value : ''}`
}

const hasFatalDiagnostics = (diagnostics: SketchDiagnostic[]) => diagnostics.some((diagnostic) => diagnostic.code !== 'REDUNDANT_CONSTRAINT' && diagnostic.code !== 'REFERENCE_DIMENSION' && diagnostic.code !== 'CONSTRAINT_CONFLICT')

const residualFor = (constraint: SketchConstraint, geometry: SketchGeometry[], diagnostics: SketchDiagnostic[]): number => {
  if (constraint.type === 'horizontal' || constraint.type === 'vertical') {
    const candidate = findGeometry(geometry, constraint.geometryId, constraint.id, diagnostics)
    if (!candidate || candidate.type !== 'line') return Infinity
    return constraint.type === 'horizontal' ? Math.abs(candidate.start.y - candidate.end.y) : Math.abs(candidate.start.x - candidate.end.x)
  }
  if (constraint.type === 'radius') {
    const candidate = findGeometry(geometry, constraint.geometryId, constraint.id, diagnostics)
    return candidate && (candidate.type === 'circle' || candidate.type === 'arc') ? Math.abs(candidate.radius - constraint.value) : Infinity
  }
  if (constraint.type === 'diameter') {
    const candidate = findGeometry(geometry, constraint.geometryId, constraint.id, diagnostics)
    return candidate && (candidate.type === 'circle' || candidate.type === 'arc') ? Math.abs(candidate.radius * 2 - constraint.value) : Infinity
  }
  if (constraint.type === 'angle') {
    const candidate = findGeometry(geometry, constraint.geometryId, constraint.id, diagnostics)
    if (!candidate || candidate.type !== 'line') return Infinity
    return Math.abs(Math.atan2(candidate.end.y - candidate.start.y, candidate.end.x - candidate.start.x) - constraint.value)
  }
  if (constraint.type === 'equal') {
    const first = findGeometry(geometry, constraint.firstGeometryId, constraint.id, diagnostics)
    const second = findGeometry(geometry, constraint.secondGeometryId, constraint.id, diagnostics)
    return first && second ? Math.abs(lineLength(first) - lineLength(second)) : Infinity
  }
  if (constraint.type === 'parallel' || constraint.type === 'perpendicular') {
    const first = findGeometry(geometry, constraint.firstGeometryId, constraint.id, diagnostics)
    const second = findGeometry(geometry, constraint.secondGeometryId, constraint.id, diagnostics)
    return first && second ? lineRelationResidual(first, second, constraint.type) : Infinity
  }
  if (constraint.type === 'pointOnObject') {
    const pointGeometry = findGeometry(geometry, constraint.point.geometryId, constraint.id, diagnostics)
    const objectGeometry = findGeometry(geometry, constraint.geometryId, constraint.id, diagnostics)
    if (!pointGeometry || !objectGeometry) return Infinity
    const point = pointFor(pointGeometry, constraint.point.point, constraint.id, diagnostics)
    const projection = point ? pointOnObjectProjection(point, objectGeometry) : null
    return point && projection ? distance(point, projection) : Infinity
  }
  if (constraint.type === 'symmetric') {
    const firstGeometry = findGeometry(geometry, constraint.first.geometryId, constraint.id, diagnostics)
    const secondGeometry = findGeometry(geometry, constraint.second.geometryId, constraint.id, diagnostics)
    const centerGeometry = findGeometry(geometry, constraint.center.geometryId, constraint.id, diagnostics)
    if (!firstGeometry || !secondGeometry || !centerGeometry) return Infinity
    const first = pointFor(firstGeometry, constraint.first.point, constraint.id, diagnostics)
    const second = pointFor(secondGeometry, constraint.second.point, constraint.id, diagnostics)
    const center = pointFor(centerGeometry, constraint.center.point, constraint.id, diagnostics)
    return first && second && center ? Math.hypot((first.x + second.x) / 2 - center.x, (first.y + second.y) / 2 - center.y) : Infinity
  }
  if (constraint.type === 'tangent') {
    const first = findGeometry(geometry, constraint.firstGeometryId, constraint.id, diagnostics)
    const second = findGeometry(geometry, constraint.secondGeometryId, constraint.id, diagnostics)
    if (!first || !second) return Infinity
    if (first.type === 'line' && (second.type === 'circle' || second.type === 'arc')) return Math.abs(lineCircleTangentResidual(first, second))
    if (second.type === 'line' && (first.type === 'circle' || first.type === 'arc')) return Math.abs(lineCircleTangentResidual(second, first))
    if ((first.type === 'circle' || first.type === 'arc') && (second.type === 'circle' || second.type === 'arc')) return Math.abs(distance(first.center, second.center) - first.radius - second.radius)
    return Infinity
  }
  if (constraint.type === 'block') return 0
  if (constraint.type !== 'coincident' && constraint.type !== 'distance' && constraint.type !== 'distanceX' && constraint.type !== 'distanceY') return Infinity
  const firstGeometry = findGeometry(geometry, constraint.first.geometryId, constraint.id, diagnostics)
  const secondGeometry = findGeometry(geometry, constraint.second.geometryId, constraint.id, diagnostics)
  if (!firstGeometry || !secondGeometry) return Infinity
  const first = pointFor(firstGeometry, constraint.first.point, constraint.id, diagnostics)
  const second = pointFor(secondGeometry, constraint.second.point, constraint.id, diagnostics)
  if (!first || !second) return Infinity
  if (constraint.type === 'coincident') return distance(first, second)
  if (constraint.type === 'distance') return Math.abs(distance(first, second) - constraint.value)
  if (constraint.type === 'distanceX') return Math.abs(Math.abs(second.x - first.x) - constraint.value)
  return Math.abs(Math.abs(second.y - first.y) - constraint.value)
}

export const solveSketch = (input: SketchSnapshot, options: SketchSolveOptions = {}): SketchSolveResult => {
  const tolerance = options.tolerance ?? 1e-7
  const maxIterations = options.maxIterations ?? 64
  const snapshot = cloneSketch(input)
  const diagnostics: SketchDiagnostic[] = []
  for (const geometry of snapshot.geometry) {
    if (!['point', 'line', 'circle', 'arc'].includes(geometry.type)) diagnostics.push({ code: 'UNSUPPORTED_GEOMETRY', geometryId: geometry.id, message: `The typescript-basic solver does not solve ${geometry.type} geometry '${geometry.id}'.` })
  }
  for (const constraint of snapshot.constraints) if (constraint.type === 'weight' || constraint.type === 'snellsLaw' || constraint.type === 'internalAlignment') diagnostics.push({ code: 'UNSUPPORTED_CONSTRAINT', constraintId: constraint.id, message: `The typescript-basic solver does not solve ${constraint.type} constraint '${constraint.id}'.` })
  const geometryById = new Map(snapshot.geometry.map((geometry) => [geometry.id, geometry]))
  const blocked = new Set(snapshot.constraints.filter((constraint) => constraint.type === 'block').map((constraint) => constraint.geometryId))
  snapshot.constraints.forEach((constraint) => validateConstraintValues(constraint, diagnostics))
  const seenConstraints = new Set<string>()
  const dimensionalConstraints = new Map<string, { id: string; value: number }>()
  for (const constraint of snapshot.constraints.filter((candidate) => candidate.driving !== false)) {
    const signature = constraintSignature(constraint)
    if (seenConstraints.has(signature)) diagnostics.push({ code: 'REDUNDANT_CONSTRAINT', constraintId: constraint.id, message: `Constraint '${constraint.id}' duplicates an existing driving constraint.` })
    else seenConstraints.add(signature)
    if (constraint.type === 'distance' || constraint.type === 'distanceX' || constraint.type === 'distanceY' || constraint.type === 'radius' || constraint.type === 'diameter' || constraint.type === 'angle') {
      const key = constraint.type === 'radius' || constraint.type === 'diameter' || constraint.type === 'angle'
        ? `${constraint.type}:${constraint.geometryId}`
        : `${constraint.type}:${pointKey(constraint.first)}:${pointKey(constraint.second)}`
      const previous = dimensionalConstraints.get(key)
      if (previous && Number.isFinite(constraint.value) && Number.isFinite(previous.value)) {
        if (Math.abs(previous.value - constraint.value) <= tolerance) diagnostics.push({ code: 'REDUNDANT_CONSTRAINT', constraintId: constraint.id, message: `Dimensional constraint '${constraint.id}' duplicates '${previous.id}'.` })
        else diagnostics.push({ code: 'CONSTRAINT_CONFLICT', constraintId: constraint.id, message: `Dimensional constraint '${constraint.id}' conflicts with '${previous.id}'.` })
      } else if (Number.isFinite(constraint.value)) dimensionalConstraints.set(key, { id: constraint.id, value: constraint.value })
    }
  }
  for (const constraint of snapshot.constraints) {
    if (constraint.driving !== false || !('value' in constraint)) continue
    if (!['distance', 'distanceX', 'distanceY', 'radius', 'diameter', 'angle'].includes(constraint.type)) continue
    diagnostics.push({ code: 'REFERENCE_DIMENSION', constraintId: constraint.id, message: `Reference dimension '${constraint.id}' is read-only and does not affect the solve or degrees of freedom.` })
  }
  let residual = Infinity
  let iterations = 0
  for (; iterations < maxIterations && residual > tolerance && !hasFatalDiagnostics(diagnostics); iterations += 1) {
    for (const constraint of snapshot.constraints) {
      if (constraint.driving === false) continue
      if (constraint.type === 'block') continue
      if (constraint.type === 'horizontal' || constraint.type === 'vertical') {
        const candidate = geometryById.get(constraint.geometryId)
        if (!candidate || candidate.type !== 'line') { findGeometry(snapshot.geometry, constraint.geometryId, constraint.id, diagnostics); continue }
        if (!isBlocked(candidate.id, blocked)) {
          if (constraint.type === 'horizontal') candidate.end = { ...candidate.end, y: candidate.start.y }
          else candidate.end = { ...candidate.end, x: candidate.start.x }
        }
      } else if (constraint.type === 'radius') {
        const candidate = geometryById.get(constraint.geometryId)
        if (!candidate || (candidate.type !== 'circle' && candidate.type !== 'arc')) { findGeometry(snapshot.geometry, constraint.geometryId, constraint.id, diagnostics); continue }
        if (!isBlocked(candidate.id, blocked)) candidate.radius = constraint.value
      } else if (constraint.type === 'diameter') {
        const candidate = geometryById.get(constraint.geometryId)
        if (!candidate || (candidate.type !== 'circle' && candidate.type !== 'arc')) { findGeometry(snapshot.geometry, constraint.geometryId, constraint.id, diagnostics); continue }
        if (!isBlocked(candidate.id, blocked)) candidate.radius = constraint.value / 2
      } else if (constraint.type === 'parallel' || constraint.type === 'perpendicular') {
        const first = geometryById.get(constraint.firstGeometryId)
        const second = geometryById.get(constraint.secondGeometryId)
        if (!first || !second) { findGeometry(snapshot.geometry, !first ? constraint.firstGeometryId : constraint.secondGeometryId, constraint.id, diagnostics); continue }
        if (first.type !== 'line' || second.type !== 'line') continue
        if (!isBlocked(second.id, blocked)) {
          const length = lineLength(second)
          const angle = Math.atan2(first.end.y - first.start.y, first.end.x - first.start.x) + (constraint.type === 'perpendicular' ? Math.PI / 2 : 0)
          second.end = { x: second.start.x + Math.cos(angle) * length, y: second.start.y + Math.sin(angle) * length }
        }
      } else if (constraint.type === 'pointOnObject') {
        const pointGeometry = geometryById.get(constraint.point.geometryId)
        const objectGeometry = geometryById.get(constraint.geometryId)
        if (!pointGeometry || !objectGeometry) { findGeometry(snapshot.geometry, !pointGeometry ? constraint.point.geometryId : constraint.geometryId, constraint.id, diagnostics); continue }
        const point = pointFor(pointGeometry, constraint.point.point, constraint.id, diagnostics)
        const projection = point ? pointOnObjectProjection(point, objectGeometry) : null
        if (projection && !isBlocked(pointGeometry.id, blocked)) adjustPoint(pointGeometry, constraint.point.point, projection, blocked)
      } else if (constraint.type === 'coincident' || constraint.type === 'distance' || constraint.type === 'distanceX' || constraint.type === 'distanceY') {
        const firstGeometry = geometryById.get(constraint.first.geometryId)
        const secondGeometry = geometryById.get(constraint.second.geometryId)
        if (!firstGeometry || !secondGeometry) { findGeometry(snapshot.geometry, !firstGeometry ? constraint.first.geometryId : constraint.second.geometryId, constraint.id, diagnostics); continue }
        const first = pointFor(firstGeometry, constraint.first.point, constraint.id, diagnostics)
        const second = pointFor(secondGeometry, constraint.second.point, constraint.id, diagnostics)
        if (!first || !second) continue
        const firstBlocked = isBlocked(firstGeometry.id, blocked)
        const secondBlocked = isBlocked(secondGeometry.id, blocked)
        if (constraint.type === 'coincident') {
          if (!firstBlocked && !secondBlocked) { const midpoint = { x: (first.x + second.x) / 2, y: (first.y + second.y) / 2 }; adjustPoint(firstGeometry, constraint.first.point, midpoint, blocked); adjustPoint(secondGeometry, constraint.second.point, midpoint, blocked) }
          else if (!firstBlocked) adjustPoint(firstGeometry, constraint.first.point, second, blocked)
          else if (!secondBlocked) adjustPoint(secondGeometry, constraint.second.point, first, blocked)
        } else {
          const dx = second.x - first.x
          const dy = second.y - first.y
          const current = constraint.type === 'distance' ? Math.hypot(dx, dy) : constraint.type === 'distanceX' ? Math.abs(dx) : Math.abs(dy)
          const delta = constraint.value - current
          if (Math.abs(delta) > tolerance && !secondBlocked) {
            if (constraint.type === 'distance') {
              const length = Math.hypot(dx, dy) || 1
              adjustPoint(secondGeometry, constraint.second.point, { x: second.x + dx / length * delta, y: second.y + dy / length * delta }, blocked)
            } else if (constraint.type === 'distanceX') adjustPoint(secondGeometry, constraint.second.point, { x: first.x + (dx < 0 ? -constraint.value : constraint.value), y: second.y }, blocked)
            else adjustPoint(secondGeometry, constraint.second.point, { x: second.x, y: first.y + (dy < 0 ? -constraint.value : constraint.value) }, blocked)
          }
        }
      } else if (constraint.type === 'symmetric') {
        const firstGeometry = geometryById.get(constraint.first.geometryId)
        const secondGeometry = geometryById.get(constraint.second.geometryId)
        const centerGeometry = geometryById.get(constraint.center.geometryId)
        if (!firstGeometry || !secondGeometry || !centerGeometry) { findGeometry(snapshot.geometry, !firstGeometry ? constraint.first.geometryId : !secondGeometry ? constraint.second.geometryId : constraint.center.geometryId, constraint.id, diagnostics); continue }
        const first = pointFor(firstGeometry, constraint.first.point, constraint.id, diagnostics)
        const second = pointFor(secondGeometry, constraint.second.point, constraint.id, diagnostics)
        const center = pointFor(centerGeometry, constraint.center.point, constraint.id, diagnostics)
        if (!first || !second || !center) continue
        const reflected = { x: 2 * center.x - first.x, y: 2 * center.y - first.y }
        if (!isBlocked(secondGeometry.id, blocked)) adjustPoint(secondGeometry, constraint.second.point, reflected, blocked)
      } else if (constraint.type === 'tangent') {
        const first = geometryById.get(constraint.firstGeometryId)
        const second = geometryById.get(constraint.secondGeometryId)
        if (!first || !second) { findGeometry(snapshot.geometry, !first ? constraint.firstGeometryId : constraint.secondGeometryId, constraint.id, diagnostics); continue }
        const line = first.type === 'line' ? first : second.type === 'line' ? second : null
        const circle = first.type === 'line' ? second : second.type === 'line' ? first : null
        if (line && circle && (circle.type === 'circle' || circle.type === 'arc') && !isBlocked(circle.id, blocked)) {
          const dx = line.end.x - line.start.x
          const dy = line.end.y - line.start.y
          const length = Math.hypot(dx, dy) || 1
          const signed = ((circle.center.x - line.start.x) * dy - (circle.center.y - line.start.y) * dx) / length
          const delta = circle.radius - Math.abs(signed)
          const sign = signed < 0 ? -1 : 1
          circle.center = { x: circle.center.x + dy / length * delta * sign, y: circle.center.y - dx / length * delta * sign }
        } else if (first && second && (first.type === 'circle' || first.type === 'arc') && (second.type === 'circle' || second.type === 'arc') && !isBlocked(second.id, blocked)) {
          const dx = second.center.x - first.center.x
          const dy = second.center.y - first.center.y
          const length = Math.hypot(dx, dy) || 1
          const target = first.radius + second.radius
          second.center = { x: first.center.x + dx / length * target, y: first.center.y + dy / length * target }
        }
      } else if (constraint.type === 'equal') {
        const first = geometryById.get(constraint.firstGeometryId)
        const second = geometryById.get(constraint.secondGeometryId)
        if (!first || !second) { findGeometry(snapshot.geometry, !first ? constraint.firstGeometryId : constraint.secondGeometryId, constraint.id, diagnostics); continue }
        if (!isBlocked(second.id, blocked)) {
          if ((second.type === 'circle' || second.type === 'arc') && (first.type === 'circle' || first.type === 'arc')) second.radius = first.radius
          else if (second.type === 'line' && first.type === 'line') { const current = lineLength(second) || 1; const target = lineLength(first); const scale = target / current; second.end = { x: second.start.x + (second.end.x - second.start.x) * scale, y: second.start.y + (second.end.y - second.start.y) * scale } }
        }
      } else if (constraint.type === 'angle') {
        const candidate = geometryById.get(constraint.geometryId)
        if (!candidate || candidate.type !== 'line') { findGeometry(snapshot.geometry, constraint.geometryId, constraint.id, diagnostics); continue }
        if (!isBlocked(candidate.id, blocked)) { const length = lineLength(candidate); candidate.end = { x: candidate.start.x + Math.cos(constraint.value) * length, y: candidate.start.y + Math.sin(constraint.value) * length } }
      }
    }
    residual = Math.max(0, ...snapshot.constraints.filter((constraint) => constraint.driving !== false).map((constraint) => residualFor(constraint, snapshot.geometry, diagnostics)))
  }
  const variableCountForGeometry = (geometry: SketchGeometry) => {
    if (geometry.type === 'point') return 2
    if (geometry.type === 'line') return 4
    if (geometry.type === 'circle') return 3
    if (geometry.type === 'arc' || geometry.type === 'ellipse') return 5
    if (geometry.type === 'arcEllipse' || geometry.type === 'arcHyperbola') return 7
    if (geometry.type === 'arcParabola') return 6
    return geometry.controlPoints.length * 2 + (geometry.weights?.length ?? 0)
  }
  const variableCount = snapshot.geometry.reduce((count, geometry) => count + variableCountForGeometry(geometry), 0)
  const rankConstraints = new Set(snapshot.constraints.filter((constraint) => constraint.driving !== false && constraint.type !== 'block').map(constraintSignature))
  const blockedVariableCount = [...blocked].reduce((count, geometryId) => {
    const geometry = geometryById.get(geometryId)
    return geometry ? count + variableCountForGeometry(geometry) : count
  }, 0)
  const rank = Math.min(variableCount, rankConstraints.size + blockedVariableCount)
  const degreesOfFreedom = Math.max(0, variableCount - rank)
  const hasConstraintConflict = diagnostics.some((diagnostic) => diagnostic.code === 'CONSTRAINT_CONFLICT')
  const status: SketchSolverStatus = hasFatalDiagnostics(diagnostics) ? 'invalid' : hasConstraintConflict || residual > tolerance ? 'conflicting' : degreesOfFreedom === 0 ? 'solved' : 'under-constrained'
  if (status === 'conflicting') {
    for (const constraint of snapshot.constraints) {
      if (constraint.driving === false) continue
      const constraintResidual = residualFor(constraint, snapshot.geometry, diagnostics)
      if (Number.isFinite(constraintResidual) && constraintResidual > tolerance && !diagnostics.some((diagnostic) => diagnostic.code === 'CONSTRAINT_CONFLICT' && diagnostic.constraintId === constraint.id)) diagnostics.push({ code: 'CONSTRAINT_CONFLICT', constraintId: constraint.id, message: `Constraint '${constraint.id}' residual ${constraintResidual} exceeded tolerance ${tolerance}.` })
    }
    diagnostics.push({ code: 'SOLVER_NOT_CONVERGED', message: `Sketch solver residual ${residual} exceeded tolerance ${tolerance}.` })
  }
  snapshot.solver = { status, degreesOfFreedom, residual, iterations, diagnostics }
  return { snapshot, status, degreesOfFreedom, residual, iterations, diagnostics }
}

export interface SketchSolverAdapter {
  solve(snapshot: SketchSnapshot, options?: SketchSolveOptions): Promise<SketchSolveResult>
}

export class BasicSketchSolverAdapter implements SketchSolverAdapter {
  solve(snapshot: SketchSnapshot, options?: SketchSolveOptions) { return Promise.resolve(solveSketch(snapshot, options)) }
}
