import Foundation
import UIKit
import CoreVideo
import Metal
import MetalKit

private func lnRectForCenterFull(sourceSize: CGSize, renderSize: CGSize) -> CGRect {
    guard sourceSize.width > 0, sourceSize.height > 0, renderSize.width > 0, renderSize.height > 0 else { return .zero }
    let sourceRatio = sourceSize.width / sourceSize.height
    let renderRatio = renderSize.width / renderSize.height
    if sourceRatio > renderRatio {
        let height = sourceSize.height
        let width = height * renderRatio
        return CGRect(x: (sourceSize.width - width) / 2.0, y: 0, width: width, height: height)
    }
    let width = sourceSize.width
    let height = width / renderRatio
    return CGRect(x: 0, y: (sourceSize.height - height) / 2.0, width: width, height: height)
}

private func lnSourceSizeForCenterFull(sourceSize: CGSize, renderSize: CGSize) -> CGSize {
    guard sourceSize.width > 0, sourceSize.height > 0, renderSize.width > 0, renderSize.height > 0 else { return .zero }
    let sourceRatio = sourceSize.width / sourceSize.height
    let renderRatio = renderSize.width / renderSize.height
    if sourceRatio > renderRatio {
        let h = sourceSize.height
        return CGSize(width: h * renderRatio, height: h)
    }
    let w = sourceSize.width
    return CGSize(width: w, height: w / renderRatio)
}

private func lnGenVertices(rect: CGRect, containerSize: CGSize) -> [Float] {
    guard containerSize.width > 0, containerSize.height > 0 else { return Array(repeating: 0, count: 16) }
    let left = Float((rect.minX / containerSize.width) * 2.0 - 1.0)
    let right = Float((rect.maxX / containerSize.width) * 2.0 - 1.0)
    let top = Float(1.0 - (rect.minY / containerSize.height) * 2.0)
    let bottom = Float(1.0 - (rect.maxY / containerSize.height) * 2.0)
    return [left, bottom, 0, 1, right, bottom, 0, 1, left, top, 0, 1, right, top, 0, 1]
}

private func lnGenTextureCoordinates(rect: CGRect, containerSize: CGSize, reverse: Bool) -> [Float] {
    guard containerSize.width > 0, containerSize.height > 0 else { return Array(repeating: 0, count: 8) }
    let u0 = Float(rect.minX / containerSize.width)
    let u1 = Float(rect.maxX / containerSize.width)
    let v0 = Float(rect.minY / containerSize.height)
    let v1 = Float(rect.maxY / containerSize.height)
    return reverse ? [u0, v1, u1, v1, u0, v0, u1, v0] : [u0, v0, u1, v0, u0, v1, u1, v1]
}

@objc public enum LNVAPOrientation: Int {
    case none = 0
    case portrait = 1
    case landscape = 2
}

@objcMembers
public final class LNVAPAttachmentConstants: NSObject {
    public static let fitTypeFitXY = "fitXY"
    public static let fitTypeCenterFull = "centerFull"
    public static let sourceTypeTextStr = "textStr"
    public static let sourceTypeImgUrl = "imgUrl"
    public static let sourceTypeText = "txt"
    public static let sourceTypeImg = "img"
    public static let sourceLoadTypeLocal = "local"
    public static let sourceLoadTypeNet = "net"
    public static let sourceStyleBoldText = "b"
}

@objcMembers
public class LNBaseDFileInfo: NSObject {
    public var filePath: String = ""
    public var occupiedCount: Int = 0
}

@objcMembers
public class LNBaseAnimatedImageFrame: NSObject {
    public var frameIndex: Int = 0
    public var duration: TimeInterval = 0
    public var pts: UInt64 = 0
    public var startDate: Date?
    public var decodeTime: TimeInterval = 0

    @objc(shouldFinishDisplaying)
    public func shouldFinishDisplaying() -> Bool {
        guard let startDate else { return true }
        return Date().timeIntervalSince(startDate) * 1000.0 + 10.0 >= duration
    }
}

@objcMembers
public final class LNMP4AnimatedImageFrame: LNBaseAnimatedImageFrame {
    public var pixelBuffer: CVPixelBuffer?
    public var defaultFps: Int32 = 0
}

@objcMembers
public final class LNMP4HWDFileInfo: LNBaseDFileInfo {
    public var mp4Parser: LNMP4ParserProxy?
}

@objcMembers
public final class LNVAPConfigModel: NSObject {
    public var info: LNVAPCommonInfo?
    public var resources: [LNVAPSourceInfo] = []
    public var mergedConfig: [NSNumber: [LNVAPMergedInfo]] = [:]

    public override var description: String {
        "<\(Swift.type(of: self)): \(Unmanaged.passUnretained(self).toOpaque())> {info:\(String(describing: info)), configs:\(mergedConfig)}"
    }
}

