import Foundation
import UIKit
import MetalKit
import AVFoundation
import ObjectiveC

@objcMembers
public final class LNVAPLogger: NSObject {
    public typealias LNLogHandler = (_ level: Int, _ file: String, _ line: Int, _ function: String, _ module: String, _ message: String) -> Void

    private static let lock = NSRecursiveLock()
    private static var handler: LNLogHandler?

    @objc(registerLogHandler:)
    public static func registerLogHandler(_ handler: @escaping LNLogHandler) {
        lock.lock()
        self.handler = handler
        lock.unlock()
    }

    // Keep the external logging hook as a stable LN logger entry point.
    @objc(registerExternalLog:)
    public static func registerExternalLog(_ handler: @escaping LNLogHandler) {
        registerLogHandler(handler)
    }

    @objc(clearLogHandler)
    public static func clearLogHandler() {
        lock.lock()
        handler = nil
        lock.unlock()
    }

    @objc(logLevel:file:line:func:module:message:)
    public static func log(level: Int, file: String, line: Int, func function: String, module: String, message: String) {
        let sanitized = message.replacingOccurrences(of: "%", with: "")

        lock.lock()
        let current = handler
        lock.unlock()

        if let current {
            current(level, file, line, function, module, sanitized)
            return
        }

#if DEBUG
        let fileName = (file as NSString).lastPathComponent
        NSLog("<\(level)> \(fileName)(\(line)):\(function) [\(module)] - \(sanitized)")
#endif
    }
}

@objcMembers
public final class LNVAPSafeMutableArray: NSObject {
    private let lock = NSRecursiveLock()
    private var storage: NSMutableArray

    public override init() {
        self.storage = NSMutableArray()
        super.init()
    }

    @objc(initWithCapacity:)
    public init(capacity: Int) {
        self.storage = NSMutableArray(capacity: capacity)
        super.init()
    }

    @objc(initWithArray:)
    public init(array: [Any]) {
        self.storage = NSMutableArray(array: array)
        super.init()
    }

    @objc(count)
    public var count: Int {
        lock.lock()
        defer { lock.unlock() }
        return storage.count
    }

    @objc(objectAtIndex:)
    public func object(at index: Int) -> Any {
        lock.lock()
        defer { lock.unlock() }
        return storage[index]
    }

    @objc(firstObject)
    public var firstObject: Any? {
        lock.lock()
        defer { lock.unlock() }
        return storage.firstObject
    }

    @objc(lastObject)
    public var lastObject: Any? {
        lock.lock()
        defer { lock.unlock() }
        return storage.lastObject
    }

    @objc(addObject:)
    public func add(_ object: Any) {
        lock.lock()
        storage.add(object)
        lock.unlock()
    }

    @objc(insertObject:atIndex:)
    public func insert(_ object: Any, at index: Int) {
        lock.lock()
        storage.insert(object, at: index)
        lock.unlock()
    }

    @objc(removeObjectAtIndex:)
    public func removeObject(at index: Int) {
        lock.lock()
        storage.removeObject(at: index)
        lock.unlock()
    }

    @objc(removeLastObject)
    public func removeLastObject() {
        lock.lock()
        storage.removeLastObject()
        lock.unlock()
    }

    @objc(removeAllObjects)
    public func removeAllObjects() {
        lock.lock()
        storage.removeAllObjects()
        lock.unlock()
    }

    @objc(containsObject:)
    public func contains(_ object: Any) -> Bool {
        lock.lock()
        defer { lock.unlock() }
        return storage.contains(object)
    }

    @objc(allObjects)
    public func allObjects() -> [Any] {
        lock.lock()
        defer { lock.unlock() }
        return storage.copy() as? [Any] ?? []
    }
}

@objcMembers
public final class LNVAPSafeMutableDictionary: NSObject {
    private let lock = NSRecursiveLock()
    private var storage: NSMutableDictionary

    public override init() {
        self.storage = NSMutableDictionary()
        super.init()
    }

    @objc(initWithCapacity:)
    public init(capacity: Int) {
        self.storage = NSMutableDictionary(capacity: capacity)
        super.init()
    }

