export type QuantityDimension = 'dimensionless' | 'length' | 'angle' | 'area' | 'volume' | 'acceleration' | 'force' | 'heat-flux' | 'percent'

export type Quantity = {
  value: number
  dimension: QuantityDimension
}

export type UnitDefinition = {
  id: string
  symbol: string
  dimension: QuantityDimension
  factor: number
}

const definitions: UnitDefinition[] = [
  { id: 'mm', symbol: 'mm', dimension: 'length', factor: 1 },
  { id: 'cm', symbol: 'cm', dimension: 'length', factor: 10 },
  { id: 'm', symbol: 'm', dimension: 'length', factor: 1000 },
  { id: 'in', symbol: 'in', dimension: 'length', factor: 25.4 },
  { id: 'ft', symbol: 'ft', dimension: 'length', factor: 304.8 },
  { id: 'mm2', symbol: 'mm2', dimension: 'area', factor: 1 },
  { id: 'cm2', symbol: 'cm2', dimension: 'area', factor: 100 },
  { id: 'm2', symbol: 'm2', dimension: 'area', factor: 1_000_000 },
  { id: 'mm3', symbol: 'mm3', dimension: 'volume', factor: 1 },
  { id: 'cm3', symbol: 'cm3', dimension: 'volume', factor: 1_000 },
  { id: 'm3', symbol: 'm3', dimension: 'volume', factor: 1_000_000_000 },
  { id: 'mm/s^2', symbol: 'mm/s^2', dimension: 'acceleration', factor: 1 },
  { id: 'cm/s^2', symbol: 'cm/s^2', dimension: 'acceleration', factor: 10 },
  { id: 'm/s^2', symbol: 'm/s^2', dimension: 'acceleration', factor: 1000 },
  { id: 'in/s^2', symbol: 'in/s^2', dimension: 'acceleration', factor: 25.4 },
  { id: 'ft/s^2', symbol: 'ft/s^2', dimension: 'acceleration', factor: 304.8 },
  { id: 'mm*kg/s^2', symbol: 'mm*kg/s^2', dimension: 'force', factor: 1 },
  { id: 'N', symbol: 'N', dimension: 'force', factor: 1_000 },
  { id: 'kN', symbol: 'kN', dimension: 'force', factor: 1_000_000 },
  { id: 'lbf', symbol: 'lbf', dimension: 'force', factor: 4_448.2216152605 },
  { id: 'kg/s^3', symbol: 'kg/s^3', dimension: 'heat-flux', factor: 1 },
  { id: 'W/m^2', symbol: 'W/m^2', dimension: 'heat-flux', factor: 1 },
  { id: 'kW/m^2', symbol: 'kW/m^2', dimension: 'heat-flux', factor: 1_000 },
  { id: 'W/mm^2', symbol: 'W/mm^2', dimension: 'heat-flux', factor: 1_000_000 },
  { id: 'deg', symbol: 'deg', dimension: 'angle', factor: 1 },
  { id: 'rad', symbol: 'rad', dimension: 'angle', factor: 180 / Math.PI },
  { id: '%', symbol: '%', dimension: 'percent', factor: 1 },
]

const bySymbol = new Map(definitions.map((definition) => [definition.symbol.toLowerCase(), definition]))

export const listUnits = (): UnitDefinition[] => definitions.map((definition) => ({ ...definition }))

export const getUnit = (symbol: string): UnitDefinition | undefined => bySymbol.get(symbol.toLowerCase())

export const quantityDimensionForUnit = (unit: string | undefined): QuantityDimension => {
  if (!unit) return 'dimensionless'
  const definition = getUnit(unit)
  if (!definition) throw new RangeError(`Unknown unit: ${unit}`)
  return definition.dimension
}

export const quantityFromNumber = (value: number, dimension: QuantityDimension = 'dimensionless'): Quantity => {
  if (!Number.isFinite(value)) throw new RangeError('Quantity value must be finite.')
  return { value, dimension }
}

export const quantityFromUnit = (value: number, unit: string): Quantity => {
  if (!Number.isFinite(value)) throw new RangeError('Quantity value must be finite.')
  const definition = getUnit(unit)
  if (!definition) throw new RangeError(`Unknown unit: ${unit}`)
  return { value: value * definition.factor, dimension: definition.dimension }
}

export const convertQuantity = (quantity: Quantity, unit: string): number => {
  const definition = getUnit(unit)
  if (!definition) throw new RangeError(`Unknown unit: ${unit}`)
  if (quantity.dimension !== definition.dimension) throw new TypeError(`Cannot convert ${quantity.dimension} to ${definition.dimension}.`)
  return quantity.value / definition.factor
}

