/**
 Each module that has a view manager definition needs to be wrapped by `RCTViewManager`.
 Unfortunately, we can't use just one class because React Native checks for duplicated classes.
 We're generating its subclasses in runtime as a workaround.
 */
@objc
public class ViewModuleWrapper: RCTViewManager {
  /**
   A reference to the module holder that stores the module definition.
   */
  let wrappedModuleHolder: ModuleHolder

  /**
   The designated initializer. At first, we use this base class to hide `ModuleHolder` from Objective-C runtime.
   */
  public init(_ wrappedModuleHolder: ModuleHolder) {
    self.wrappedModuleHolder = wrappedModuleHolder
  }

  /**
   Convenience initializer intended to be run on the subclasses and from Objective-C.
   Initializes an object based on another one initialized with the base class.
   */
  @objc
  public convenience init(from module: ViewModuleWrapper) {
    self.init(module.wrappedModuleHolder)
  }

  /**
   Returns the original name of the wrapped module.
   */
  @objc
  public func name() -> String {
    return wrappedModuleHolder.name
  }

  /**
   Static function that returns the prefixed name of the wrapped module.
   Here is just a stub that must be overriden in the autogenerated subclasses (see `createViewModuleWrapperClass`).
   */
  @objc
  dynamic public override class func moduleName() -> String! {
    fatalError("Something unexpected has happened. The original implementation of `moduleName` method must be replaced in runtime (see `viewModuleWrapperClass` below).")
  }

  /**
   Creates a view from the wrapped module.
   */
  @objc
  public override func view() -> UIView! {
    guard let view = wrappedModuleHolder.definition.viewManager?.createView() else {
      fatalError("Module `\(wrappedModuleHolder.name)` doesn't define the view manager nor view factory.")
    }
    return view
  }

  /**
   The config for `proxiedProperties` prop. In Objective-C style, this function is generated by `RCT_CUSTOM_VIEW_PROPERTY` macro.
   */
  @objc
  public class func propConfig_proxiedProperties() -> [String] {
    return ["NSDictionary", "__custom__"];
  }

  /**
   The setter for `proxiedProperties` prop. In Objective-C style, this function is generated by `RCT_CUSTOM_VIEW_PROPERTY` macro.
   */
  @objc
  public func set_proxiedProperties(_ json: Any?, forView view: UIView, withDefaultView defaultView: UIView) {
    guard let json = json as? [String: Any?],
          let props = wrappedModuleHolder.definition.viewManager?.propsDict() else {
      return
    }
    for (key, value) in json {
      if let prop = props[key] {
        prop.set(value: value, onView: view)
      }
    }
  }

  /**
   Creates a subclass of `ViewModuleWrapper` in runtime. The new class overrides `moduleName` stub.
   */
  @objc
  public static func createViewModuleWrapperClass(viewName: String) -> ViewModuleWrapper.Type? {
    // We're namespacing the view name so we know it uses our architecture.
    let prefixedViewName = "ViewManagerAdapter_\(viewName)"

    return prefixedViewName.withCString { viewNamePtr in
      // Create a new meta class that inherits from `ViewModuleWrapper`. The class name passed here, doesn't work for Swift classes,
      // so we also have to override `moduleName` class method.
      let wrapperClass: AnyClass? = objc_allocateClassPair(ViewModuleWrapper.self, viewNamePtr, 0)

      // Prepare the selector and new implementation that returns correct view name.
      let moduleNameSel: Selector = #selector(moduleName)
      let moduleNameBlock: @convention(block) () -> String = { prefixedViewName }
      let moduleNameImp: IMP = imp_implementationWithBlock(moduleNameBlock)

      // Get the `moduleName` class method and replace its implementation.
      // We can assume the method already exists as it's defined in `ViewModuleManager` superclass,
      // but if not then just add it with the proper implementation.
      if let moduleNameMethod = class_getClassMethod(wrapperClass, moduleNameSel) {
        method_setImplementation(moduleNameMethod, moduleNameImp)
      } else {
        class_addMethod(object_getClass(wrapperClass), moduleNameSel, moduleNameImp, "@@:")
      }

      return wrapperClass as? ViewModuleWrapper.Type
    }
  }
}