    @objc(initWithDictionary:)
    public init(dictionary: [AnyHashable: Any]) {
        self.storage = NSMutableDictionary(dictionary: dictionary)
        super.init()
    }

    @objc(count)
    public var count: Int {
        lock.lock()
        defer { lock.unlock() }
        return storage.count
    }

    @objc(objectForKey:)
    public func object(forKey key: NSCopying) -> Any? {
        lock.lock()
        defer { lock.unlock() }
        return storage.object(forKey: key)
    }

    @objc(setObject:forKey:)
    public func setObject(_ object: Any, forKey key: NSCopying) {
        lock.lock()
        storage.setObject(object, forKey: key)
        lock.unlock()
    }

    @objc(removeObjectForKey:)
    public func removeObject(forKey key: NSCopying) {
        lock.lock()
        storage.removeObject(forKey: key)
        lock.unlock()
    }

    @objc(removeAllObjects)
    public func removeAllObjects() {
        lock.lock()
        storage.removeAllObjects()
        lock.unlock()
    }

    @objc(allKeys)
    public var allKeys: [Any] {
        lock.lock()
        defer { lock.unlock() }
        return storage.allKeys
    }

    @objc(allValues)
    public var allValues: [Any] {
        lock.lock()
        defer { lock.unlock() }
        return storage.allValues
    }

    @objc(dictionaryRepresentation)
    public func dictionaryRepresentation() -> [AnyHashable: Any] {
        lock.lock()
        defer { lock.unlock() }
        return storage.copy() as? [AnyHashable: Any] ?? [:]
    }
}

@objcMembers
public final class LNVAPWeakProxy: NSObject {
    private weak var target: NSObjectProtocol?

    @objc(initWithTarget:)
    public init(target: NSObjectProtocol?) {
        self.target = target
        super.init()
    }

    @objc(proxyWithTarget:)
    public static func proxy(with target: NSObjectProtocol?) -> LNVAPWeakProxy {
        LNVAPWeakProxy(target: target)
    }

    public override func forwardingTarget(for aSelector: Selector!) -> Any? {
        target
    }

    public override func responds(to aSelector: Selector!) -> Bool {
        target?.responds(to: aSelector) ?? false
    }
}

@objcMembers
public final class LNVAPMetalShaderFunctionLoader: NSObject {
    private let device: MTLDevice
    private var defaultLibrary: MTLLibrary?
    private var fallbackLibrary: MTLLibrary?

    @objc(initWithDevice:)
    public init(device: MTLDevice) {
        self.device = device
        super.init()
    }

    @objc(loadFunctionWithName:)
    public func loadFunction(withName functionName: String) -> MTLFunction? {
        if defaultLibrary == nil {
            if let path = Bundle(for: Self.self).path(forResource: "default", ofType: "metallib") {
                defaultLibrary = try? device.makeLibrary(filepath: path)
            }
            if defaultLibrary == nil {
                defaultLibrary = device.makeDefaultLibrary()
            }
        }
        if let function = defaultLibrary?.makeFunction(name: functionName) {
            return function
        }
        if fallbackLibrary == nil {
            fallbackLibrary = try? device.makeLibrary(source: Self.fallbackShaderSource, options: nil)
        }
        return fallbackLibrary?.makeFunction(name: functionName)
    }

