import { Node, Type } from 'ts-morph'
import type { SourceFile } from './syntax/SourceFile.js'
import { createCppHybridObject } from './syntax/c++/CppHybridObject.js'
import {
  extendsHybridObject,
  isHybridView,
  isAnyHybridSubclass,
  isDirectlyHybridObject,
  type Language,
  isHybridViewProps,
  isHybridViewMethods,
} from './getPlatformSpecs.js'
import type { HybridObjectSpec } from './syntax/HybridObjectSpec.js'
import { Property } from './syntax/Property.js'
import { Method } from './syntax/Method.js'
import { createSwiftHybridObject } from './syntax/swift/SwiftHybridObject.js'
import { createKotlinHybridObject } from './syntax/kotlin/KotlinHybridObject.js'
import { createType } from './syntax/createType.js'
import { Parameter } from './syntax/Parameter.js'
import { getBaseTypes } from './utils.js'

export function generatePlatformFiles(
  interfaceType: Type,
  language: Language
): SourceFile[] {
  const spec = getHybridObjectSpec(interfaceType, language)

  // TODO: We currently just call this so the HybridObject itself is a "known type".
  // This causes the Swift Umbrella header to properly forward-declare it.
  // Without this, only Hybrid Objects that are actually used in public APIs will be forward-declared.
  createType(language, interfaceType, false)

  switch (language) {
    case 'c++':
      return generateCppFiles(spec)
    case 'swift':
      return generateSwiftFiles(spec)
    case 'kotlin':
      return generateKotlinFiles(spec)
    default:
      throw new Error(`Language "${language}" is not supported!`)
  }
}

function getHybridObjectSpec(type: Type, language: Language): HybridObjectSpec {
  if (isHybridView(type)) {
    const symbol = type.getAliasSymbolOrThrow()
    const name = symbol.getEscapedName()

    // It's a Hybrid View - the `Props & Methods` types are just intersected together.
    const unions = type.getIntersectionTypes()
    const props = unions.find((t) => isHybridViewProps(t))
    const methods = unions.find((t) => isHybridViewMethods(t))
    if (props == null)
      throw new Error(
        `Props cannot be null! ${name}<...> (HybridView) requires type arguments.`
      )
    const propsSpec = getHybridObjectSpec(props, language)
    const methodsSpec =
      methods != null ? getHybridObjectSpec(methods, language) : undefined

    return {
      baseTypes: [],
      isHybridView: true,
      language: language,
      methods: methodsSpec?.methods ?? [],
      properties: propsSpec.properties,
      name: name,
    }
  }

  const symbol = type.getSymbolOrThrow()
  const name = symbol.getEscapedName()

  const properties: Property[] = []
  const methods: Method[] = []
  for (const prop of type.getProperties()) {
    const declarations = prop.getDeclarations()
    if (declarations.length > 1) {
      throw new Error(
        `${name}: Function overloading is not supported! (In "${prop.getName()}")`
      )
    }
    let declaration = declarations[0]
    if (declaration == null) {
      throw new Error(
        `${name}: Property "${prop.getName()}" does not have a type declaration!`
      )
    }

    const parent = declaration.getParentOrThrow().getType()

    if (parent === type) {
      // it's an own property. declared literally here. fine.
    } else if (
      extendsHybridObject(parent, true) ||
      isDirectlyHybridObject(parent)
    ) {
      // it's coming from a base class that is already a HybridObject. We can grab this via inheritance.
      // don't generate this property natively.
      continue
    } else {
      // it's coming from any TypeScript type that is not a HybridObject.
      // Maybe just a literal interface, then we copy over the props.
    }

    if (Node.isPropertySignature(declaration)) {
      const t = declaration.getType()
      const propType = createType(
        language,
        t,
        prop.isOptional() || t.isNullable()
      )
      properties.push(
        new Property(prop.getName(), propType, declaration.isReadonly())
      )
    } else if (Node.isMethodSignature(declaration)) {
      const returnType = declaration.getReturnType()
      const methodReturnType = createType(
        language,
        returnType,
        returnType.isNullable()
      )
      const methodParameters = declaration
        .getParameters()
        .map((p) => new Parameter(p, language))
      methods.push(
        new Method(prop.getName(), methodReturnType, methodParameters)
      )
    } else {
      throw new Error(
        `${name}: Property "${prop.getName()}" is neither a property, nor a method!`
      )
    }
  }

  const bases = getBaseTypes(type)
    .filter((t) => isAnyHybridSubclass(t))
    .map((t) => getHybridObjectSpec(t, language))

  const spec: HybridObjectSpec = {
    language: language,
    name: name,
    properties: properties,
    methods: methods,
    baseTypes: bases,
    isHybridView: isHybridView(type),
  }
  return spec
}

function generateCppFiles(spec: HybridObjectSpec): SourceFile[] {
  const cppFiles = createCppHybridObject(spec)
  return cppFiles
}

function generateSwiftFiles(spec: HybridObjectSpec): SourceFile[] {
  // 1. Always generate a C++ spec for the shared layer and type declarations (enums, interfaces, ...)
  const cppFiles = generateCppFiles(spec)
  // 2. Generate Swift specific files and potentially a C++ binding layer
  const swiftFiles = createSwiftHybridObject(spec)
  return [...cppFiles, ...swiftFiles]
}

function generateKotlinFiles(spec: HybridObjectSpec): SourceFile[] {
  // 1. Always generate a C++ spec for the shared layer and type declarations (enums, interfaces, ...)
  const cppFiles = generateCppFiles(spec)
  // 2. Generate Kotlin specific files and potentially a C++ binding layer
  const kotlinFiles = createKotlinHybridObject(spec)
  return [...cppFiles, ...kotlinFiles]
}
