16ad80deeSTomasz Sapeta // Copyright 2022-present 650 Industries. All rights reserved.
26ad80deeSTomasz Sapeta 
3d2f2d83cSTomasz Sapeta // MARK: - Arguments
4d2f2d83cSTomasz Sapeta 
5c4fc6f47STomasz Sapeta /**
69b8bcdc4STomasz Sapeta  Tries to cast a given value to the type that is wrapped by the dynamic type.
7d2f2d83cSTomasz Sapeta  - Parameters:
89b8bcdc4STomasz Sapeta   - value: A value to be cast. If it's a ``JavaScriptValue``, it's first unpacked to the raw value.
99b8bcdc4STomasz Sapeta   - type: Something that implements ``AnyDynamicType`` and knows how to cast the argument.
104cafe471STomasz Sapeta  - Returns: A new value converted according to the dynamic type.
114cafe471STomasz Sapeta  - Throws: Rethrows various exceptions that could be thrown by the dynamic types.
12c4fc6f47STomasz Sapeta  */
castnull13553b6180STomasz Sapeta internal func cast(_ value: Any, toType type: AnyDynamicType, appContext: AppContext) throws -> Any {
14*45a388e1SŁukasz Kosmaty   if let dynamicJSType = type as? DynamicJavaScriptType, dynamicJSType.equals(~JavaScriptValue.self)  {
15*45a388e1SŁukasz Kosmaty     return value
16*45a388e1SŁukasz Kosmaty   }
17002d516eSTomasz Sapeta   if !(type is DynamicTypedArrayType), let value = value as? JavaScriptValue {
18553b6180STomasz Sapeta     return try type.cast(value.getRaw(), appContext: appContext)
1978516026STomasz Sapeta   }
20553b6180STomasz Sapeta   return try type.cast(value, appContext: appContext)
21c4fc6f47STomasz Sapeta }
22c4fc6f47STomasz Sapeta 
23d2f2d83cSTomasz Sapeta /**
249b8bcdc4STomasz Sapeta  Tries to cast the given arguments to the types expected by the function.
25d2f2d83cSTomasz Sapeta  - Parameters:
26d2f2d83cSTomasz Sapeta    - arguments: An array of arguments to be cast.
279b8bcdc4STomasz Sapeta    - function: A function for which to cast the arguments.
28553b6180STomasz Sapeta    - appContext: A context of the app.
29d2f2d83cSTomasz Sapeta  - Returns: An array of arguments after casting. Its size is the same as the input arrays.
309b8bcdc4STomasz Sapeta  - Throws: `InvalidArgsNumberException` when the number of arguments is not equal to the actual number
319b8bcdc4STomasz Sapeta  of function's arguments (without an owner and promise). Rethrows exceptions thrown by `cast(_:toType:)`.
32d2f2d83cSTomasz Sapeta  */
castnull33553b6180STomasz Sapeta internal func cast(arguments: [Any], forFunction function: AnyFunction, appContext: AppContext) throws -> [Any] {
34d2f2d83cSTomasz Sapeta   return try arguments.enumerated().map { index, argument in
353eaf6df6STomasz Sapeta     let argumentType = function.dynamicArgumentTypes[index]
36d2f2d83cSTomasz Sapeta 
37d2f2d83cSTomasz Sapeta     do {
38553b6180STomasz Sapeta       return try cast(argument, toType: argumentType, appContext: appContext)
39d2f2d83cSTomasz Sapeta     } catch {
40d2f2d83cSTomasz Sapeta       throw ArgumentCastException((index: index, type: argumentType)).causedBy(error)
41d2f2d83cSTomasz Sapeta     }
42d2f2d83cSTomasz Sapeta   }
43d2f2d83cSTomasz Sapeta }
44d2f2d83cSTomasz Sapeta 
459b8bcdc4STomasz Sapeta /**
463eaf6df6STomasz Sapeta  Casts an array of JavaScript values to non-JavaScript types.
473eaf6df6STomasz Sapeta  */
castnull483eaf6df6STomasz Sapeta internal func cast(jsValues: [Any], forFunction function: AnyFunction, appContext: AppContext) throws -> [Any] {
493eaf6df6STomasz Sapeta   // TODO: Replace `[Any]` with `[JavaScriptValue]` once we make sure only JS values are passed here
503eaf6df6STomasz Sapeta   return try jsValues.enumerated().map { index, jsValue in
513eaf6df6STomasz Sapeta     let type = function.dynamicArgumentTypes[index]
523eaf6df6STomasz Sapeta 
533eaf6df6STomasz Sapeta     do {
543eaf6df6STomasz Sapeta       // Temporarily some values might already be cast to primitive types, so make sure we cast only `JavaScriptValue` and leave the others as they are.
553eaf6df6STomasz Sapeta       if let jsValue = jsValue as? JavaScriptValue {
563eaf6df6STomasz Sapeta         return try type.cast(jsValue: jsValue, appContext: appContext)
573eaf6df6STomasz Sapeta       } else {
583eaf6df6STomasz Sapeta         return jsValue
593eaf6df6STomasz Sapeta       }
603eaf6df6STomasz Sapeta     } catch {
613eaf6df6STomasz Sapeta       throw ArgumentCastException((index: index, type: type)).causedBy(error)
623eaf6df6STomasz Sapeta     }
633eaf6df6STomasz Sapeta   }
643eaf6df6STomasz Sapeta }
653eaf6df6STomasz Sapeta 
663eaf6df6STomasz Sapeta /**
673eaf6df6STomasz Sapeta  Validates whether the number of received arguments is enough to call the given function.
683eaf6df6STomasz Sapeta  Throws `InvalidArgsNumberException` otherwise.
693eaf6df6STomasz Sapeta  */
validateArgumentsNumbernull703eaf6df6STomasz Sapeta internal func validateArgumentsNumber(function: AnyFunction, received: Int) throws {
713eaf6df6STomasz Sapeta   let argumentsCount = function.argumentsCount
723eaf6df6STomasz Sapeta   let requiredArgumentsCount = function.requiredArgumentsCount
733eaf6df6STomasz Sapeta 
743eaf6df6STomasz Sapeta   if received < requiredArgumentsCount || received > argumentsCount {
753eaf6df6STomasz Sapeta     throw InvalidArgsNumberException((
763eaf6df6STomasz Sapeta       received: received,
773eaf6df6STomasz Sapeta       expected: argumentsCount,
783eaf6df6STomasz Sapeta       required: requiredArgumentsCount
793eaf6df6STomasz Sapeta     ))
803eaf6df6STomasz Sapeta   }
813eaf6df6STomasz Sapeta }
823eaf6df6STomasz Sapeta 
833eaf6df6STomasz Sapeta /**
844a8d2497STomasz Sapeta  Ensures the provided array of arguments matches the number of arguments expected by the function.
854a8d2497STomasz Sapeta  - If the function takes the owner, it's added to the beginning.
864a8d2497STomasz Sapeta  - If the array is still too small, missing arguments are very likely to be optional so it puts `nil` in their place.
879b8bcdc4STomasz Sapeta  */
883eaf6df6STomasz Sapeta internal func concat(
893eaf6df6STomasz Sapeta   arguments: [Any],
903eaf6df6STomasz Sapeta   withOwner owner: AnyObject?,
913eaf6df6STomasz Sapeta   withPromise promise: Promise?,
923eaf6df6STomasz Sapeta   forFunction function: AnyFunction,
933eaf6df6STomasz Sapeta   appContext: AppContext
943eaf6df6STomasz Sapeta ) -> [Any] {
954a8d2497STomasz Sapeta   var result = arguments
964a8d2497STomasz Sapeta 
973eaf6df6STomasz Sapeta   if function.takesOwner {
984a8d2497STomasz Sapeta     result = [owner] + arguments
999b8bcdc4STomasz Sapeta   }
1004a8d2497STomasz Sapeta   if arguments.count < function.argumentsCount {
1014a8d2497STomasz Sapeta     result += Array(repeating: Any?.none as Any, count: function.argumentsCount - arguments.count)
1024a8d2497STomasz Sapeta   }
1033eaf6df6STomasz Sapeta   // Add promise to the array of arguments if necessary.
1043eaf6df6STomasz Sapeta   if let promise {
1053eaf6df6STomasz Sapeta     result += [promise]
1063eaf6df6STomasz Sapeta   }
1074a8d2497STomasz Sapeta   return result
1089b8bcdc4STomasz Sapeta }
1099b8bcdc4STomasz Sapeta 
110447e3428STomasz Sapeta // MARK: - Exceptions
111447e3428STomasz Sapeta 
1124a8d2497STomasz Sapeta internal class InvalidArgsNumberException: GenericException<(received: Int, expected: Int, required: Int)> {
113d2f2d83cSTomasz Sapeta   override var reason: String {
1144a8d2497STomasz Sapeta     if param.required < param.expected {
1154a8d2497STomasz Sapeta       return "Received \(param.received) arguments, but \(param.expected) was expected and at least \(param.required) is required"
1164a8d2497STomasz Sapeta     } else {
1174a8d2497STomasz Sapeta       return "Received \(param.received) arguments, but \(param.expected) was expected"
1184a8d2497STomasz Sapeta     }
119d2f2d83cSTomasz Sapeta   }
120d2f2d83cSTomasz Sapeta }
121d2f2d83cSTomasz Sapeta 
1224cafe471STomasz Sapeta internal class ArgumentCastException: GenericException<(index: Int, type: AnyDynamicType)> {
123d2f2d83cSTomasz Sapeta   override var reason: String {
12432943018STomasz Sapeta     "The \(formatOrdinalNumber(param.index + 1)) argument cannot be cast to type \(param.type.description)"
12532943018STomasz Sapeta   }
12632943018STomasz Sapeta 
formatOrdinalNumbernull12732943018STomasz Sapeta   func formatOrdinalNumber(_ number: Int) -> String {
12832943018STomasz Sapeta     let formatter = NumberFormatter()
12932943018STomasz Sapeta     formatter.numberStyle = .ordinal
13032943018STomasz Sapeta     formatter.locale = Locale(identifier: "en_US")
13132943018STomasz Sapeta     return formatter.string(from: NSNumber(value: number)) ?? ""
132d2f2d83cSTomasz Sapeta   }
133d2f2d83cSTomasz Sapeta }
134d2f2d83cSTomasz Sapeta 
1356ad80deeSTomasz Sapeta private class ModuleUnavailableException: GenericException<String> {
1366ad80deeSTomasz Sapeta   override var reason: String {
137d33619e0STomasz Sapeta     "Module '\(param)' is no longer available"
1386ad80deeSTomasz Sapeta   }
1396ad80deeSTomasz Sapeta }
140