import type { TopoRefValue } from './types'

export type TechDrawPoint = { x: number; y: number; z: number }
export type TechDrawSourcePoint = { id: string; ref: TopoRefValue; point: TechDrawPoint }
export type TechDrawSourceEdge = { id: string; start: string; end: string }
export type TechDrawSource = { id: string; revision: number; points: TechDrawSourcePoint[]; edges: TechDrawSourceEdge[] }
export type TechDrawViewKind = 'projection' | 'section'
export type TechDrawView = { id: string; sourceId: string; kind: TechDrawViewKind; direction: TechDrawPoint; position: { x: number; y: number }; scale: number; visible: boolean; sourceRevision: number; projected: Record<string, { x: number; y: number }>; unresolved: string[]; section?: { normal: TechDrawPoint; offset: number } }
export type TechDrawDimension = { id: string; sourceId: string; first: TopoRefValue; second: TopoRefValue; label: string; value?: number; status: 'resolved' | 'unresolved' }
export type TechDrawAnnotation = { id: string; text: string; position: { x: number; y: number }; style: 'text' | 'callout' }
export type TechDrawGeometricTolerance = { id: string; sourceId: string; reference: TopoRefValue; characteristic: 'flatness' | 'parallelism' | 'perpendicularity' | 'position'; value: number; datum?: string; status: 'resolved' | 'unresolved' }
export type TechDrawSnapshot = { id: string; label: string; page: { width: number; height: number; unit: 'mm' }; sources: TechDrawSource[]; views: TechDrawView[]; dimensions: TechDrawDimension[]; annotations: TechDrawAnnotation[]; tolerances: TechDrawGeometricTolerance[]; version: number }

export type TechDrawApi = {
  snapshot(): TechDrawSnapshot
  addSource(source: TechDrawSource): TechDrawSource
  updateSource(source: TechDrawSource): TechDrawSnapshot
  addView(input: { id: string; sourceId: string; kind?: TechDrawViewKind; direction: TechDrawPoint; position: { x: number; y: number }; scale?: number; section?: { normal: TechDrawPoint; offset: number } }): TechDrawView
  addDimension(input: { id: string; sourceId: string; first: TopoRefValue; second: TopoRefValue; label?: string }): TechDrawDimension
  addAnnotation(input: { id: string; text: string; position: { x: number; y: number }; style?: 'text' | 'callout' }): TechDrawAnnotation
  addGeometricTolerance(input: { id: string; sourceId: string; reference: TopoRefValue; characteristic: TechDrawGeometricTolerance['characteristic']; value: number; datum?: string }): TechDrawGeometricTolerance
  exportSvg(): string
  exportPdf(): Uint8Array
}

const clonePoint = (point: TechDrawPoint): TechDrawPoint => ({ ...point })
const cloneRef = (ref: TopoRefValue): TopoRefValue => ({ ...ref, candidates: ref.candidates ? [...ref.candidates] : undefined })
const cloneSource = (source: TechDrawSource): TechDrawSource => ({ ...source, points: source.points.map((point) => ({ ...point, ref: cloneRef(point.ref), point: clonePoint(point.point) })), edges: source.edges.map((edge) => ({ ...edge })) })
const cloneView = (view: TechDrawView): TechDrawView => ({ ...view, direction: clonePoint(view.direction), position: { ...view.position }, projected: Object.fromEntries(Object.entries(view.projected).map(([key, value]) => [key, { ...value }])), unresolved: [...view.unresolved], section: view.section ? { normal: clonePoint(view.section.normal), offset: view.section.offset } : undefined })
const cloneDimension = (dimension: TechDrawDimension): TechDrawDimension => ({ ...dimension, first: cloneRef(dimension.first), second: cloneRef(dimension.second) })
const cloneTolerance = (tolerance: TechDrawGeometricTolerance): TechDrawGeometricTolerance => ({ ...tolerance, reference: cloneRef(tolerance.reference) })
const finitePoint = (point: TechDrawPoint, label: string) => { if (![point.x, point.y, point.z].every(Number.isFinite)) throw new RangeError(`${label} must be finite.`) }
const norm = (point: TechDrawPoint) => { const length = Math.hypot(point.x, point.y, point.z); if (length <= 1e-12) throw new RangeError('TechDraw direction must be non-zero.'); return { x: point.x / length, y: point.y / length, z: point.z / length } }
const dot = (left: TechDrawPoint, right: TechDrawPoint) => left.x * right.x + left.y * right.y + left.z * right.z
const cross = (left: TechDrawPoint, right: TechDrawPoint) => ({ x: left.y * right.z - left.z * right.y, y: left.z * right.x - left.x * right.z, z: left.x * right.y - left.y * right.x })
const projectPoint = (point: TechDrawPoint, direction: TechDrawPoint) => {
  const normal = norm(direction)
  const seed = Math.abs(normal.z) < 0.9 ? { x: 0, y: 0, z: 1 } : { x: 0, y: 1, z: 0 }
  const right = norm(cross(seed, normal))
  const up = norm(cross(normal, right))
  return { x: dot(point, right), y: dot(point, up) }
}
const xmlEscape = (value: string) => value.replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;').replace(/>/g, '&gt;')