@objcMembers
public final class LNVAPCommonInfo: NSObject {
    public var version: Int = 0
    public var framesCount: Int = 0
    public var size: CGSize = .zero
    public var videoSize: CGSize = .zero
    public var targetOrientaion: LNVAPOrientation = .none
    public var fps: Int = 0
    public var isMerged: Bool = false
    public var alphaAreaRect: CGRect = .zero
    public var rgbAreaRect: CGRect = .zero

    public override var description: String {
        "<\(Swift.type(of: self)): \(Unmanaged.passUnretained(self).toOpaque())> {version:\(version), frames:\(framesCount), size:(\(size.width),\(size.height)), videoSize:(\(videoSize.width),\(videoSize.height)) orien:\(targetOrientaion.rawValue), fps:\(fps), merged:\(isMerged)}"
    }
}

@objcMembers
public final class LNVAPSourceInfo: NSObject {
    public var type: String?
    public var loadType: String?
    public var contentTag: String?
    public var contentTagValue: String?
    public var color: UIColor?
    public var style: String?
    public var size: CGSize = .zero
    public var fitType: String?

    public var sourceImage: UIImage?
    public var texture: MTLTexture?
    public var colorParamsBuffer: MTLBuffer?

    public override var description: String {
        "<\(Swift.type(of: self)): \(Unmanaged.passUnretained(self).toOpaque())> {type:\(type ?? ""), tag:\(contentTag ?? "")-\(contentTagValue ?? "") fitType:\(fitType ?? "") }"
    }
}

@objcMembers
public final class LNVAPSourceDisplayItem: NSObject {
    public var frame: CGRect = .zero
    public var sourceInfo: LNVAPSourceInfo?
}

@objcMembers
public final class LNVAPMergedInfo: NSObject {
    public var source: LNVAPSourceInfo?
    public var renderIndex: Int = 0
    public var renderRect: CGRect = .zero
    public var needMask: Bool = false
    public var maskRect: CGRect = .zero
    public var maskRotation: Int = 0

    @objc(vertexBufferWithContainerSize:maskContianerSize:device:)
    public func vertexBuffer(containerSize size: CGSize, maskContianerSize mSize: CGSize, device: MTLDevice) -> MTLBuffer? {
        guard size.width > 0, size.height > 0, mSize.width > 0, mSize.height > 0 else { return nil }

        let vertices = lnGenVertices(rect: renderRect, containerSize: size)
        let maskCoordinates = lnGenTextureCoordinates(rect: maskRect, containerSize: mSize, reverse: true)
        let sourceCoordinates: [Float]
        if source?.fitType == LNVAPAttachmentConstants.fitTypeCenterFull, let source {
            let sourceRect = lnRectForCenterFull(sourceSize: source.size, renderSize: renderRect.size)
            let sourceSize = lnSourceSizeForCenterFull(sourceSize: source.size, renderSize: renderRect.size)
            sourceCoordinates = lnGenTextureCoordinates(rect: sourceRect, containerSize: sourceSize, reverse: false)
        } else {
            sourceCoordinates = [0, 1, 1, 1, 0, 0, 1, 0]
        }

        var vertexData = [Float](repeating: 0, count: 32)
        var idx = 0
        for i in 0..<16 {
            vertexData[idx] = vertices[i]
            idx += 1
            if i % 4 == 3 {
                let row = i / 4
                vertexData[idx] = sourceCoordinates[row * 2]; idx += 1
                vertexData[idx] = sourceCoordinates[row * 2 + 1]; idx += 1
                vertexData[idx] = maskCoordinates[row * 2]; idx += 1
                vertexData[idx] = maskCoordinates[row * 2 + 1]; idx += 1
            }
        }

        return device.makeBuffer(bytes: vertexData, length: vertexData.count * MemoryLayout<Float>.size, options: .storageModeShared)
    }
}

@objcMembers
public final class LNVAPMaskInfo: NSObject {
    public var data: Data = Data() {
        didSet { cachedTexture = nil }
    }
    public var sampleRect: CGRect = .zero
    public var dataSize: CGSize = .zero {
        didSet { cachedTexture = nil }
    }
    public var blurLength: Int = 0

    private var cachedTexture: MTLTexture?
    private var cachedDeviceID: ObjectIdentifier?

    @objc(textureWithDevice:)
    public func texture(with device: MTLDevice?) -> MTLTexture? {
        guard let device else { return nil }
        let deviceID = ObjectIdentifier(device)
        if let cachedTexture, cachedDeviceID == deviceID {
            return cachedTexture
        }
        cachedTexture = LNVAPTextureLoader.loadTexture(withData: data, device: device, width: dataSize.width, height: dataSize.height)
        cachedDeviceID = cachedTexture == nil ? nil : deviceID
        return cachedTexture
    }

