import { createSketch, solveSketch, type SketchConstraint, type SketchGeometry, type SketchSolverStatus } from './sketcher'

export type SketchStressCategory = 'constraints' | 'conflict' | 'redundant' | 'reference' | 'drag'

export type SketchStressReport = {
  seed: number
  models: number
  solves: number
  durationMs: number
  averageSolveMs: number
  maxIterations: number
  maxResidual: number
  deterministicMismatches: number
  unexpectedResults: number
  digest: string
  categories: Record<SketchStressCategory, number>
  statuses: Record<SketchSolverStatus, number>
}

export type SketchStressOptions = { seed?: number; models?: number }

const randomGenerator = (seed: number) => {
  let state = seed >>> 0
  return () => {
    state += 0x6d2b79f5
    let value = state
    value = Math.imul(value ^ value >>> 15, value | 1)
    value ^= value + Math.imul(value ^ value >>> 7, value | 61)
    return ((value ^ value >>> 14) >>> 0) / 0x1_0000_0000
  }
}

const digestText = (current: number, value: string) => {
  let digest = current
  for (let index = 0; index < value.length; index += 1) digest = Math.imul(digest ^ value.charCodeAt(index), 16777619)
  return digest >>> 0
}

const fixtureFor = (index: number, random: () => number) => {
  const category = (['constraints', 'conflict', 'redundant', 'reference', 'drag'] as const)[index % 5]
  const offset = Math.round(random() * 1000) / 100
  let geometry: SketchGeometry[]
  let constraints: SketchConstraint[]
  let expected: SketchSolverStatus
  if (category === 'constraints') {
    geometry = [
      { id: 'axis', type: 'line', start: { x: offset, y: 0 }, end: { x: offset + 4, y: 0 } },
      { id: 'parallel', type: 'line', start: { x: 0, y: 2 }, end: { x: 1, y: 3 } },
      { id: 'perpendicular', type: 'line', start: { x: 2, y: 0 }, end: { x: 4, y: 1 } },
    ]
    constraints = [
      { id: 'parallel', type: 'parallel', firstGeometryId: 'axis', secondGeometryId: 'parallel' },
      { id: 'perpendicular', type: 'perpendicular', firstGeometryId: 'axis', secondGeometryId: 'perpendicular' },
    ]
    expected = 'under-constrained'
  } else if (category === 'conflict') {
    geometry = [{ id: 'line', type: 'line', start: { x: 0, y: 0 }, end: { x: 1 + offset, y: 1 } }]
    constraints = [{ id: 'block', type: 'block', geometryId: 'line' }, { id: 'horizontal', type: 'horizontal', geometryId: 'line' }]
    expected = 'conflicting'
  } else if (category === 'redundant') {
    geometry = [{ id: 'line', type: 'line', start: { x: 0, y: offset }, end: { x: 2, y: offset + 1 } }]
    constraints = [{ id: 'horizontal', type: 'horizontal', geometryId: 'line' }, { id: 'duplicate', type: 'horizontal', geometryId: 'line' }]
    expected = 'under-constrained'
  } else if (category === 'reference') {
    geometry = [{ id: 'line', type: 'line', start: { x: 0, y: 0 }, end: { x: 2 + offset, y: 1 } }]
    constraints = [
      { id: 'horizontal', type: 'horizontal', geometryId: 'line' },
      { id: 'reference', type: 'distance', first: { geometryId: 'line', point: 'start' }, second: { geometryId: 'line', point: 'end' }, value: 999, driving: false },
    ]
    expected = 'under-constrained'
  } else {
    geometry = [{ id: 'circle', type: 'circle', center: { x: offset, y: 0 }, radius: 2 }, { id: 'point', type: 'point', position: { x: offset + 4, y: 1 } }]
    constraints = [{ id: 'drag-on-circle', type: 'pointOnObject', point: { geometryId: 'point', point: 'position' }, geometryId: 'circle' }]
    expected = 'under-constrained'
  }
  return { category, snapshot: createSketch(`stress-${index}`, geometry, constraints), expected }
}

export const runSketchStress = (options: SketchStressOptions = {}): SketchStressReport => {
  const seed = options.seed ?? 0x20260803
  const models = options.models ?? 500
  if (!Number.isSafeInteger(models) || models < 1) throw new RangeError('Sketch stress models must be a positive safe integer.')
  const random = randomGenerator(seed)
  const categories: SketchStressReport['categories'] = { constraints: 0, conflict: 0, redundant: 0, reference: 0, drag: 0 }
  const statuses: SketchStressReport['statuses'] = { solved: 0, 'under-constrained': 0, conflicting: 0, invalid: 0 }
  let deterministicMismatches = 0
  let unexpectedResults = 0
  let maxIterations = 0
  let maxResidual = 0
  let digest = 2166136261
  const startedAt = performance.now()
  for (let index = 0; index < models; index += 1) {
    const fixture = fixtureFor(index, random)
    categories[fixture.category] += 1
    const first = solveSketch(fixture.snapshot)
    const second = solveSketch(fixture.snapshot)
    statuses[first.status] += 1
    maxIterations = Math.max(maxIterations, first.iterations)
    if (fixture.expected !== 'conflicting' && Number.isFinite(first.residual)) maxResidual = Math.max(maxResidual, first.residual)
    const firstSnapshot = JSON.stringify(first.snapshot)
    if (firstSnapshot !== JSON.stringify(second.snapshot)) deterministicMismatches += 1
    const requiredDiagnostic = fixture.category === 'redundant' ? first.diagnostics.some((diagnostic) => diagnostic.code === 'REDUNDANT_CONSTRAINT') : fixture.category === 'conflict' ? first.diagnostics.some((diagnostic) => diagnostic.code === 'SOLVER_NOT_CONVERGED') : true
    if (first.status !== fixture.expected || !requiredDiagnostic) unexpectedResults += 1
    digest = digestText(digest, firstSnapshot)
  }
  const durationMs = performance.now() - startedAt
  return { seed, models, solves: models * 2, durationMs, averageSolveMs: durationMs / (models * 2), maxIterations, maxResidual, deterministicMismatches, unexpectedResults, digest: digest.toString(16).padStart(8, '0'), categories, statuses }
}