export const createTechDrawPage = (id = 'techdraw', label = 'TechDraw page', width = 297, height = 210): TechDrawApi => {
  if (![width, height].every((value) => Number.isFinite(value) && value > 0)) throw new RangeError('TechDraw page dimensions must be positive and finite.')
  const sources = new Map<string, TechDrawSource>()
  const views = new Map<string, TechDrawView>()
  const dimensions = new Map<string, TechDrawDimension>()
  const annotations = new Map<string, TechDrawAnnotation>()
  const tolerances = new Map<string, TechDrawGeometricTolerance>()
  let version = 0
  const snapshot = (): TechDrawSnapshot => ({ id, label, page: { width, height, unit: 'mm' }, sources: [...sources.values()].map(cloneSource), views: [...views.values()].map(cloneView), dimensions: [...dimensions.values()].map(cloneDimension), annotations: [...annotations.values()].map((annotation) => ({ ...annotation, position: { ...annotation.position } })), tolerances: [...tolerances.values()].map(cloneTolerance), version })
  const sourceById = (sourceId: string) => { const source = sources.get(sourceId); if (!source) throw new RangeError(`TechDraw source does not exist: ${sourceId}`); return source }
  const addSource = (source: TechDrawSource) => { if (!source.id || sources.has(source.id)) throw new RangeError(`TechDraw source already exists: ${source.id}`); if (!Number.isInteger(source.revision) || source.revision < 0) throw new RangeError('TechDraw source revision must be a non-negative integer.'); const pointIds = new Set<string>(); for (const point of source.points) { if (!point.id || pointIds.has(point.id)) throw new RangeError('TechDraw source point IDs must be unique.'); pointIds.add(point.id); finitePoint(point.point, 'TechDraw source point') }; for (const edge of source.edges) if (!pointIds.has(edge.start) || !pointIds.has(edge.end)) throw new RangeError(`TechDraw edge ${edge.id} references an unknown point.`); sources.set(source.id, cloneSource(source)); version += 1; return cloneSource(source) }
  const updateSource = (source: TechDrawSource) => { const current = sourceById(source.id); if (source.revision <= current.revision) throw new RangeError('TechDraw source revision must increase.'); sources.set(source.id, cloneSource(source)); for (const view of views.values()) if (view.sourceId === source.id) { const previousRefs = Object.keys(view.projected); view.sourceRevision = source.revision; view.projected = {}; view.unresolved = []; const refs = new Set(source.points.map((point) => point.ref.persistentId)); for (const point of source.points) view.projected[point.ref.persistentId] = projectPoint(point.point, view.direction); for (const reference of previousRefs) if (!refs.has(reference)) view.unresolved.push(reference) }; for (const dimension of dimensions.values()) if (dimension.sourceId === source.id) { const first = source.points.find((point) => point.ref.persistentId === dimension.first.persistentId); const second = source.points.find((point) => point.ref.persistentId === dimension.second.persistentId); dimension.status = first && second ? 'resolved' : 'unresolved'; dimension.value = first && second ? Math.hypot(second.point.x - first.point.x, second.point.y - first.point.y, second.point.z - first.point.z) : undefined }; for (const tolerance of tolerances.values()) if (tolerance.sourceId === source.id) tolerance.status = source.points.some((point) => point.ref.persistentId === tolerance.reference.persistentId) ? 'resolved' : 'unresolved'; version += 1; return snapshot() }
  const addView = (input: { id: string; sourceId: string; kind?: TechDrawViewKind; direction: TechDrawPoint; position: { x: number; y: number }; scale?: number; section?: { normal: TechDrawPoint; offset: number } }) => { if (views.has(input.id)) throw new RangeError(`TechDraw view already exists: ${input.id}`); const source = sourceById(input.sourceId); const direction = norm(input.direction); if (!Number.isFinite(input.position.x) || !Number.isFinite(input.position.y)) throw new RangeError('TechDraw view position must be finite.'); const scale = input.scale ?? 1; if (!Number.isFinite(scale) || scale <= 0) throw new RangeError('TechDraw view scale must be positive and finite.'); const view: TechDrawView = { id: input.id, sourceId: input.sourceId, kind: input.kind ?? 'projection', direction, position: { ...input.position }, scale, visible: true, sourceRevision: source.revision, projected: Object.fromEntries(source.points.map((point) => [point.ref.persistentId, projectPoint(point.point, direction)])), unresolved: [], section: input.section ? { normal: norm(input.section.normal), offset: input.section.offset } : undefined }; views.set(view.id, view); version += 1; return cloneView(view) }
  const addDimension = (input: { id: string; sourceId: string; first: TopoRefValue; second: TopoRefValue; label?: string }) => { if (dimensions.has(input.id)) throw new RangeError(`TechDraw dimension already exists: ${input.id}`); const source = sourceById(input.sourceId); const first = source.points.find((point) => point.ref.persistentId === input.first.persistentId); const second = source.points.find((point) => point.ref.persistentId === input.second.persistentId); const dimension: TechDrawDimension = { id: input.id, sourceId: input.sourceId, first: cloneRef(input.first), second: cloneRef(input.second), label: input.label ?? input.id, status: first && second ? 'resolved' : 'unresolved', value: first && second ? Math.hypot(second.point.x - first.point.x, second.point.y - first.point.y, second.point.z - first.point.z) : undefined }; dimensions.set(dimension.id, dimension); version += 1; return cloneDimension(dimension) }
  const addAnnotation = (input: { id: string; text: string; position: { x: number; y: number }; style?: 'text' | 'callout' }) => { if (!input.id || annotations.has(input.id) || !input.text) throw new RangeError('TechDraw annotation id/text must be unique and non-empty.'); if (![input.position.x, input.position.y].every(Number.isFinite)) throw new RangeError('TechDraw annotation position must be finite.'); const annotation = { id: input.id, text: input.text, position: { ...input.position }, style: input.style ?? 'text' as const }; annotations.set(annotation.id, annotation); version += 1; return { ...annotation, position: { ...annotation.position } } }
  const addGeometricTolerance = (input: { id: string; sourceId: string; reference: TopoRefValue; characteristic: TechDrawGeometricTolerance['characteristic']; value: number; datum?: string }) => { if (!input.id || tolerances.has(input.id)) throw new RangeError(`TechDraw tolerance already exists: ${input.id}`); if (!Number.isFinite(input.value) || input.value <= 0) throw new RangeError('TechDraw tolerance value must be positive and finite.'); if (input.datum !== undefined && !/^[A-Z][A-Z0-9]{0,2}$/.test(input.datum)) throw new RangeError('TechDraw tolerance datum must be an uppercase datum identifier.'); const source = sourceById(input.sourceId); const tolerance: TechDrawGeometricTolerance = { ...input, reference: cloneRef(input.reference), status: source.points.some((point) => point.ref.persistentId === input.reference.persistentId) ? 'resolved' : 'unresolved' }; tolerances.set(input.id, tolerance); version += 1; return cloneTolerance(tolerance) }
  const exportSvg = () => {
    const viewMarkup = [...views.values()].filter((view) => view.visible).map((view) => { const source = sourceById(view.sourceId); const edges = source.edges.map((edge) => { const start = view.projected[source.points.find((point) => point.id === edge.start)?.ref.persistentId ?? '']; const end = view.projected[source.points.find((point) => point.id === edge.end)?.ref.persistentId ?? '']; return start && end ? `<line x1="${(view.position.x + start.x * view.scale).toFixed(3)}" y1="${(view.position.y - start.y * view.scale).toFixed(3)}" x2="${(view.position.x + end.x * view.scale).toFixed(3)}" y2="${(view.position.y - end.y * view.scale).toFixed(3)}" stroke="#202428"/>` : '' }).join(''); const section = view.kind === 'section' ? `<text x="${view.position.x}" y="${view.position.y + 8}">${xmlEscape(`${view.id} section`)}</text>` : ''; return `<g data-view="${xmlEscape(view.id)}">${edges}${section}</g>` }).join('')
    const dimensionMarkup = [...dimensions.values()].map((dimension) => `<text data-dimension="${xmlEscape(dimension.id)}" x="12" y="${24 + [...dimensions.values()].indexOf(dimension) * 12}">${xmlEscape(`${dimension.label}: ${dimension.status === 'resolved' ? `${dimension.value!.toFixed(3)} mm` : 'unresolved'}`)}</text>`).join('')
    const annotationMarkup = [...annotations.values()].map((annotation) => `<text data-annotation="${xmlEscape(annotation.id)}" x="${annotation.position.x}" y="${annotation.position.y}">${xmlEscape(annotation.text)}</text>`).join('')
    const toleranceMarkup = [...tolerances.values()].map((tolerance, index) => `<text data-tolerance="${xmlEscape(tolerance.id)}" x="12" y="${48 + dimensions.size * 12 + index * 12}">${xmlEscape(`${tolerance.characteristic} ${tolerance.value.toFixed(3)} mm${tolerance.datum ? ` | ${tolerance.datum}` : ''} | ${tolerance.status}`)}</text>`).join('')
    return `<svg xmlns="http://www.w3.org/2000/svg" width="${width}mm" height="${height}mm" viewBox="0 0 ${width} ${height}" role="img" aria-label="${xmlEscape(label)}"><rect width="100%" height="100%" fill="#fff"/>${viewMarkup}${dimensionMarkup}${annotationMarkup}${toleranceMarkup}</svg>`
  }
  const exportPdf = () => {
    const pointScale = 72 / 25.4
    const clean = (value: string) => value.replace(/[^\x20-\x7e]/g, '?').replace(/([\\()])/g, '\\$1')
    const commands: string[] = ['0 0 0 RG', '0.75 w']
    for (const view of views.values()) {
      const source = sourceById(view.sourceId)
      for (const edge of source.edges) {
        const startId = source.points.find((point) => point.id === edge.start)?.ref.persistentId
        const endId = source.points.find((point) => point.id === edge.end)?.ref.persistentId
        const start = startId ? view.projected[startId] : undefined
        const end = endId ? view.projected[endId] : undefined
        if (start && end) commands.push(`${((view.position.x + start.x * view.scale) * pointScale).toFixed(3)} ${((height - view.position.y + start.y * view.scale) * pointScale).toFixed(3)} m ${((view.position.x + end.x * view.scale) * pointScale).toFixed(3)} ${((height - view.position.y + end.y * view.scale) * pointScale).toFixed(3)} l S`)
      }
    }
    const textLines = [label, ...[...dimensions.values()].map((dimension) => `${dimension.label}: ${dimension.status === 'resolved' ? `${dimension.value!.toFixed(3)} mm` : 'unresolved'}`), ...[...annotations.values()].map((annotation) => annotation.text), ...[...tolerances.values()].map((tolerance) => `${tolerance.characteristic} ${tolerance.value.toFixed(3)} mm${tolerance.datum ? ` ${tolerance.datum}` : ''} ${tolerance.status}`)]
    textLines.forEach((line, index) => commands.push(`BT /F1 9 Tf 24 ${(height * pointScale - 24 - index * 12).toFixed(3)} Td (${clean(line)}) Tj ET`))
    const content = `${commands.join('\n')}\n`
    const objects = [
      '<< /Type /Catalog /Pages 2 0 R >>',
      '<< /Type /Pages /Kids [3 0 R] /Count 1 >>',
      `<< /Type /Page /Parent 2 0 R /MediaBox [0 0 ${(width * pointScale).toFixed(3)} ${(height * pointScale).toFixed(3)}] /Resources << /Font << /F1 4 0 R >> >> /Contents 5 0 R >>`,
      '<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>',
      `<< /Length ${new TextEncoder().encode(content).byteLength} >>\nstream\n${content}endstream`,
    ]
    let output = '%PDF-1.4\n% BitBybit TechDraw\n'
    const offsets = [0]
    for (let index = 0; index < objects.length; index += 1) { offsets.push(new TextEncoder().encode(output).byteLength); output += `${index + 1} 0 obj\n${objects[index]}\nendobj\n` }
    const xrefOffset = new TextEncoder().encode(output).byteLength
    output += `xref\n0 ${objects.length + 1}\n0000000000 65535 f \n${offsets.slice(1).map((offset) => `${String(offset).padStart(10, '0')} 00000 n `).join('\n')}\ntrailer\n<< /Size ${objects.length + 1} /Root 1 0 R >>\nstartxref\n${xrefOffset}\n%%EOF\n`
    return new TextEncoder().encode(output)
  }
  return { snapshot, addSource, updateSource, addView, addDimension, addAnnotation, addGeometricTolerance, exportSvg, exportPdf }
}
