import { access, readdir, readFile, writeFile } from 'node:fs/promises'
import { join, relative, resolve } from 'node:path'

const root = resolve(new URL('..', import.meta.url).pathname)
const sourceRoot = process.env.FREECAD_SOURCE_DIR
  ? resolve(process.env.FREECAD_SOURCE_DIR)
  : join(root, '.cache/freecad/FreeCAD')
const outputPath = resolve(process.env.FREECAD_TYPE_PROPERTY_INVENTORY_FILE || join(root, 'config/freecad-type-property-inventory.json'))
const baselineCommit = '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d'
const sourceMod = join(sourceRoot, 'src/Mod')
const exists = async (path) => access(path).then(() => true).catch(() => false)
if (!(await exists(sourceMod))) throw new Error(`FreeCAD source directory is unavailable: ${sourceMod}`)

const sourceFiles = async (directory) => {
  const files = []
  for (const entry of await readdir(directory, { withFileTypes: true })) {
    const path = join(directory, entry.name)
    if (entry.isDirectory()) files.push(...await sourceFiles(path))
    else if (/\.(?:cpp|h|hpp|hxx|py)$/.test(entry.name)) files.push(path)
  }
  return files
}

const classPattern = /\b(?:class|struct)\s+(?:[A-Z_][A-Za-z0-9_]*::)?([A-Za-z_][A-Za-z0-9_]*)\s*(?::|\{)/g
const propertyRegistrationPattern = /\bADD_PROPERTY(?:_TYPE)?\s*\(\s*([A-Za-z_][A-Za-z0-9_]*)/g
const propertyMemberPattern = /\bApp::(Property[A-Za-z0-9_]+)\s+([A-Za-z_][A-Za-z0-9_]*)\s*(?:;|,)/g
const pythonPropertyPattern = /\.addProperty\(\s*([^,\n]+),\s*["']([^"']+)["']/g
const typeIdPattern = /\b([A-Za-z_][A-Za-z0-9_]*)::(?:getClassTypeId|getTypeId)\s*\(/g
const typeDeclarationPattern = /\b(?:PROPERTY_SOURCE(?:_[A-Z_]+)?|TYPESYSTEM_SOURCE(?:_[A-Z_]+)?|EXTENSION_PROPERTY_SOURCE(?:_[A-Z_]+)?)\s*\(\s*([A-Za-z_][A-Za-z0-9_]*::[A-Za-z_][A-Za-z0-9_]*)/g
const documentObjectUsagePattern = /\baddObject\s*\(\s*["']([A-Za-z_][A-Za-z0-9_]*::[A-Za-z_][A-Za-z0-9_]*)["']/g
const unique = (values) => [...new Set(values)].sort()

const moduleEntries = (await readdir(sourceMod, { withFileTypes: true })).filter((entry) => entry.isDirectory()).sort((a, b) => a.name.localeCompare(b.name))
const modules = []
for (const entry of moduleEntries) {
  const modulePath = join(sourceMod, entry.name)
  const files = await sourceFiles(modulePath)
  const objects = []
  let propertyRegistrationCount = 0
  let propertyMemberCount = 0
  let pythonPropertyCount = 0
  let typeIdDeclarationCount = 0
  let documentObjectUsageCount = 0
  for (const file of files) {
    const text = await readFile(file, 'utf8')
    const classes = unique([...text.matchAll(classPattern)].map((match) => match[1]))
    const registrations = unique([...text.matchAll(propertyRegistrationPattern)].map((match) => match[1]))
    const members = unique([...text.matchAll(propertyMemberPattern)].map((match) => ({ type: match[1], name: match[2] })))
    const pythonProperties = [...text.matchAll(pythonPropertyPattern)].map((match) => ({ typeExpression: match[1].trim(), name: match[2] }))
    const typeIds = unique([...text.matchAll(typeIdPattern)].map((match) => match[1]))
    const typeIdDeclarations = unique([...text.matchAll(typeDeclarationPattern)].map((match) => match[1]))
    const documentObjectUsages = unique([...text.matchAll(documentObjectUsagePattern)].map((match) => match[1]))
    propertyRegistrationCount += registrations.length
    propertyMemberCount += members.length
    pythonPropertyCount += pythonProperties.length
    typeIdDeclarationCount += typeIdDeclarations.length
    documentObjectUsageCount += documentObjectUsages.length
    if (classes.length || registrations.length || members.length || pythonProperties.length || typeIds.length || typeIdDeclarations.length || documentObjectUsages.length) {
      const object = {
        sourceFile: relative(sourceRoot, file),
        classes,
        typeIdSymbols: typeIds,
        propertyRegistrations: registrations,
        propertyMembers: members,
        pythonProperties,
      }
      if (typeIdDeclarations.length > 0) object.typeIdDeclarations = typeIdDeclarations
      if (documentObjectUsages.length > 0) object.documentObjectUsages = documentObjectUsages
      objects.push(object)
    }
  }
  modules.push({
    name: entry.name,
    sourcePath: `src/Mod/${entry.name}`,
    sourceFileCount: files.length,
    objectSourceCount: objects.length,
    propertyRegistrationCount,
    propertyMemberCount,
    pythonPropertyCount,
    typeIdDeclarationCount,
    documentObjectUsageCount,
    objects,
    runtimeStatus: 'requires-freecad-reference-probe',
  })
}

const indexTypeIds = (field) => {
  const index = new Map()
  for (const module of modules) for (const object of module.objects) for (const typeId of object[field] ?? []) {
    const record = index.get(typeId) || { typeId, sourceModules: new Set(), sourceFiles: new Set() }
    record.sourceModules.add(module.name)
    record.sourceFiles.add(object.sourceFile)
    index.set(typeId, record)
  }
  return [...index.values()]
    .map((record) => ({ typeId: record.typeId, sourceModules: [...record.sourceModules].sort(), sourceFiles: [...record.sourceFiles].sort() }))
    .sort((left, right) => left.typeId.localeCompare(right.typeId))
}
const typeIdDeclarations = indexTypeIds('typeIdDeclarations')
const documentObjectUsages = indexTypeIds('documentObjectUsages')

const inventory = {
  schemaVersion: 1,
  baseline: { freecadVersion: '1.1.1', commit: baselineCommit, sourcePath: '.cache/freecad/FreeCAD' },
  generatedBy: 'scripts/generate-freecad-type-property-inventory.mjs',
  moduleCount: modules.length,
  typeIdDeclarations,
  documentObjectUsages,
  totals: {
    sourceFileCount: modules.reduce((sum, module) => sum + module.sourceFileCount, 0),
    objectSourceCount: modules.reduce((sum, module) => sum + module.objectSourceCount, 0),
    propertyRegistrationCount: modules.reduce((sum, module) => sum + module.propertyRegistrationCount, 0),
    propertyMemberCount: modules.reduce((sum, module) => sum + module.propertyMemberCount, 0),
    pythonPropertyCount: modules.reduce((sum, module) => sum + module.pythonPropertyCount, 0),
    typeIdDeclarationCount: typeIdDeclarations.length,
    typeIdDeclarationEvidenceCount: modules.reduce((sum, module) => sum + module.typeIdDeclarationCount, 0),
    documentObjectUsageCount: documentObjectUsages.length,
    documentObjectUsageEvidenceCount: modules.reduce((sum, module) => sum + module.documentObjectUsageCount, 0),
  },
  modules,
}
if (modules.length !== 34) throw new Error(`Expected 34 FreeCAD modules, found ${modules.length}.`)
await writeFile(outputPath, `${JSON.stringify(inventory, null, 2)}\n`, 'utf8')
console.log(JSON.stringify({ status: 'type-property-inventory-generated', output: outputPath, moduleCount: modules.length, totals: inventory.totals }, null, 2))
