import type { AttachmentSupportValue, PlacementValue, TopoRefValue, VectorValue } from './types'

export const ATTACHMENT_MAP_MODES = ['Deactivated', 'ObjectXY', 'ObjectXZ', 'ObjectYZ', 'FlatFace', 'NormalToEdge'] as const
export type AttachmentMapMode = typeof ATTACHMENT_MAP_MODES[number]

export type AttachmentSupport = AttachmentSupportValue

export type AttachmentFrame = {
  support: AttachmentSupport | null
  mapMode: AttachmentMapMode
  offset: PlacementValue
}

const finite = (value: number, name: string) => {
  if (!Number.isFinite(value)) throw new TypeError(`${name} must be finite.`)
  return value
}

const vector = (value: VectorValue, name: string) => ({ x: finite(value.x, `${name}.x`), y: finite(value.y, `${name}.y`), z: finite(value.z, `${name}.z`) })

export function validateAttachmentMapMode(mode: unknown): asserts mode is AttachmentMapMode {
  if (typeof mode !== 'string' || !(ATTACHMENT_MAP_MODES as readonly string[]).includes(mode)) throw new RangeError(`MapMode '${String(mode)}' is not supported.`)
}

export function validateAttachmentSupport(support: unknown): asserts support is AttachmentSupport | null {
  if (support === null) return
  if (!support || typeof support !== 'object' || Array.isArray(support)) throw new TypeError('Support requires an object link.')
  const candidate = support as Record<string, unknown>
  if (typeof candidate.objectId !== 'string' || !candidate.objectId.trim()) throw new TypeError('Support objectId is required.')
  if (candidate.subElement !== undefined && candidate.subElement !== null) {
    if (typeof candidate.subElement === 'string') {
      if (!candidate.subElement.trim()) throw new TypeError('Support subElement must be a non-empty string.')
    } else if (typeof candidate.subElement === 'object' && !Array.isArray(candidate.subElement)) {
      const ref = candidate.subElement as Partial<TopoRefValue>
      if (ref.schemaVersion !== 1 || typeof ref.objectId !== 'string' || typeof ref.persistentId !== 'string' || !['face', 'edge', 'vertex'].includes(ref.kind as string)) throw new TypeError('Support subElement TopoRef is invalid.')
    } else throw new TypeError('Support subElement must be a non-empty string or TopoRef.')
  }
}

export function validateAttachmentOffset(offset: unknown): asserts offset is PlacementValue {
  if (!offset || typeof offset !== 'object' || Array.isArray(offset)) throw new TypeError('AttachmentOffset requires a Placement value.')
  const value = offset as PlacementValue
  if (!value.position || typeof value.position !== 'object' || !value.rotation || typeof value.rotation !== 'object' || !value.rotation.axis || typeof value.rotation.axis !== 'object') throw new TypeError('AttachmentOffset requires position and rotation.')
  const position = vector(value.position, 'AttachmentOffset position')
  const axis = vector(value.rotation?.axis, 'AttachmentOffset rotation axis')
  const angle = finite(value.rotation?.angle, 'AttachmentOffset rotation angle')
  if (Math.hypot(axis.x, axis.y, axis.z) === 0) throw new RangeError('AttachmentOffset rotation axis cannot be zero.')
  if (angle < 0 || angle > 360) throw new RangeError('AttachmentOffset rotation angle must be between 0 and 360 degrees.')
  void position
}

const quaternion = (placement: PlacementValue): [number, number, number, number] => {
  const axis = vector(placement.rotation.axis, 'rotation axis')
  const length = Math.hypot(axis.x, axis.y, axis.z)
  if (length === 0) return [1, 0, 0, 0]
  const half = placement.rotation.angle * Math.PI / 360
  const scale = Math.sin(half) / length
  return [Math.cos(half), axis.x * scale, axis.y * scale, axis.z * scale]
}

const multiply = (left: [number, number, number, number], right: [number, number, number, number]): [number, number, number, number] => [
  left[0] * right[0] - left[1] * right[1] - left[2] * right[2] - left[3] * right[3],
  left[0] * right[1] + left[1] * right[0] + left[2] * right[3] - left[3] * right[2],
  left[0] * right[2] - left[1] * right[3] + left[2] * right[0] + left[3] * right[1],
  left[0] * right[3] + left[1] * right[2] - left[2] * right[1] + left[3] * right[0],
]

const rotate = (value: VectorValue, q: [number, number, number, number]): VectorValue => {
  const point: [number, number, number, number] = [0, value.x, value.y, value.z]
  const inverse: [number, number, number, number] = [q[0], -q[1], -q[2], -q[3]]
  const result = multiply(multiply(q, point), inverse)
  return { x: result[1], y: result[2], z: result[3] }
}

export const composeAttachmentPlacement = (supportPlacement: PlacementValue, offset: PlacementValue): PlacementValue => {
  validateAttachmentOffset(supportPlacement)
  validateAttachmentOffset(offset)
  const rotation = multiply(quaternion(supportPlacement), quaternion(offset))
  const translatedOffset = rotate(offset.position, quaternion(supportPlacement))
  const position = {
    x: supportPlacement.position.x + translatedOffset.x,
    y: supportPlacement.position.y + translatedOffset.y,
    z: supportPlacement.position.z + translatedOffset.z,
  }
  const vectorLength = Math.hypot(rotation[1], rotation[2], rotation[3])
  const rawAngle = Math.min(360, Math.max(0, 2 * Math.atan2(vectorLength, rotation[0]) * 180 / Math.PI))
  const angle = Math.abs(rawAngle - Math.round(rawAngle)) < 1e-12 ? Math.round(rawAngle) : Number(rawAngle.toFixed(12))
  const axis = vectorLength < 1e-12 ? { x: 0, y: 0, z: 1 } : { x: rotation[1] / vectorLength, y: rotation[2] / vectorLength, z: rotation[3] / vectorLength }
  return { position, rotation: { axis, angle } }
}

export const identityAttachmentOffset = (): PlacementValue => ({ position: { x: 0, y: 0, z: 0 }, rotation: { axis: { x: 0, y: 0, z: 1 }, angle: 0 } })