    private static let fallbackShaderSource = """
    #include <metal_stdlib>
    #import <simd/simd.h>
    using namespace metal;

    typedef struct {
        packed_float4 position;
        packed_float2 textureColorCoordinate;
        packed_float2 textureAlphaCoordinate;
    } QGHWDVertex;

    typedef struct {
        packed_float4 position;
        packed_float2 textureColorCoordinate;
        packed_float2 textureAlphaCoordinate;
        packed_float2 textureMaskCoordinate;
    } QGVAPVertex;

    struct ColorParameters {
        matrix_float3x3 matrix;
        packed_float2 offset;
    };

    struct MaskParameters {
        matrix_float3x3 weightMatrix;
        int coreSize;
        float texelOffset;
    };

    typedef struct {
        int needOriginRGB;
        packed_float4 fillColor;
    } VapAttachmentFragmentParameter;

    typedef struct {
        packed_float4 position;
        packed_float2 sourceTextureCoordinate;
        packed_float2 maskTextureCoordinate;
    } QGHWDAttachmentVertex;

    typedef struct {
        float4 clipSpacePostion [[ position ]];
        float2 textureColorCoordinate;
        float2 textureAlphaCoordinate;
    } HWDRasterizerData;

    typedef struct {
        float4 clipSpacePostion [[ position ]];
        float2 textureColorCoordinate;
        float2 textureAlphaCoordinate;
        float2 textureMaskCoordinate;
    } VAPRasterizerData;

    typedef struct {
        float4 position [[ position ]];
        float2 sourceTextureCoordinate;
        float2 maskTextureCoordinate;
    } VAPAttachmentRasterizerData;

    float3 RGBColorFromYuvTextures(sampler textureSampler, float2 coordinate, texture2d<float> texture_luma, texture2d<float> texture_chroma, matrix_float3x3 rotationMatrix, float2 offset) {
        float3 color;
        color.x = texture_luma.sample(textureSampler, coordinate).r;
        color.yz = texture_chroma.sample(textureSampler, coordinate).rg - offset;
        return float3(rotationMatrix * color);
    }

    float4 RGBAColor(sampler textureSampler, float2 colorCoordinate, float2 alphaCoordinate, texture2d<float> lumaTexture, texture2d<float> chromaTexture, constant ColorParameters *colorParameters) {
        matrix_float3x3 rotationMatrix = colorParameters[0].matrix;
        float2 offset = colorParameters[0].offset;
        float3 color = RGBColorFromYuvTextures(textureSampler, colorCoordinate, lumaTexture, chromaTexture, rotationMatrix, offset);
        float3 alpha = RGBColorFromYuvTextures(textureSampler, alphaCoordinate, lumaTexture, chromaTexture, rotationMatrix, offset);
        return float4(color, alpha.r);
    }

    vertex HWDRasterizerData hwd_vertexShader(uint vertexID [[ vertex_id ]], constant QGHWDVertex *vertexArray [[ buffer(0) ]]) {
        HWDRasterizerData out;
        out.clipSpacePostion = vertexArray[vertexID].position;
        out.textureColorCoordinate = vertexArray[vertexID].textureColorCoordinate;
        out.textureAlphaCoordinate = vertexArray[vertexID].textureAlphaCoordinate;
        return out;
    }

    fragment float4 hwd_yuvFragmentShader(HWDRasterizerData input [[ stage_in ]], texture2d<float> lumaTexture [[ texture(0) ]], texture2d<float> chromaTexture [[ texture(1) ]], constant ColorParameters *colorParameters [[ buffer(0) ]]) {
        constexpr sampler textureSampler(mag_filter::linear, min_filter::linear);
        return RGBAColor(textureSampler, input.textureColorCoordinate, input.textureAlphaCoordinate, lumaTexture, chromaTexture, colorParameters);
    }

    vertex VAPRasterizerData vap_vertexShader(uint vertexID [[ vertex_id ]], constant QGVAPVertex *vertexArray [[ buffer(0) ]]) {
        VAPRasterizerData out;
        out.clipSpacePostion = vertexArray[vertexID].position;
        out.textureColorCoordinate = vertexArray[vertexID].textureColorCoordinate;
        out.textureAlphaCoordinate = vertexArray[vertexID].textureAlphaCoordinate;
        out.textureMaskCoordinate = vertexArray[vertexID].textureMaskCoordinate;
        return out;
    }

    fragment float4 vap_yuvFragmentShader(VAPRasterizerData input [[ stage_in ]], texture2d<float> lumaTexture [[ texture(0) ]], texture2d<float> chromaTexture [[ texture(1) ]], constant ColorParameters *colorParameters [[ buffer(0) ]]) {
        constexpr sampler textureSampler(mag_filter::linear, min_filter::linear);
        return RGBAColor(textureSampler, input.textureColorCoordinate, input.textureAlphaCoordinate, lumaTexture, chromaTexture, colorParameters);
    }

    fragment float4 vap_maskFragmentShader(VAPRasterizerData input [[ stage_in ]], texture2d<float> lumaTexture [[ texture(0) ]], texture2d<float> chromaTexture [[ texture(1) ]], texture2d<float> maskTexture [[ texture(2) ]], constant ColorParameters *colorParameters [[ buffer(0) ]]) {
        constexpr sampler textureSampler(mag_filter::linear, min_filter::linear);
        float4 originColor = RGBAColor(textureSampler, input.textureColorCoordinate, input.textureAlphaCoordinate, lumaTexture, chromaTexture, colorParameters);
        float4 maskColor = maskTexture.sample(textureSampler, input.textureMaskCoordinate);
        float needMask = maskColor.r * 255;
        return float4(originColor.rgb, (1 - needMask) * originColor.a);
    }

    fragment float4 vap_maskBlurFragmentShader(VAPRasterizerData input [[ stage_in ]], texture2d<float> lumaTexture [[ texture(0) ]], texture2d<float> chromaTexture [[ texture(1) ]], texture2d<float> maskTexture [[ texture(2) ]], constant ColorParameters *colorParameters [[ buffer(0) ]], constant MaskParameters *maskParameters [[ buffer(1) ]]) {
        constexpr sampler textureSampler(mag_filter::linear, min_filter::linear);
        float4 originColor = RGBAColor(textureSampler, input.textureColorCoordinate, input.textureAlphaCoordinate, lumaTexture, chromaTexture, colorParameters);
        int uniform = 255;
        float3x3 weightMatrix = maskParameters[0].weightMatrix;
        int coreSize = maskParameters[0].coreSize;
        float texelOffset = maskParameters[0].texelOffset;
        float alphaResult = 0;
        for (int y = 0; y < coreSize; y++) {
            for (int x = 0; x < coreSize; x++) {
                float2 nearMaskColor = float2(input.textureMaskCoordinate.x + (-1.0 + float(x)) * texelOffset, input.textureMaskCoordinate.y + (-1.0 + float(y)) * texelOffset);
                alphaResult += maskTexture.sample(textureSampler, nearMaskColor).r * uniform * weightMatrix[x][y];
            }
        }
        int needOrigin = step(alphaResult, 0.01) + step(originColor.a, 0.01);
        return float4(originColor.rgb, needOrigin * originColor.a + (1 - needOrigin) * (1 - alphaResult));
    }

    vertex VAPAttachmentRasterizerData vapAttachment_vertexShader(uint vertexID [[ vertex_id ]], constant QGHWDAttachmentVertex *vertexArray [[ buffer(0) ]]) {
        VAPAttachmentRasterizerData out;
        out.position = vertexArray[vertexID].position;
        out.sourceTextureCoordinate = vertexArray[vertexID].sourceTextureCoordinate;
        out.maskTextureCoordinate = vertexArray[vertexID].maskTextureCoordinate;
        return out;
    }

    fragment float4 vapAttachment_FragmentShader(VAPAttachmentRasterizerData input [[ stage_in ]], texture2d<float> lumaTexture [[ texture(0) ]], texture2d<float> chromaTexture [[ texture(1) ]], texture2d<float> sourceTexture [[ texture(2) ]], constant ColorParameters *colorParameters [[ buffer(0) ]], constant VapAttachmentFragmentParameter *fillParams [[ buffer(1) ]]) {
        constexpr sampler textureSampler(mag_filter::linear, min_filter::linear);
        matrix_float3x3 rotationMatrix = colorParameters[0].matrix;
        float2 offset = colorParameters[0].offset;
        float3 mask = RGBColorFromYuvTextures(textureSampler, input.maskTextureCoordinate, lumaTexture, chromaTexture, rotationMatrix, offset);
        float4 source = sourceTexture.sample(textureSampler, input.sourceTextureCoordinate);
        return float4(source.rgb, source.a * mask.r);
    }
    """
}