type Token = { kind: 'number' | 'identifier' | 'operator' | 'eof'; text: string; position: number }

const tokenize = (source: string): Token[] => {
  const tokens: Token[] = []
  let index = 0
  while (index < source.length) {
    const character = source[index]
    if (/\s/.test(character)) { index += 1; continue }
    if ('+-*/^(),'.includes(character)) { tokens.push({ kind: 'operator', text: character, position: index }); index += 1; continue }
    if (character === '%') { tokens.push({ kind: 'identifier', text: character, position: index }); index += 1; continue }
    const number = source.slice(index).match(/^(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?/)
    if (number) { tokens.push({ kind: 'number', text: number[0], position: index }); index += number[0].length; continue }
    const identifier = source.slice(index).match(/^[A-Za-z_][A-Za-z0-9_.:]*/)
    if (identifier) { tokens.push({ kind: 'identifier', text: identifier[0], position: index }); index += identifier[0].length; continue }
    throw new SyntaxError(`Unexpected character '${character}' at position ${index}.`)
  }
  tokens.push({ kind: 'eof', text: '', position: source.length })
  return tokens
}

const sameDimension = (left: Quantity, right: Quantity) => left.dimension === right.dimension

const add = (left: Quantity, right: Quantity, operator: '+' | '-') => {
  if (!sameDimension(left, right)) throw new TypeError(`Cannot ${operator === '+' ? 'add' : 'subtract'} ${left.dimension} and ${right.dimension}.`)
  return { value: operator === '+' ? left.value + right.value : left.value - right.value, dimension: left.dimension }
}

const multiply = (left: Quantity, right: Quantity): Quantity => {
  if (left.dimension === 'dimensionless') return { value: left.value * right.value, dimension: right.dimension }
  if (right.dimension === 'dimensionless') return { value: left.value * right.value, dimension: left.dimension }
  throw new TypeError(`Cannot multiply ${left.dimension} by ${right.dimension}.`)
}

const divide = (left: Quantity, right: Quantity): Quantity => {
  if (right.value === 0) throw new RangeError('Division by zero.')
  if (right.dimension === 'dimensionless') return { value: left.value / right.value, dimension: left.dimension }
  if (left.dimension === right.dimension) return { value: left.value / right.value, dimension: 'dimensionless' }
  throw new TypeError(`Cannot divide ${left.dimension} by ${right.dimension}.`)
}

const power = (base: Quantity, exponent: Quantity): Quantity => {
  if (exponent.dimension !== 'dimensionless' || !Number.isInteger(exponent.value)) throw new TypeError('Exponent must be a dimensionless integer.')
  if (base.dimension === 'dimensionless') return { value: Math.pow(base.value, exponent.value), dimension: 'dimensionless' }
  if (exponent.value === 1) return { ...base }
  if (exponent.value === 2 && base.dimension === 'length') return { value: base.value ** 2, dimension: 'area' }
  if (exponent.value === 3 && base.dimension === 'length') return { value: base.value ** 3, dimension: 'volume' }
  throw new TypeError(`Cannot raise ${base.dimension} to the power ${exponent.value}.`)
}

const evaluateFunction = (name: string, args: Quantity[]): Quantity => {
  const normalized = name.toLowerCase()
  if (normalized === 'abs' || normalized === 'round') {
    if (args.length !== 1) throw new TypeError(`${name} requires one argument.`)
    return { value: normalized === 'abs' ? Math.abs(args[0].value) : Math.round(args[0].value), dimension: args[0].dimension }
  }
  if (normalized === 'sin' || normalized === 'cos' || normalized === 'tan') {
    if (args.length !== 1 || (args[0].dimension !== 'angle' && args[0].dimension !== 'dimensionless')) throw new TypeError(`${name} requires an angle or dimensionless argument.`)
    const radians = args[0].dimension === 'angle' ? args[0].value * Math.PI / 180 : args[0].value
    return { value: Math[normalized](radians), dimension: 'dimensionless' }
  }
  if (normalized === 'atan2') {
    if (args.length !== 2 || !sameDimension(args[0], args[1])) throw new TypeError('atan2 requires two values with the same dimension.')
    return { value: Math.atan2(args[0].value, args[1].value) * 180 / Math.PI, dimension: 'angle' }
  }
  if (normalized === 'min' || normalized === 'max') {
    if (args.length < 1 || args.some((argument) => !sameDimension(argument, args[0]))) throw new TypeError(`${name} requires at least one set of same-dimension values.`)
    const values = args.map((argument) => argument.value)
    return { value: normalized === 'min' ? Math.min(...values) : Math.max(...values), dimension: args[0].dimension }
  }
  if (normalized === 'clamp') {
    if (args.length !== 3 || !sameDimension(args[0], args[1]) || !sameDimension(args[0], args[2])) throw new TypeError('clamp requires three values with the same dimension.')
    return { value: Math.min(Math.max(args[0].value, args[1].value), args[2].value), dimension: args[0].dimension }
  }
  if (normalized === 'pow') {
    if (args.length !== 2 || args[0].dimension !== 'dimensionless' || args[1].dimension !== 'dimensionless') throw new TypeError('pow requires two dimensionless values.')
    return { value: Math.pow(args[0].value, args[1].value), dimension: 'dimensionless' }
  }
  throw new ReferenceError(`Unknown expression function: ${name}.`)
}

class QuantityParser {
  private index = 0
  readonly references = new Set<string>()

  constructor(private readonly tokens: Token[], private readonly variables: ReadonlyMap<string, Quantity>) {}

  parse(): Quantity {
    const result = this.parseAddSub()
    const token = this.peek()
    if (token.kind !== 'eof') throw new SyntaxError(`Unexpected token '${token.text}' at position ${token.position}.`)
    return result
  }

  private peek() { return this.tokens[this.index] }

  private take() { return this.tokens[this.index++] }

  private match(text: string) {
    if (this.peek().text !== text) return false
    this.index += 1
    return true
  }

  private parseAddSub(): Quantity {
    let result = this.parseMulDiv()
    while (this.peek().text === '+' || this.peek().text === '-') {
      const operator = this.take().text as '+' | '-'
      result = add(result, this.parseMulDiv(), operator)
    }
    return result
  }

  private parseMulDiv(): Quantity {
    let result = this.parseUnary()
    while (this.peek().text === '*' || this.peek().text === '/') {
      const operator = this.take().text
      result = operator === '*' ? multiply(result, this.parseUnary()) : divide(result, this.parseUnary())
    }
    return result
  }

  private parseUnary(): Quantity {
    if (this.match('+')) return this.parseUnary()
    if (this.match('-')) { const value = this.parseUnary(); return { ...value, value: -value.value } }
    return this.parsePower()
  }

  private parsePower(): Quantity {
    const base = this.parsePrimary()
    if (!this.match('^')) return base
    return power(base, this.parseUnary())
  }

  private parsePrimary(): Quantity {
    const token = this.take()
    if (token.text === '(') {
      const result = this.parseAddSub()
      if (!this.match(')')) throw new SyntaxError(`Missing ')' at position ${this.peek().position}.`)
      return result
    }
    if (token.kind === 'number') {
      const value = Number(token.text)
      const next = this.peek()
      if (next.kind === 'identifier' && getUnit(next.text)) return quantityFromUnit(value, this.take().text)
      return quantityFromNumber(value)
    }
    if (token.kind === 'identifier') {
      if (token.text.toLowerCase() === 'pi') return quantityFromNumber(Math.PI)
      if (this.match('(')) {
        const args: Quantity[] = []
        if (!this.match(')')) {
          do args.push(this.parseAddSub()); while (this.match(','))
          if (!this.match(')')) throw new SyntaxError(`Missing ')' at position ${this.peek().position}.`)
        }
        return evaluateFunction(token.text, args)
      }
      const value = this.variables.get(token.text)
      if (!value) throw new ReferenceError(`Unknown expression reference: ${token.text}.`)
      this.references.add(token.text)
      return { ...value }
    }
    throw new SyntaxError(`Expected a quantity at position ${token.position}.`)
  }
}

export const evaluateQuantityExpression = (expression: string, variables: ReadonlyMap<string, Quantity> = new Map()) => {
  if (!expression.trim()) throw new SyntaxError('Expression cannot be empty.')
  const parser = new QuantityParser(tokenize(expression), variables)
  const value = parser.parse()
  if (!Number.isFinite(value.value)) throw new RangeError('Expression result must be finite.')
  return { value, references: [...parser.references] }
}

export const extractExpressionReferences = (expression: string): string[] => {
  const references = new Set<string>()
  for (const token of tokenize(expression)) {
    if (token.kind !== 'identifier' || token.text.toLowerCase() === 'pi' || getUnit(token.text)) continue
    if (token.text.includes('.')) references.add(token.text)
  }
  return [...references]
}

export const formatQuantity = (quantity: Quantity, unit: string, fractionDigits = 6): string => {
  const value = convertQuantity(quantity, unit)
  return `${Number(value.toFixed(fractionDigits))} ${unit}`
}