    public var texture: MTLTexture? {
        texture(with: MTLCreateSystemDefaultDevice())
    }
}

@objcMembers
public final class LNVAPTextureLoader: NSObject {
#if targetEnvironment(simulator)
    @objc(lnLoadVAPColorFillBufferWith:device:)
    public static func lnLoadVAPColorFillBuffer(with color: UIColor?, device: MTLDevice?) -> MTLBuffer? { nil }

    @objc(loadTextureWithImage:device:)
    public static func loadTexture(with image: UIImage?, device: MTLDevice?) -> MTLTexture? { nil }

    @objc(loadTextureWithData:device:width:height:)
    public static func loadTexture(withData data: Data?, device: MTLDevice?, width: CGFloat, height: CGFloat) -> MTLTexture? {
        guard let data, let device, !data.isEmpty else { return nil }
        let descriptor = MTLTextureDescriptor.texture2DDescriptor(pixelFormat: .r8Unorm, width: Int(width), height: Int(height), mipmapped: false)
        guard let texture = device.makeTexture(descriptor: descriptor) else { return nil }
        let region = MTLRegionMake3D(0, 0, 0, Int(width), Int(height), 1)
        data.withUnsafeBytes { ptr in
            if let base = ptr.baseAddress {
                texture.replace(region: region, mipmapLevel: 0, withBytes: base, bytesPerRow: Int(width))
            }
        }
        return texture
    }

    @objc(drawingImageForText:color:size:bold:)
    public static func drawingImage(forText textStr: String?, color: UIColor?, size: CGSize, bold: Bool) -> UIImage? { nil }

    @objc(getAppropriateFontWith:rect:designedSize:bold:textSize:)
    public static func getAppropriateFont(with text: String?, rect fitFrame: CGRect, designedSize: CGFloat, bold isBold: Bool, textSize: UnsafeMutablePointer<CGSize>?) -> UIFont? { nil }
#else
    private struct LNVAPAttachmentFragmentParameter {
        var needOriginRGB: Int32
        var fillColor: SIMD4<Float>
    }

    @objc(lnLoadVAPColorFillBufferWith:device:)
    public static func lnLoadVAPColorFillBuffer(with color: UIColor?, device: MTLDevice?) -> MTLBuffer? {
        guard let device else { return nil }
        var red: CGFloat = 0
        var green: CGFloat = 0
        var blue: CGFloat = 0
        var alpha: CGFloat = 0
        if let color { color.getRed(&red, green: &green, blue: &blue, alpha: &alpha) }
        var params = LNVAPAttachmentFragmentParameter(
            needOriginRGB: color == nil ? 1 : 0,
            fillColor: SIMD4<Float>(Float(red), Float(green), Float(blue), Float(alpha))
        )
        return device.makeBuffer(bytes: &params, length: MemoryLayout<LNVAPAttachmentFragmentParameter>.stride, options: .storageModeShared)
    }