@objcMembers
public final class LNVAPMetalUtil: NSObject {
    public static let vapAttachmentVertexFunctionName = "vapAttachment_vertexShader"
    public static let vapAttachmentFragmentFunctionName = "vapAttachment_FragmentShader"
    public static let vapVertexFunctionName = "vap_vertexShader"
    public static let vapYUVFragmentFunctionName = "vap_yuvFragmentShader"
    public static let vapMaskFragmentFunctionName = "vap_maskFragmentShader"
    public static let vapMaskBlurFragmentFunctionName = "vap_maskBlurFragmentShader"

    public static let vapMTLVerticesIdentity: [Float] = [-1.0, -1.0, 0.0, 1.0, -1.0, 1.0, 0.0, 1.0, 1.0, -1.0, 0.0, 1.0, 1.0, 1.0, 0.0, 1.0]
    public static let vapMTLTextureCoordinatesIdentity: [Float] = [0.0, 1.0, 0.0, 0.0, 1.0, 1.0, 1.0, 0.0]
    public static let vapMTLTextureCoordinatesFor90: [Float] = [0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 1.0, 1.0]

    @objc(genVerticesWithRect:containerSize:reverse:)
    public static func genVertices(rect: CGRect, containerSize: CGSize, reverse: Bool) -> [Float] {
        guard containerSize.width > 0, containerSize.height > 0 else { return vapMTLVerticesIdentity }

        let originX = Float(-1 + 2 * rect.origin.x / containerSize.width)
        let originY = Float(1 - 2 * rect.origin.y / containerSize.height)
        let width = Float(2 * rect.size.width / containerSize.width)
        let height = Float(2 * rect.size.height / containerSize.height)

        if reverse {
            return [originX, originY - height, 0.0, 1.0,
                    originX, originY, 0.0, 1.0,
                    originX + width, originY - height, 0.0, 1.0,
                    originX + width, originY, 0.0, 1.0]
        }

        return [originX, originY, 0.0, 1.0,
                originX, originY - height, 0.0, 1.0,
                originX + width, originY, 0.0, 1.0,
                originX + width, originY - height, 0.0, 1.0]
    }

