export type DraftPoint = { x: number; y: number }
export type DraftVector3 = { x: number; y: number; z: number }
export type DraftWorkingPlane = { origin: DraftVector3; xAxis: DraftVector3; yAxis: DraftVector3 }
export type DraftLayer = { id: string; label: string; visible: boolean; color: string }
type DraftObjectBase = { id: string; label: string; layerId: string }
export type DraftLine = DraftObjectBase & { kind: 'line'; start: DraftPoint; end: DraftPoint }
export type DraftWire = DraftObjectBase & { kind: 'wire'; points: DraftPoint[]; closed: boolean }
export type DraftCircle = DraftObjectBase & { kind: 'circle'; center: DraftPoint; radius: number }
export type DraftClone = DraftObjectBase & { kind: 'clone'; sourceId: string; translation: DraftPoint; rotation: number; scale: number }
export type DraftObject = DraftLine | DraftWire | DraftCircle | DraftClone
export type DraftSnapshot = { id: string; label: string; workingPlane: DraftWorkingPlane; grid: { spacing: number; enabled: boolean }; layers: DraftLayer[]; objects: DraftObject[]; version: number }

export type DraftApi = {
  snapshot(): DraftSnapshot
  setWorkingPlane(plane: DraftWorkingPlane): DraftSnapshot
  setGrid(spacing: number, enabled?: boolean): DraftSnapshot
  snap(point: DraftPoint): DraftPoint
  mapToWorld(point: DraftPoint): DraftVector3
  addLayer(layer: DraftLayer): DraftLayer
  createLine(id: string, start: DraftPoint, end: DraftPoint, layerId?: string): DraftLine
  createWire(id: string, points: DraftPoint[], closed?: boolean, layerId?: string): DraftWire
  createCircle(id: string, center: DraftPoint, radius: number, layerId?: string): DraftCircle
  move(id: string, delta: DraftPoint): DraftObject
  rotate(id: string, angle: number, center?: DraftPoint): DraftObject
  scale(id: string, factor: number, center?: DraftPoint): DraftObject
  offsetLine(id: string, distance: number, outputId: string): DraftLine
  trimLine(id: string, startParameter: number, endParameter: number): DraftLine
  clone(sourceId: string, outputId: string, translation?: DraftPoint): DraftClone
  array(sourceId: string, options: { columns: number; rows: number; columnSpacing: number; rowSpacing: number; idPrefix: string }): DraftClone[]
  assignLayer(id: string, layerId: string): DraftObject
}

const finitePoint = (point: DraftPoint, label: string) => { if (!Number.isFinite(point.x) || !Number.isFinite(point.y)) throw new RangeError(`${label} must be finite.`) }
const finiteVector = (vector: DraftVector3, label: string) => { if (!Number.isFinite(vector.x) || !Number.isFinite(vector.y) || !Number.isFinite(vector.z)) throw new RangeError(`${label} must be finite.`) }
const length3 = (vector: DraftVector3) => Math.hypot(vector.x, vector.y, vector.z)
const dot3 = (left: DraftVector3, right: DraftVector3) => left.x * right.x + left.y * right.y + left.z * right.z
const clonePoint = (point: DraftPoint): DraftPoint => ({ ...point })
const cloneVector = (vector: DraftVector3): DraftVector3 => ({ ...vector })
const clonePlane = (plane: DraftWorkingPlane): DraftWorkingPlane => ({ origin: cloneVector(plane.origin), xAxis: cloneVector(plane.xAxis), yAxis: cloneVector(plane.yAxis) })
const cloneObject = (object: DraftObject): DraftObject => object.kind === 'line' ? { ...object, start: clonePoint(object.start), end: clonePoint(object.end) } : object.kind === 'wire' ? { ...object, points: object.points.map(clonePoint) } : object.kind === 'circle' ? { ...object, center: clonePoint(object.center) } : { ...object, translation: clonePoint(object.translation) }

const validatePlane = (plane: DraftWorkingPlane) => {
  finiteVector(plane.origin, 'Working plane origin'); finiteVector(plane.xAxis, 'Working plane X axis'); finiteVector(plane.yAxis, 'Working plane Y axis')
  const xLength = length3(plane.xAxis); const yLength = length3(plane.yAxis)
  if (Math.abs(xLength - 1) > 1e-9 || Math.abs(yLength - 1) > 1e-9) throw new RangeError('Working plane axes must be normalized.')
  if (Math.abs(dot3(plane.xAxis, plane.yAxis)) > 1e-9) throw new RangeError('Working plane axes must be orthogonal.')
}

const transformPoint = (point: DraftPoint, center: DraftPoint, angle: number, factor = 1): DraftPoint => {
  const radians = angle * Math.PI / 180
  const x = (point.x - center.x) * factor
  const y = (point.y - center.y) * factor
  return { x: center.x + x * Math.cos(radians) - y * Math.sin(radians), y: center.y + x * Math.sin(radians) + y * Math.cos(radians) }
}

