import ExpoModulesJSI

/**
 A protocol that allows initializing the object with a dictionary.
 For supported field types, see https://docs.expo.dev/modules/module-api/#argument-types
 */
public protocol Record: Convertible {
  /**
   The dictionary type that the record can be created from or converted back.
   */
  typealias Dict = [String: Any]

  /**
   The default initializer. It enforces the structs not to have any uninitialized properties.
   */
  nonisolated init()

  /**
   Initializes a record from given dictionary. Only members wrapped by `@Field` will be set in the object.
   */
  init(from: Dict, appContext: AppContext) throws

  /**
   Converts the record back to the dictionary. Only members wrapped by `@Field` will be set in the dictionary.
   */
  func toDictionary(appContext: AppContext?) -> Dict
}

internal protocol RecordJavaScriptValueConvertible {
  @JavaScriptActor
  func toJSValue(appContext: AppContext) throws -> JavaScriptValue
}

/**
 Provides the default implementation of `Record` protocol.
 */
public extension Record {
  static func convert(from value: Any?, appContext: AppContext) throws -> Self {
    if let value = value as? Dict {
      return try Self(from: value, appContext: appContext)
    }
    // It's possible that the current implementation tries to convert a value that is already of the desired type.
    // Handle that gracefully instead of throwing an exception.
    if let record = value as? Self {
      return record
    }
    throw Conversions.ConvertingException<Self>(value)
  }

  init(from dict: Dict, appContext: AppContext) throws {
    self.init()
    try update(withDict: dict, appContext: appContext)
  }

  func update(withDict dict: Dict, appContext: AppContext) throws {
    let dictKeys = dict.keys

    try fieldsOf(self).forEach { field in
      guard let key = field.key else {
        // This should never happen, but just in case skip fields without the key.
        return
      }
      if dictKeys.contains(key) || field.isRequired {
        try field.set(dict[key], appContext: appContext)
      }
    }
  }

  @JavaScriptActor
  func update(withObject object: borrowing JavaScriptObject, appContext: AppContext) throws {
    // Using a set keeps declared-field lookups O(1) when selectively hydrating the record.
    let propertyNames = Set(object.getPropertyNames())

    try fieldsOf(self).forEach { field in
      guard let key = field.key else {
        return
      }
      if propertyNames.contains(key) {
        let property = object.getProperty(key)

        if property.isUndefined() {
          if field.isRequired {
            try field.set(nil, appContext: appContext)
          }
          return
        }
        try field.set(jsValue: property, appContext: appContext)
      } else if field.isRequired {
        try field.set(nil, appContext: appContext)
      }
    }
  }

  func toDictionary(appContext: AppContext? = nil) -> Dict {
    return fieldsOf(self).reduce(into: Dict()) { result, field in
      if let key = field.key {
        result[key] = Conversions.convertFunctionResult(field.get(), appContext: appContext)
      }
    }
  }

  static func convertResult(_ result: Any, appContext: AppContext) throws -> Any {
    if let value = result as? Record {
      return value.toDictionary(appContext: appContext)
    }
    return result
  }

  @JavaScriptActor
  func toJSValue(appContext: AppContext) throws -> JavaScriptValue {
    let object = try appContext.runtime.createObject()

    for field in fieldsOf(self) {
      guard let key = field.key else {
        continue
      }
      let value = try recordFieldValueToJSValue(field.get(), dynamicType: field.fieldType, appContext: appContext)
      object.setProperty(key, value: value)
    }
    return object.asValue()
  }
}

/**
 Recursively collects all children from a Mirror, including inherited properties from superclasses.
 */
internal func allMirrorChildren(_ mirror: Mirror) -> [Mirror.Child] {
  var children: [Mirror.Child] = Array(mirror.children)
  if let superclassMirror = mirror.superclassMirror {
    children.append(contentsOf: allMirrorChildren(superclassMirror))
  }
  return children
}

/**
 Returns an array of fields found in record's mirror. If the field is missing the `key`,
 it gets assigned to the property label, so after all it's safe to enforce unwrapping it (using `key!`).
 This function now supports inheritance by recursively traversing the superclass hierarchy.
 */
internal func fieldsOf(_ record: Record) -> [AnyFieldInternal] {
  let mirror = Mirror(reflecting: record)
  return allMirrorChildren(mirror).compactMap { (label: String?, value: Any) in
    guard let field = value as? AnyFieldInternal, let key = field.key ?? convertLabelToKey(label) else {
      return nil
    }
    field.withOptions { options in
      let alreadyKeyed = options.contains { $0.rawValue == FieldOption.keyed("").rawValue }
      if !alreadyKeyed {
        options.insert(.keyed(key))
      }
    }
    return field
  }
}

/**
 Converts mirror's label to field's key by dropping the "_" prefix from wrapped property label.
 */
internal func convertLabelToKey(_ label: String?) -> String? {
  return (label != nil && label!.starts(with: "_")) ? String(label!.dropFirst()) : label
}

@JavaScriptActor
internal func recordFieldValueToJSValue(
  _ value: Any,
  dynamicType: AnyDynamicType? = nil,
  appContext: AppContext
) throws -> JavaScriptValue {
  let convertedValue: Any

  if let dynamicType {
    return try dynamicType.convertToJS(value, appContext: appContext)
  }

  convertedValue = Conversions.convertFunctionResult(value, appContext: appContext)
  if Optional.isNil(convertedValue) {
    return .null
  }
  if let jsValue = convertedValue as? JavaScriptValue {
    return jsValue
  }
  return try Conversions.unknownToJavaScriptValue(convertedValue, appContext: appContext)
}