    @objc(genTextureCoordinatesWithRect:containerSize:reverse:degree:)
    public static func genTextureCoordinates(rect: CGRect, containerSize: CGSize, reverse: Bool, degree: Int) -> [Float] {
        _ = degree
        guard containerSize.width > 0, containerSize.height > 0 else { return vapMTLTextureCoordinatesIdentity }

        let originX = Float(rect.origin.x / containerSize.width)
        let originY = Float(rect.origin.y / containerSize.height)
        let width = Float(rect.size.width / containerSize.width)
        let height = Float(rect.size.height / containerSize.height)

        if reverse {
            return [originX, originY,
                    originX, originY + height,
                    originX + width, originY,
                    originX + width, originY + height]
        }

        return [originX, originY + height,
                originX, originY,
                originX + width, originY + height,
                originX + width, originY]
    }

    @objc(sourceSizeForCenterFullWithSourceSize:renderSize:)
    public static func sourceSizeForCenterFull(sourceSize: CGSize, renderSize: CGSize) -> CGSize {
        if sourceSize.width >= renderSize.width, sourceSize.height >= renderSize.height {
            return sourceSize
        }
        return rect(inside: CGRect(origin: .zero, size: renderSize), aspectRatio: sourceSize, contentMode: .scaleAspectFill).size
    }

    @objc(rectForCenterFullWithSourceSize:renderSize:)
    public static func rectForCenterFull(sourceSize: CGSize, renderSize: CGSize) -> CGRect {
        if sourceSize.width >= renderSize.width, sourceSize.height >= renderSize.height {
            return CGRect(x: (sourceSize.width - renderSize.width) / 2.0,
                          y: (sourceSize.height - renderSize.height) / 2.0,
                          width: renderSize.width,
                          height: renderSize.height)
        }

        let fill = rect(inside: CGRect(origin: .zero, size: renderSize), aspectRatio: sourceSize, contentMode: .scaleAspectFill)
        return CGRect(x: -fill.origin.x, y: -fill.origin.y, width: renderSize.width, height: renderSize.height)
    }