export const createDraftDocument = (id = 'draft', label = 'Draft'): DraftApi => {
  let workingPlane: DraftWorkingPlane = { origin: { x: 0, y: 0, z: 0 }, xAxis: { x: 1, y: 0, z: 0 }, yAxis: { x: 0, y: 1, z: 0 } }
  let grid = { spacing: 1, enabled: true }
  let version = 0
  const layers = new Map<string, DraftLayer>([['default', { id: 'default', label: 'Default', visible: true, color: '#202428' }]])
  const objects = new Map<string, DraftObject>()
  const requireLayer = (layerId: string) => { if (!layers.has(layerId)) throw new RangeError(`Draft layer does not exist: ${layerId}`) }
  const requireObject = (objectId: string) => { const object = objects.get(objectId); if (!object) throw new RangeError(`Draft object does not exist: ${objectId}`); return object }
  const putObject = <T extends DraftObject>(object: T): T => { if (objects.has(object.id)) throw new RangeError(`Draft object already exists: ${object.id}`); objects.set(object.id, cloneObject(object)); version += 1; return cloneObject(object) as T }
  const replaceObject = <T extends DraftObject>(object: T): T => { objects.set(object.id, cloneObject(object)); version += 1; return cloneObject(object) as T }
  const snapshot = (): DraftSnapshot => ({ id, label, workingPlane: clonePlane(workingPlane), grid: { ...grid }, layers: [...layers.values()].map((layer) => ({ ...layer })), objects: [...objects.values()].map(cloneObject), version })
  const setWorkingPlane = (plane: DraftWorkingPlane) => { validatePlane(plane); workingPlane = clonePlane(plane); version += 1; return snapshot() }
  const setGrid = (spacing: number, enabled = true) => { if (!Number.isFinite(spacing) || spacing <= 0) throw new RangeError('Draft grid spacing must be positive and finite.'); grid = { spacing, enabled }; version += 1; return snapshot() }
  const snap = (point: DraftPoint) => { finitePoint(point, 'Draft snap point'); return grid.enabled ? { x: Math.round(point.x / grid.spacing) * grid.spacing, y: Math.round(point.y / grid.spacing) * grid.spacing } : clonePoint(point) }
  const mapToWorld = (point: DraftPoint) => { finitePoint(point, 'Draft plane point'); return { x: workingPlane.origin.x + workingPlane.xAxis.x * point.x + workingPlane.yAxis.x * point.y, y: workingPlane.origin.y + workingPlane.xAxis.y * point.x + workingPlane.yAxis.y * point.y, z: workingPlane.origin.z + workingPlane.xAxis.z * point.x + workingPlane.yAxis.z * point.y } }
  const addLayer = (layer: DraftLayer) => { if (!layer.id || !layer.label || !/^#[0-9a-f]{6}$/i.test(layer.color)) throw new RangeError('Draft layer requires an id, label and six-digit color.'); if (layers.has(layer.id)) throw new RangeError(`Draft layer already exists: ${layer.id}`); layers.set(layer.id, { ...layer }); version += 1; return { ...layer } }
  const createLine = (objectId: string, start: DraftPoint, end: DraftPoint, layerId = 'default') => { requireLayer(layerId); finitePoint(start, 'Draft line start'); finitePoint(end, 'Draft line end'); if (Math.hypot(end.x - start.x, end.y - start.y) <= 1e-9) throw new RangeError('Draft line must have non-zero length.'); return putObject({ id: objectId, label: objectId, layerId, kind: 'line', start: clonePoint(start), end: clonePoint(end) }) }
  const createWire = (objectId: string, points: DraftPoint[], closed = false, layerId = 'default') => { requireLayer(layerId); if (points.length < (closed ? 3 : 2)) throw new RangeError('Draft wire has too few points.'); points.forEach((point) => finitePoint(point, 'Draft wire point')); return putObject({ id: objectId, label: objectId, layerId, kind: 'wire', points: points.map(clonePoint), closed }) }
  const createCircle = (objectId: string, center: DraftPoint, radius: number, layerId = 'default') => { requireLayer(layerId); finitePoint(center, 'Draft circle center'); if (!Number.isFinite(radius) || radius <= 0) throw new RangeError('Draft circle radius must be positive and finite.'); return putObject({ id: objectId, label: objectId, layerId, kind: 'circle', center: clonePoint(center), radius }) }
  const move = (objectId: string, delta: DraftPoint) => { finitePoint(delta, 'Draft move delta'); const object = requireObject(objectId); if (object.kind === 'clone') return replaceObject({ ...object, translation: { x: object.translation.x + delta.x, y: object.translation.y + delta.y } }); if (object.kind === 'line') return replaceObject({ ...object, start: { x: object.start.x + delta.x, y: object.start.y + delta.y }, end: { x: object.end.x + delta.x, y: object.end.y + delta.y } }); if (object.kind === 'wire') return replaceObject({ ...object, points: object.points.map((point) => ({ x: point.x + delta.x, y: point.y + delta.y })) }); return replaceObject({ ...object, center: { x: object.center.x + delta.x, y: object.center.y + delta.y } }) }
  const rotate = (objectId: string, angle: number, center: DraftPoint = { x: 0, y: 0 }) => { if (!Number.isFinite(angle)) throw new RangeError('Draft rotation must be finite.'); finitePoint(center, 'Draft rotation center'); const object = requireObject(objectId); if (object.kind === 'clone') return replaceObject({ ...object, rotation: object.rotation + angle }); if (object.kind === 'line') return replaceObject({ ...object, start: transformPoint(object.start, center, angle), end: transformPoint(object.end, center, angle) }); if (object.kind === 'wire') return replaceObject({ ...object, points: object.points.map((point) => transformPoint(point, center, angle)) }); return replaceObject({ ...object, center: transformPoint(object.center, center, angle) }) }
  const scale = (objectId: string, factor: number, center: DraftPoint = { x: 0, y: 0 }) => { if (!Number.isFinite(factor) || factor <= 0) throw new RangeError('Draft scale factor must be positive and finite.'); finitePoint(center, 'Draft scale center'); const object = requireObject(objectId); if (object.kind === 'clone') return replaceObject({ ...object, scale: object.scale * factor }); if (object.kind === 'line') return replaceObject({ ...object, start: transformPoint(object.start, center, 0, factor), end: transformPoint(object.end, center, 0, factor) }); if (object.kind === 'wire') return replaceObject({ ...object, points: object.points.map((point) => transformPoint(point, center, 0, factor)) }); return replaceObject({ ...object, center: transformPoint(object.center, center, 0, factor), radius: object.radius * factor }) }
  const offsetLine = (objectId: string, distance: number, outputId: string) => { const object = requireObject(objectId); if (object.kind !== 'line') throw new TypeError('Draft offset currently supports line objects only.'); if (!Number.isFinite(distance) || distance === 0) throw new RangeError('Draft offset distance must be finite and non-zero.'); const dx = object.end.x - object.start.x; const dy = object.end.y - object.start.y; const length = Math.hypot(dx, dy); const offset = { x: -dy / length * distance, y: dx / length * distance }; return createLine(outputId, { x: object.start.x + offset.x, y: object.start.y + offset.y }, { x: object.end.x + offset.x, y: object.end.y + offset.y }, object.layerId) }
  const trimLine = (objectId: string, startParameter: number, endParameter: number) => {
    const object = requireObject(objectId)
    if (object.kind !== 'line') throw new TypeError('Draft trim currently supports line objects only.')
    if (!Number.isFinite(startParameter) || !Number.isFinite(endParameter) || startParameter < 0 || endParameter > 1 || startParameter >= endParameter) throw new RangeError('Draft trim parameters must satisfy 0 <= start < end <= 1.')
    const pointAt = (parameter: number): DraftPoint => ({ x: object.start.x + (object.end.x - object.start.x) * parameter, y: object.start.y + (object.end.y - object.start.y) * parameter })
    return replaceObject({ ...object, start: pointAt(startParameter), end: pointAt(endParameter) })
  }
  const clone = (sourceId: string, outputId: string, translation: DraftPoint = { x: 0, y: 0 }) => { requireObject(sourceId); finitePoint(translation, 'Draft clone translation'); return putObject({ id: outputId, label: outputId, layerId: requireObject(sourceId).layerId, kind: 'clone', sourceId, translation: clonePoint(translation), rotation: 0, scale: 1 }) }
  const array = (sourceId: string, options: { columns: number; rows: number; columnSpacing: number; rowSpacing: number; idPrefix: string }) => { requireObject(sourceId); if (!Number.isInteger(options.columns) || !Number.isInteger(options.rows) || options.columns < 1 || options.rows < 1 || options.columns * options.rows > 1000) throw new RangeError('Draft array dimensions must create between 1 and 1000 instances.'); if (![options.columnSpacing, options.rowSpacing].every(Number.isFinite) || !options.idPrefix) throw new RangeError('Draft array spacing and prefix are invalid.'); const result: DraftClone[] = []; for (let row = 0; row < options.rows; row += 1) for (let column = 0; column < options.columns; column += 1) result.push(clone(sourceId, `${options.idPrefix}-${row + 1}-${column + 1}`, { x: column * options.columnSpacing, y: row * options.rowSpacing })); return result }
  const assignLayer = (objectId: string, layerId: string) => { requireLayer(layerId); return replaceObject({ ...requireObject(objectId), layerId } as DraftObject) }
  return { snapshot, setWorkingPlane, setGrid, snap, mapToWorld, addLayer, createLine, createWire, createCircle, move, rotate, scale, offsetLine, trimLine, clone, array, assignLayer }
}
