// MIT License
//
// Copyright (c) 2020 linhey
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.

// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.

import Foundation

public class Invocation {
    
    private let target: NSObject
    private let selector: Selector
    
    var invocation: NSObject?
    
    var numberOfArguments: Int = 0
    var returnLength: Int = 0
    var returnType: UnsafePointer<CChar>?
    var returnTypeString: String? {
        guard let returnType = returnType else { return nil }
        return String(cString: returnType)
    }
    var returnsObject: Bool {
        /// `@` is the type encoding for an object
        returnTypeString == "@"
    }
    var returnsAny: Bool {
        /// `v` is the type encoding for Void
        returnTypeString != "v"
    }
    lazy var returnedObject: AnyObject? = {
        returnedObjectValue()
    }()
    private(set) var isInvoked: Bool = false
    
    public init(target: NSObject, selector: Selector) throws {
        self.target = target
        self.selector = selector
        try initialize()
    }
    
    private func initialize() throws {
        /// `NSMethodSignature *methodSignature = [target methodSignatureForSelector: selector]`
        let methodSignature: NSObject
        do {
            let selector = NSSelectorFromString("methodSignatureForSelector:")
            let signature = (@convention(c)(NSObject, Selector, Selector) -> Any).self
            let method = unsafeBitCast(target.method(for: selector), to: signature)
            guard let result = method(target, selector, self.selector) as? NSObject else {
                let error = Error.unrecognizedSelector(type(of: target), self.selector)
                throw error
            }
            methodSignature = result
        }
        
        /// `numberOfArguments = methodSignature.numberOfArguments`
        self.numberOfArguments = methodSignature.value(forKeyPath: "numberOfArguments") as? Int ?? 0
        
        /// `methodReturnLength = methodSignature.methodReturnLength`
        self.returnLength = methodSignature.value(forKeyPath: "methodReturnLength") as? Int ?? 0
        
        /// `methodReturnType = methodSignature.methodReturnType`
        let methodReturnType: UnsafePointer<CChar>
        do {
            let selector = NSSelectorFromString("methodReturnType")
            let signature = (@convention(c)(NSObject, Selector) -> UnsafePointer<CChar>).self
            let method = unsafeBitCast(methodSignature.method(for: selector), to: signature)
            methodReturnType = method(methodSignature, selector)
        }
        self.returnType = methodReturnType
        
        /// `NSInvocation *invocation = [NSInvocation invocationWithMethodSignature: methodSignature]`
        let invocation: NSObject
        do {
            let NSInvocation = NSClassFromString("NSInvocation") as AnyObject
            let selector = NSSelectorFromString("invocationWithMethodSignature:")
            let signature = (@convention(c)(AnyObject, Selector, AnyObject) -> AnyObject).self
            let method = unsafeBitCast(NSInvocation.method(for: selector), to: signature)
            guard let result = method(NSInvocation, selector, methodSignature) as? NSObject else {
                let error = Error.unrecognizedSelector(type(of: target), self.selector)
                throw error
            }
            invocation = result
        }
        self.invocation = invocation
        
        /// `invocation.selector = selector`
        do {
            let selector = NSSelectorFromString("setSelector:")
            let signature = (@convention(c)(NSObject, Selector, Selector) -> Void).self
            let method = unsafeBitCast(invocation.method(for: selector), to: signature)
            method(invocation, selector, self.selector)
        }
        
        /// `[invocation retainArguments]`
        do {
            let selector = NSSelectorFromString("retainArguments")
            let signature = (@convention(c)(NSObject, Selector) -> Void).self
            let method = unsafeBitCast(invocation.method(for: selector), to: signature)
            method(invocation, selector)
        }
    }
    
}

public extension Invocation {
    
    func setArgument(_ argument: Any?, at index: NSInteger) {
        guard let invocation = invocation else { return }
        
        /// `[invocation setArgument:&argument atIndex:i + 2]`
        let selector = NSSelectorFromString("setArgument:atIndex:")
        let signature = (@convention(c)(NSObject, Selector, UnsafeRawPointer, Int) -> Void).self
        let method = unsafeBitCast(invocation.method(for: selector), to: signature)
        
        if let valueArgument = argument as? NSValue {
            /// Get the type byte size
            let typeSize = UnsafeMutablePointer<Int>.allocate(capacity: 1)
            defer { typeSize.deallocate() }
            NSGetSizeAndAlignment(valueArgument.objCType, typeSize, nil)
            
            /// Get the actual value
            let buffer = UnsafeMutablePointer<Int8>.allocate(capacity: typeSize.pointee)
            defer { buffer.deallocate() }
            valueArgument.getValue(buffer)
            
            method(invocation, selector, buffer, index)
        } else {
            withUnsafePointer(to: argument) { pointer in
                method(invocation, selector, pointer, index)
            }
        }
    }
    
    func invoke() {
        guard let invocation = invocation, !isInvoked else { return }
        isInvoked = true
        
        /// `[invocation invokeWithTarget: target]`
        do {
            let selector = NSSelectorFromString("invokeWithTarget:")
            let signature = (@convention(c)(NSObject, Selector, AnyObject) -> Void).self
            let method = unsafeBitCast(invocation.method(for: selector), to: signature)
            method(invocation, selector, target)
        }
    }
    
    func getReturnValue<T>(result: inout T) {
        guard let invocation = invocation else { return }
        
        /// `[invocation getReturnValue: returnValue]`
        do {
            let selector = NSSelectorFromString("getReturnValue:")
            let signature = (@convention(c)(NSObject, Selector, UnsafeMutableRawPointer) -> Void).self
            let method = unsafeBitCast(invocation.method(for: selector), to: signature)
            withUnsafeMutablePointer(to: &result) { pointer in
                method(invocation, selector, pointer)
            }
        }
    }
    
    private func returnedObjectValue() -> AnyObject? {
        guard returnsObject, returnLength > 0 else {
            return nil
        }
        
        var result: AnyObject?
        
        getReturnValue(result: &result)
        
        guard let object = result else {
            return nil
        }
        
        /// Take the ownership of the initialized objects to ensure they're deallocated properly.
        if isRetainingMethod() {
            return Unmanaged.passRetained(object).takeRetainedValue()
        }
        
        /// `NSInvocation.getReturnValue()` doesn't give us the ownership of the returned object, but the compiler
        /// tries to release this object anyway. So, we are retaining it to balance with the compiler's release.
        return Unmanaged.passRetained(object).takeUnretainedValue()
    }
    
    private func isRetainingMethod() -> Bool {
        /// Refer to: https://bit.ly/308okXm
        let selector = NSStringFromSelector(self.selector)
        return selector == "alloc" ||
            selector.hasPrefix("new") ||
            selector.hasPrefix("copy") ||
            selector.hasPrefix("mutableCopy")
    }
}

public extension Invocation {
    
    enum Error: CustomNSError {
        case unrecognizedSelector(_ classType: AnyClass, _ selector: Selector)
        
        public static var errorDomain: String { String(describing: Invocation.self) }
        
        public var errorCode: Int {
            switch self {
            case .unrecognizedSelector:
                return 404
            }
        }
        
        public var errorUserInfo: [String: Any] {
            var message: String
            switch self {
            case .unrecognizedSelector(let classType, let selector):
                message = "'\(String(describing: classType))' doesn't recognize selector '\(selector)'"
            }
            return [NSLocalizedDescriptionKey: message]
        }
    }
    
}