    @objc(rectInsideBoundingRect:aspectRatio:contentMode:)
    public static func rect(inside boundingRect: CGRect, aspectRatio: CGSize, contentMode: UIView.ContentMode) -> CGRect {
        guard aspectRatio.width > 0, aspectRatio.height > 0 else { return boundingRect }

        switch contentMode {
        case .scaleToFill:
            return boundingRect
        case .scaleAspectFit:
            return AVMakeRect(aspectRatio: aspectRatio, insideRect: boundingRect)
        case .scaleAspectFill:
            let ratio = max(boundingRect.width / aspectRatio.width, boundingRect.height / aspectRatio.height)
            let size = CGSize(width: aspectRatio.width * ratio, height: aspectRatio.height * ratio)
            return CGRect(x: boundingRect.origin.x + (boundingRect.width - size.width) / 2.0,
                          y: boundingRect.origin.y + (boundingRect.height - size.height) / 2.0,
                          width: size.width,
                          height: size.height)
        case .center:
            return CGRect(x: boundingRect.origin.x + (boundingRect.width - aspectRatio.width) / 2.0,
                          y: boundingRect.origin.y + (boundingRect.height - aspectRatio.height) / 2.0,
                          width: aspectRatio.width,
                          height: aspectRatio.height)
        case .top:
            return CGRect(x: boundingRect.origin.x + (boundingRect.width - aspectRatio.width) / 2.0,
                          y: boundingRect.origin.y,
                          width: aspectRatio.width,
                          height: aspectRatio.height)
        case .bottom:
            return CGRect(x: boundingRect.origin.x + (boundingRect.width - aspectRatio.width) / 2.0,
                          y: boundingRect.maxY - aspectRatio.height,
                          width: aspectRatio.width,
                          height: aspectRatio.height)
        case .left:
            return CGRect(x: boundingRect.origin.x,
                          y: boundingRect.origin.y + (boundingRect.height - aspectRatio.height) / 2.0,
                          width: aspectRatio.width,
                          height: aspectRatio.height)
        case .right:
            return CGRect(x: boundingRect.maxX - aspectRatio.width,
                          y: boundingRect.origin.y + (boundingRect.height - aspectRatio.height) / 2.0,
                          width: aspectRatio.width,
                          height: aspectRatio.height)
        case .topLeft:
            return CGRect(origin: boundingRect.origin, size: aspectRatio)
        case .topRight:
            return CGRect(x: boundingRect.maxX - aspectRatio.width,
                          y: boundingRect.origin.y,
                          width: aspectRatio.width,
                          height: aspectRatio.height)
        case .bottomLeft:
            return CGRect(x: boundingRect.origin.x,
                          y: boundingRect.maxY - aspectRatio.height,
                          width: aspectRatio.width,
                          height: aspectRatio.height)
        case .bottomRight:
            return CGRect(x: boundingRect.maxX - aspectRatio.width,
                          y: boundingRect.maxY - aspectRatio.height,
                          width: aspectRatio.width,
                          height: aspectRatio.height)
        default:
            return boundingRect
        }
    }
}

public extension UIView {
    @objc func lnStopVAPIfNeeded() {
        (self as? LNVAPPlayerView)?.lnStop()
    }
}

@objcMembers
public final class LNVAPDeviceUtil: NSObject {
    private static var cachedSystemVersion: Double = (UIDevice.current.systemVersion as NSString).doubleValue

    public static var systemVersionNum: Double {
        cachedSystemVersion
    }

    public static var defaultMTLResourceOption: MTLResourceOptions {
        if #available(iOS 9.0, *) {
            return .storageModeShared
        }
        return []
    }
}

public extension UIDevice {
    @objc(ln_isSimulator)
    func ln_isSimulator() -> Bool {
        #if targetEnvironment(simulator)
        return true
        #else
        return false
        #endif
    }
}

public extension NSArray {
    @objc(ln_rectValue)
    func ln_rectValue() -> CGRect {
        guard count >= 4 else { return .zero }
        for index in 0..<4 {
            let value = self[index]
            if !(value is NSNumber) && !(value is NSString) {
                return .zero
            }
        }
        return CGRect(x: (self[0] as? NSNumber)?.doubleValue ?? ((self[0] as? NSString)?.doubleValue ?? 0),
                      y: (self[1] as? NSNumber)?.doubleValue ?? ((self[1] as? NSString)?.doubleValue ?? 0),
                      width: (self[2] as? NSNumber)?.doubleValue ?? ((self[2] as? NSString)?.doubleValue ?? 0),
                      height: (self[3] as? NSNumber)?.doubleValue ?? ((self[3] as? NSString)?.doubleValue ?? 0))
    }
}