    @objc(loadTextureWithImage:device:)
    public static func loadTexture(with image: UIImage?, device: MTLDevice?) -> MTLTexture? {
        guard let image else {
            LNVAPLogger.log(level: 2, file: #file, line: #line, func: #function, module: "common", message: "attempt to loadTexture with nil image")
            return nil
        }
        guard let device else { return nil }
        guard let cgImage = image.cgImage else {
            LNVAPLogger.log(level: 2, file: #file, line: #line, func: #function, module: "common", message: "attempt to loadTexture with invalid CGImage")
            return nil
        }
        if #available(iOS 10.0, *) {
            let loader = MTKTextureLoader(device: device)
            do {
                return try loader.newTexture(cgImage: cgImage, options: [
                    .origin: MTKTextureLoader.Origin.flippedVertically,
                    .SRGB: NSNumber(value: false)
                ])
            } catch {
                LNVAPLogger.log(level: 2, file: #file, line: #line, func: #function, module: "common", message: "loadTexture error: \(error.localizedDescription)")
                return nil
            }
        }
        return cgLoadTexture(with: image, device: device)
    }

    @objc(loadTextureWithData:device:width:height:)
    public static func loadTexture(withData data: Data?, device: MTLDevice?, width: CGFloat, height: CGFloat) -> MTLTexture? {
        guard let data, !data.isEmpty else {
            LNVAPLogger.log(level: 2, file: #file, line: #line, func: #function, module: "common", message: "attempt to loadTexture with nil data")
            return nil
        }
        guard let device else { return nil }
        let textureWidth = Int(width)
        let textureHeight = Int(height)
        guard textureWidth > 0, textureHeight > 0 else { return nil }
        let descriptor = MTLTextureDescriptor.texture2DDescriptor(pixelFormat: .r8Unorm, width: textureWidth, height: textureHeight, mipmapped: false)
        guard let texture = device.makeTexture(descriptor: descriptor) else { return nil }
        let region = MTLRegionMake3D(0, 0, 0, textureWidth, textureHeight, 1)
        data.withUnsafeBytes { ptr in
            if let base = ptr.baseAddress {
                texture.replace(region: region, mipmapLevel: 0, withBytes: base, bytesPerRow: textureWidth)
            }
        }
        return texture
    }

    @objc(drawingImageForText:color:size:bold:)
    public static func drawingImage(forText textStr: String?, color: UIColor?, size: CGSize, bold: Bool) -> UIImage? {
        guard let textStr, !textStr.isEmpty else {
            LNVAPLogger.log(level: 2, file: #file, line: #line, func: #function, module: "common", message: "draw text resource fail because text is nil")
            return nil
        }
        let textColor = color ?? .black
        let rect = CGRect(x: 0, y: 0, width: size.width / 2.0, height: size.height / 2.0)

        var textSize = CGSize.zero
        guard let font = getAppropriateFont(with: textStr, rect: rect, designedSize: rect.height * 0.8, bold: bold, textSize: &textSize) else {
            LNVAPLogger.log(level: 2, file: #file, line: #line, func: #function, module: "common", message: "draw text resource \(textStr) failed because font is nil")
            return nil
        }

        let paragraphStyle = NSMutableParagraphStyle()
        paragraphStyle.alignment = .center
        paragraphStyle.lineBreakMode = .byTruncatingTail
        let attributes: [NSAttributedString.Key: Any] = [.font: font, .paragraphStyle: paragraphStyle, .foregroundColor: textColor]

        let renderer = UIGraphicsImageRenderer(size: rect.size)
        return renderer.image { _ in
            var drawRect = rect
            drawRect.origin.y = (rect.height - font.lineHeight) / 2.0
            textStr.draw(with: drawRect, options: .usesLineFragmentOrigin, attributes: attributes, context: nil)
        }
    }

    @objc(getAppropriateFontWith:rect:designedSize:bold:textSize:)
    public static func getAppropriateFont(with text: String?, rect fitFrame: CGRect, designedSize: CGFloat, bold isBold: Bool, textSize: UnsafeMutablePointer<CGSize>?) -> UIFont? {
        var font = isBold ? UIFont.boldSystemFont(ofSize: designedSize) : UIFont.systemFont(ofSize: designedSize)
        guard let text, !text.isEmpty, fitFrame != .zero else {
            textSize?.pointee = fitFrame.size
            return font
        }
        var stringSize = (text as NSString).size(withAttributes: [.font: font])
        var fontSize = designedSize
        var remainCount = 100
        while stringSize.width > fitFrame.width && fontSize > 2.0 && remainCount > 0 {
            fontSize *= 0.9
            remainCount -= 1
            font = isBold ? UIFont.boldSystemFont(ofSize: fontSize) : UIFont.systemFont(ofSize: fontSize)
            stringSize = (text as NSString).size(withAttributes: [.font: font])
        }
        textSize?.pointee = stringSize
        return font
    }

    private static func cgLoadTexture(with image: UIImage, device: MTLDevice) -> MTLTexture? {
        guard let imageRef = image.cgImage else { return nil }
        let width = imageRef.width
        let height = imageRef.height
        let bytesPerPixel = 4
        let bytesPerRow = bytesPerPixel * width

        guard let colorSpace = CGColorSpace(name: CGColorSpace.sRGB),
              let rawData = calloc(height * width * bytesPerPixel, MemoryLayout<UInt8>.size),
              let context = CGContext(data: rawData, width: width, height: height, bitsPerComponent: 8, bytesPerRow: bytesPerRow, space: colorSpace, bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue | CGBitmapInfo.byteOrder32Big.rawValue) else {
            LNVAPLogger.log(level: 2, file: #file, line: #line, func: #function, module: "common", message: "cg_loadTexture alloc context failed")
            return nil
        }

        context.translateBy(x: 0, y: CGFloat(height))
        context.scaleBy(x: 1, y: -1)
        context.draw(imageRef, in: CGRect(x: 0, y: 0, width: width, height: height))

        let descriptor = MTLTextureDescriptor.texture2DDescriptor(pixelFormat: .rgba8Unorm, width: width, height: height, mipmapped: false)
        guard let texture = device.makeTexture(descriptor: descriptor) else {
            free(rawData)
            LNVAPLogger.log(level: 2, file: #file, line: #line, func: #function, module: "common", message: "cg_loadTexture failed getting texture")
            return nil
        }

        let region = MTLRegionMake3D(0, 0, 0, width, height, 1)
        texture.replace(region: region, mipmapLevel: 0, withBytes: rawData, bytesPerRow: bytesPerRow)
        free(rawData)
        return texture
    }
#endif
}