public extension NSDictionary {
    @objc(ln_floatValueForKey:)
    func ln_floatValue(forKey key: String?) -> CGFloat {
        guard let key else { return 0 }
        guard let value = self[key], !(value is NSNull) else { return 0 }
        if let number = value as? NSNumber { return CGFloat(number.doubleValue) }
        if let string = value as? NSString { return CGFloat(string.doubleValue) }
        return 0
    }

    @objc(ln_integerValueForKey:)
    func ln_integerValue(forKey key: String?) -> Int {
        guard let key else { return 0 }
        guard let value = self[key], !(value is NSNull) else { return 0 }
        if let number = value as? NSNumber { return number.intValue }
        if let string = value as? NSString { return string.integerValue }
        return 0
    }

    @objc(ln_stringValueForKey:)
    func ln_stringValue(forKey key: String?) -> String {
        guard let key else { return "" }
        guard let value = self[key], !(value is NSNull) else { return "" }
        if let string = value as? String { return string }
        if let number = value as? NSNumber { return number.description }
        return ""
    }

    @objc(ln_dictionaryValueForKey:)
    func ln_dictionaryValue(forKey key: String?) -> NSDictionary? {
        guard let key else { return nil }
        return self[key] as? NSDictionary
    }

    @objc(ln_arrayValueForKey:)
    func ln_arrayValue(forKey key: String?) -> NSArray? {
        guard let key else { return nil }
        return self[key] as? NSArray
    }
}

public extension UIColor {
    @objc(ln_colorWithHexString:)
    static func ln_color(hexString: String) -> UIColor? {
        var normalized = hexString.trimmingCharacters(in: .whitespacesAndNewlines).uppercased()
        if normalized.hasPrefix("#") {
            normalized.removeFirst()
        } else if normalized.hasPrefix("0X") {
            normalized.removeFirst(2)
        }

        guard [3, 4, 6, 8].contains(normalized.count) else { return nil }

        func hexValue(_ text: String) -> CGFloat? {
            UInt64(text, radix: 16).map { CGFloat($0) / 255.0 }
        }

        let r: CGFloat
        let g: CGFloat
        let b: CGFloat
        let a: CGFloat
        switch normalized.count {
        case 3, 4:
            guard
                let rr = hexValue(String(normalized[normalized.startIndex])),
                let gg = hexValue(String(normalized[normalized.index(normalized.startIndex, offsetBy: 1)])),
                let bb = hexValue(String(normalized[normalized.index(normalized.startIndex, offsetBy: 2)]))
            else { return nil }
            r = rr
            g = gg
            b = bb
            if normalized.count == 4 {
                let index = normalized.index(normalized.startIndex, offsetBy: 3)
                guard let aa = hexValue(String(normalized[index])) else { return nil }
                a = aa
            } else {
                a = 1
            }
        case 6, 8:
            let start = normalized.startIndex
            let r0 = normalized[start...normalized.index(start, offsetBy: 1)]
            let g0 = normalized[normalized.index(start, offsetBy: 2)...normalized.index(start, offsetBy: 3)]
            let b0 = normalized[normalized.index(start, offsetBy: 4)...normalized.index(start, offsetBy: 5)]
            guard
                let rr = hexValue(String(r0)),
                let gg = hexValue(String(g0)),
                let bb = hexValue(String(b0))
            else { return nil }
            r = rr
            g = gg
            b = bb
            if normalized.count == 8 {
                let a0 = normalized[normalized.index(start, offsetBy: 6)...normalized.index(start, offsetBy: 7)]
                guard let aa = hexValue(String(a0)) else { return nil }
                a = aa
            } else {
                a = 1
            }
        default:
            return nil
        }

        return UIColor(red: r, green: g, blue: b, alpha: a)
    }
}

private final class LNGestureBlockTarget: NSObject {
    private let block: (Any) -> Void

    init(block: @escaping (Any) -> Void) {
        self.block = block
    }

    @objc func invoke(_ sender: Any) {
        block(sender)
    }
}

private var lnGestureTargetKey: UInt8 = 0

public extension UIGestureRecognizer {
    @objc(ln_initWithActionBlock:)
    convenience init(lnActionBlock: @escaping (Any) -> Void) {
        self.init()
        ln_addActionBlock(lnActionBlock)
    }

    @objc(ln_addActionBlock:)
    func ln_addActionBlock(_ block: @escaping (Any) -> Void) {
        let target = LNGestureBlockTarget(block: block)
        addTarget(target, action: #selector(LNGestureBlockTarget.invoke(_:)))
        var targets = objc_getAssociatedObject(self, &lnGestureTargetKey) as? [LNGestureBlockTarget] ?? []
        targets.append(target)
        objc_setAssociatedObject(self, &lnGestureTargetKey, targets, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
    }

    @objc(ln_removeAllActionBlocks)
    func ln_removeAllActionBlocks() {
        let targets = objc_getAssociatedObject(self, &lnGestureTargetKey) as? [LNGestureBlockTarget] ?? []
        for target in targets {
            removeTarget(target, action: #selector(LNGestureBlockTarget.invoke(_:)))
        }
        objc_setAssociatedObject(self, &lnGestureTargetKey, nil, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
    }
}

private enum LNNotificationQueueStore {
    static let lock = NSRecursiveLock()
    static var store: [String: OperationQueue] = [:]
}

public extension Notification.Name {
    var ln_notificationOperationQueue: OperationQueue {
        get {
            LNNotificationQueueStore.lock.lock()
            defer { LNNotificationQueueStore.lock.unlock() }
            if let queue = LNNotificationQueueStore.store[rawValue] {
                return queue
            }
            let queue = OperationQueue()
            queue.maxConcurrentOperationCount = 1
            LNNotificationQueueStore.store[rawValue] = queue
            return queue
        }
        nonmutating set {
            LNNotificationQueueStore.lock.lock()
            LNNotificationQueueStore.store[rawValue] = newValue
            LNNotificationQueueStore.lock.unlock()
        }
    }
}

private final class LNWeakNotificationBox: NSObject {
    weak var observer: NSObjectProtocol?
    weak var center: NotificationCenter?
    var token: NSObjectProtocol?

    init(observer: NSObjectProtocol, center: NotificationCenter) {
        self.observer = observer
        self.center = center
    }
}

public extension NotificationCenter {
    @objc(ln_addSafeObserver:selector:name:object:)
    func ln_addSafeObserver(_ observer: NSObject, selector: Selector, name: Notification.Name?, object: Any?) {
        if #available(iOS 9.0, *) {
            addObserver(observer, selector: selector, name: name, object: object)
            return
        }
        let queue = name?.ln_notificationOperationQueue
        let box = LNWeakNotificationBox(observer: observer, center: self)
        box.token = addObserver(forName: name, object: object, queue: queue) { [weak box] note in
            guard let box, let target = box.observer else {
                if let token = box?.token {
                    box?.center?.removeObserver(token)
                }
                return
            }
            _ = (target as AnyObject).perform(selector, with: note)
        }
    }

    @objc(ln_addSafeObserver:selector:name:object:queue:)
    func ln_addSafeObserver(_ observer: NSObject, selector: Selector, name: Notification.Name?, object: Any?, queue: OperationQueue) {
        if let name {
            name.ln_notificationOperationQueue = queue
        }
        ln_addSafeObserver(observer, selector: selector, name: name, object: object)
    }

    func ln_addWeakObserver(_ weakObserver: NSObject, name: Notification.Name?, using block: @escaping (Notification, NSObject) -> Void) {
        let box = LNWeakNotificationBox(observer: weakObserver, center: self)
        box.token = addObserver(forName: name, object: nil, queue: nil) { [weak box] note in
            guard let box, let target = box.observer as? NSObject else {
                if let token = box?.token {
                    box?.center?.removeObserver(token)
                }
                return
            }
            block(note, target)
        }
    }
}
