import Foundation

public let kLNBoxSizeLengthInBytes = 4
public let kLNBoxTypeLengthInBytes = 4
public let kLNBoxLargeSizeLengthInBytes = 8
public let kLNBoxLargeSizeFlagLengthInBytes = 1

private func lnReadU32(_ bytes: UnsafePointer<UInt8>, _ offset: Int) -> UInt32 {
    (UInt32(bytes[offset]) << 24)
    | (UInt32(bytes[offset + 1]) << 16)
    | (UInt32(bytes[offset + 2]) << 8)
    | UInt32(bytes[offset + 3])
}

private extension Data {
    func lnU32(at offset: Int) -> UInt32 {
        guard count >= offset + 4 else { return 0 }
        return withUnsafeBytes { rawPtr in
            guard let base = rawPtr.baseAddress?.assumingMemoryBound(to: UInt8.self) else { return 0 }
            return lnReadU32(base, offset)
        }
    }

    func lnU64(at offset: Int) -> UInt64 {
        guard count >= offset + 8 else { return 0 }
        return withUnsafeBytes { rawPtr in
            guard let base = rawPtr.baseAddress?.assumingMemoryBound(to: UInt8.self) else { return 0 }
            return lnReadU64(base.advanced(by: offset))
        }
    }
}

private func lnReadU64(_ bytes: UnsafePointer<UInt8>) -> UInt64 {
    let b0 = UInt64(bytes[0]) << 56
    let b1 = UInt64(bytes[1]) << 48
    let b2 = UInt64(bytes[2]) << 40
    let b3 = UInt64(bytes[3]) << 32
    let b4 = UInt64(bytes[4]) << 24
    let b5 = UInt64(bytes[5]) << 16
    let b6 = UInt64(bytes[6]) << 8
    let b7 = UInt64(bytes[7])
    return b0 | b1 | b2 | b3 | b4 | b5 | b6 | b7
}

@objc public enum LNMP4CodecType: Int {
    case unknown = 0
    case video
    case audio
}

@objc public enum LNMP4TrackType: UInt32 {
    case video = 0x76696465
    case audio = 0x736F756E
    case hint = 0x68696E74
}

@objc public enum LNMP4VideoStreamCodecID: Int {
    case unknown = 0
    case h264
    case h265
}

@objc public enum LNMP4BoxType: UInt32 {
    case unknown = 0x0
    case ftyp = 0x66747970
    case free = 0x66726565
    case mdat = 0x6D646174
    case moov = 0x6D6F6F76
    case mvhd = 0x6D766864
    case iods = 0x696F6473
    case trak = 0x7472616B
    case tkhd = 0x746B6864
    case edts = 0x65647473
    case elst = 0x656C7374
    case mdia = 0x6D646961
    case mdhd = 0x6D646864
    case hdlr = 0x68646C72
    case minf = 0x6D696E66
    case vmhd = 0x766D6864
    case dinf = 0x64696E66
    case dref = 0x64726566
    case url = 0x0075726C
    case stbl = 0x7374626C
    case stsd = 0x73747364
    case avc1 = 0x61766331
    case avcC = 0x61766343
    case stts = 0x73747473
    case stss = 0x73747373
    case stsc = 0x73747363
    case stsz = 0x7374737A
    case stco = 0x7374636F
    case ctts = 0x63747473
    case udta = 0x75647461
    case meta = 0x6D657461
    case ilst = 0x696C7374
    case data = 0x64617461
    case wide = 0x77696465
    case loci = 0x6C6F6369
    case smhd = 0x736D6864
    case vapc = 0x76617063
    case hvc1 = 0x68766331
    case hvcC = 0x68766343
}

public typealias LNMP4BoxDataFetcher = (LNMP4Box) -> Data?

@objcMembers
open class LNMP4Box: NSObject {
    public var type: LNMP4BoxType
    public var length: UInt64
    public var startIndexInBytes: UInt64
    public weak var superBox: LNMP4Box?
    public var subBoxes: [LNMP4Box] = []

    @objc(initWithType:startIndex:length:)
    public init(type: LNMP4BoxType, startIndex: UInt64, length: UInt64) {
        self.type = type
        self.startIndexInBytes = startIndex
        self.length = length
    }

    @objc(subBoxOfType:)
    public func subBox(ofType type: LNMP4BoxType) -> LNMP4Box? {
        for subBox in subBoxes {
            if subBox.type == type { return subBox }
            if let box = subBox.subBox(ofType: type) { return box }
        }
        return nil
    }

    @objc(superBoxOfType:)
    public func superBox(ofType type: LNMP4BoxType) -> LNMP4Box? {
        guard let superBox else { return nil }
        if superBox.type == type { return superBox }
        return superBox.superBox(ofType: type)
    }

    open func boxDidParsed(_ dataFetcher: LNMP4BoxDataFetcher) {}

    private func description(level: Int) -> String {
        var desc = "Box:\(typeString) offset:\(startIndexInBytes) size:\(length) "
        for _ in 0..<level { desc = "|--\(desc)" }
        desc = "\n\(desc)"
        for sub in subBoxes {
            desc += sub.description(level: level + 1)
        }
        return desc
    }

    public override var description: String {
        description(level: 0)
    }

    private var typeString: String {
        let value = type.rawValue
        if value == 0 { return "unknown" }
        let chars: [UInt8] = [
            UInt8((value >> 24) & 0xFF),
            UInt8((value >> 16) & 0xFF),
            UInt8((value >> 8) & 0xFF),
            UInt8(value & 0xFF)
        ]
        return String(bytes: chars.filter { $0 != 0 }, encoding: .ascii) ?? "unknown"
    }
}

@objcMembers public final class LNMP4MdatBox: LNMP4Box {}
@objcMembers public final class LNMP4AvccBox: LNMP4Box {}
@objcMembers public final class LNMP4HvccBox: LNMP4Box {}
@objcMembers public final class LNMP4MvhdBox: LNMP4Box {}
@objcMembers public final class LNMP4StsdBox: LNMP4Box {}
@objcMembers public final class LNMP4TrackBox: LNMP4Box {}

@objcMembers
public final class LNStscEntry: NSObject {
    public var firstChunk: UInt32 = 0
    public var samplesPerChunk: UInt32 = 0
    public var sampleDescriptionIndex: UInt32 = 0
}

@objcMembers
public final class LNMP4StscBox: LNMP4Box {
    public var entries: [LNStscEntry] = []

    public override func boxDidParsed(_ dataFetcher: LNMP4BoxDataFetcher) {
        guard let stscData = dataFetcher(self), stscData.count >= 16 else { return }
        let entryCount = Int(stscData.lnU32(at: 12))
        entries.removeAll(keepingCapacity: true)
        for i in 0..<entryCount {
            let base = 16 + i * 12
            guard stscData.count >= base + 12 else { break }
            let entry = LNStscEntry()
            entry.firstChunk = stscData.lnU32(at: base)
            entry.samplesPerChunk = stscData.lnU32(at: base + 4)
            entry.sampleDescriptionIndex = stscData.lnU32(at: base + 8)
            entries.append(entry)
        }
    }
}

@objcMembers
public final class LNMP4StcoBox: LNMP4Box {
    public var chunkCount: UInt32 = 0
    public var chunkOffsets: [NSNumber] = []

    public override func boxDidParsed(_ dataFetcher: LNMP4BoxDataFetcher) {
        guard let stcoData = dataFetcher(self), stcoData.count >= 16 else { return }
        let entryCount = Int(stcoData.lnU32(at: 12))
        chunkCount = UInt32(entryCount)
        chunkOffsets.removeAll(keepingCapacity: true)
        for i in 0..<entryCount {
            let base = 16 + i * 4
            guard stcoData.count >= base + 4 else { break }
            chunkOffsets.append(NSNumber(value: stcoData.lnU32(at: base)))
        }
    }
}

@objcMembers
public final class LNMP4StssBox: LNMP4Box {
    public var syncSamples: [NSNumber] = []

    public override func boxDidParsed(_ dataFetcher: LNMP4BoxDataFetcher) {
        guard let stssData = dataFetcher(self), stssData.count >= 16 else { return }
        let sampleCount = Int(stssData.lnU32(at: 12))
        syncSamples.removeAll(keepingCapacity: true)
        for i in 0..<sampleCount {
            let base = 16 + i * 4
            guard stssData.count >= base + 4 else { break }
            let index = Int(stssData.lnU32(at: base)) - 1
            syncSamples.append(NSNumber(value: max(index, 0)))
        }
    }
}

@objcMembers
public final class LNMP4CttsBox: LNMP4Box {
    public var compositionOffsets: [NSNumber] = []

    public override func boxDidParsed(_ dataFetcher: LNMP4BoxDataFetcher) {
        guard let cttsData = dataFetcher(self), cttsData.count >= 16 else { return }
        let entryCount = Int(cttsData.lnU32(at: 12))
        compositionOffsets.removeAll(keepingCapacity: true)
        for i in 0..<entryCount {
            let base = 16 + i * 8
            guard cttsData.count >= base + 8 else { break }
            let sampleCount = Int(cttsData.lnU32(at: base))
            let compositionOffset = cttsData.lnU32(at: base + 4)
            for _ in 0..<sampleCount {
                compositionOffsets.append(NSNumber(value: compositionOffset))
            }
        }
    }
}

@objcMembers
public final class LNSttsEntry: NSObject {
    public var sampleCount: UInt32 = 0
    public var sampleDelta: UInt32 = 0
}

@objcMembers
public final class LNMP4SttsBox: LNMP4Box {
    public var entries: [LNSttsEntry] = []

    public override func boxDidParsed(_ dataFetcher: LNMP4BoxDataFetcher) {
        guard let sttsData = dataFetcher(self), sttsData.count >= 16 else { return }
        let entryCount = Int(sttsData.lnU32(at: 12))
        entries.removeAll(keepingCapacity: true)
        for i in 0..<entryCount {
            let base = 16 + i * 8
            guard sttsData.count >= base + 8 else { break }
            let entry = LNSttsEntry()
            entry.sampleCount = sttsData.lnU32(at: base)
            entry.sampleDelta = sttsData.lnU32(at: base + 4)
            entries.append(entry)
        }
    }
}

@objcMembers
public final class LNMP4StszBox: LNMP4Box {
    public var sampleCount: UInt32 = 0
    public var sampleSizes: [NSNumber] = []

    public override func boxDidParsed(_ dataFetcher: LNMP4BoxDataFetcher) {
        guard let stszData = dataFetcher(self), stszData.count >= 20 else { return }
        let sampleSize = stszData.lnU32(at: 12)
        let sampleCount = Int(stszData.lnU32(at: 16))
        self.sampleCount = UInt32(sampleCount)
        sampleSizes.removeAll(keepingCapacity: true)
        for i in 0..<sampleCount {
            if sampleSize > 0 {
                sampleSizes.append(NSNumber(value: sampleSize))
            } else {
                let base = 20 + i * 4
                guard stszData.count >= base + 4 else { break }
                sampleSizes.append(NSNumber(value: stszData.lnU32(at: base)))
            }
        }
    }
}

@objcMembers
public final class LNMP4HdlrBox: LNMP4Box {
    public var trackType: LNMP4TrackType = .video

    public override func boxDidParsed(_ dataFetcher: LNMP4BoxDataFetcher) {
        guard let hdlrData = dataFetcher(self), hdlrData.count >= 20 else { return }
        let trackTypeValue = hdlrData.lnU32(at: 16)
        trackType = LNMP4TrackType(rawValue: trackTypeValue) ?? .video
    }
}

@objcMembers
public final class LNMP4Sample: NSObject {
    public var codecType: LNMP4CodecType = .unknown
    public var sampleDelta: UInt32 = 0
    public var sampleSize: UInt32 = 0
    public var sampleIndex: UInt32 = 0
    public var chunkIndex: UInt32 = 0
    public var streamOffset: UInt32 = 0
    public var pts: UInt64 = 0
    public var dts: UInt64 = 0
    public var isKeySample: Bool = false
}

@objcMembers
public final class LNChunkOffsetEntry: NSObject {
    public var samplesPerChunk: UInt32 = 0
    public var offset: UInt32 = 0
}

@objcMembers
public final class LNCttsEntry: NSObject {
    public var sampleCount: UInt32 = 0
    public var compositionOffset: UInt32 = 0
}

@objcMembers
public final class LNMP4BoxFactory: NSObject {
    @objc(isTypeValueValid:)
    public static func isTypeValueValid(_ type: LNMP4BoxType) -> Bool {
        boxClass(for: type) != nil
    }

    @objc(boxClassForType:)
    public static func boxClass(for type: LNMP4BoxType) -> AnyClass? {
        switch type {
        case .stss:
            return LNMP4StssBox.self
        case .mdat:
            return LNMP4MdatBox.self
        case .avcC:
            return LNMP4AvccBox.self
        case .mdhd:
            return LNMP4MvhdBox.self
        case .stsd:
            return LNMP4StsdBox.self
        case .stsz:
            return LNMP4StszBox.self
        case .hdlr:
            return LNMP4HdlrBox.self
        case .stsc:
            return LNMP4StscBox.self
        case .stts:
            return LNMP4SttsBox.self
        case .stco:
            return LNMP4StcoBox.self
        case .hvcC:
            return LNMP4HvccBox.self
        case .ctts:
            return LNMP4CttsBox.self
        case .trak:
            return LNMP4TrackBox.self
        case .ftyp, .free, .moov, .mvhd, .tkhd, .edts, .elst, .mdia, .minf, .vmhd, .dinf, .dref, .url, .stbl, .avc1, .udta, .meta, .ilst, .data, .iods, .wide, .loci, .smhd, .vapc, .hvc1:
            return LNMP4Box.self
        default:
            return nil
        }
    }

    @objc(createBoxForType:startIndex:length:)
    public static func createBox(for type: LNMP4BoxType, startIndex: UInt64, length: UInt64) -> LNMP4Box {
        switch type {
        case .stss: return LNMP4StssBox(type: type, startIndex: startIndex, length: length)
        case .mdat: return LNMP4MdatBox(type: type, startIndex: startIndex, length: length)
        case .avcC: return LNMP4AvccBox(type: type, startIndex: startIndex, length: length)
        case .mdhd: return LNMP4MvhdBox(type: type, startIndex: startIndex, length: length)
        case .stsd: return LNMP4StsdBox(type: type, startIndex: startIndex, length: length)
        case .stsz: return LNMP4StszBox(type: type, startIndex: startIndex, length: length)
        case .hdlr: return LNMP4HdlrBox(type: type, startIndex: startIndex, length: length)
        case .stsc: return LNMP4StscBox(type: type, startIndex: startIndex, length: length)
        case .stts: return LNMP4SttsBox(type: type, startIndex: startIndex, length: length)
        case .stco: return LNMP4StcoBox(type: type, startIndex: startIndex, length: length)
        case .hvcC: return LNMP4HvccBox(type: type, startIndex: startIndex, length: length)
        case .ctts: return LNMP4CttsBox(type: type, startIndex: startIndex, length: length)
        case .trak: return LNMP4TrackBox(type: type, startIndex: startIndex, length: length)
        default: return LNMP4Box(type: type, startIndex: startIndex, length: length)
        }
    }
}

@objc
public protocol LNMP4ParserDelegate: AnyObject {
    @objc optional func didParseMP4Box(_ box: LNMP4Box, parser: LNMP4Parser)
    @objc(MP4FileDidFinishParse:) optional func mp4FileDidFinishParse(_ parser: LNMP4Parser)
}

@objcMembers
public final class LNMP4Parser: NSObject {
    public var rootBox: LNMP4Box?
    public var fileHandle: FileHandle?
    public weak var delegate: LNMP4ParserDelegate?

    private let filePath: String

    @objc(initWithFilePath:)
    public init(filePath: String) {
        self.filePath = filePath
        self.fileHandle = FileHandle(forReadingAtPath: filePath)
        super.init()
    }


    deinit {
        fileHandle?.closeFile()
    }

    public func parse() {
        guard !filePath.isEmpty, let fileHandle else { return }

        let fileSize = fileHandle.seekToEndOfFile()
        fileHandle.seek(toFileOffset: 0)

        rootBox = LNMP4BoxFactory.createBox(for: .unknown, startIndex: 0, length: UInt64(fileSize))
        guard let rootBox else { return }

        var bfsQueue: [LNMP4Box] = [rootBox]

        while !bfsQueue.isEmpty {
            let calBox = bfsQueue.removeFirst()
            if calBox.length <= UInt64(2 * (kLNBoxSizeLengthInBytes + kLNBoxTypeLengthInBytes)) {
                continue
            }

            var offset: UInt64 = calBox.superBox == nil ? 0 : (calBox.startIndexInBytes + UInt64(kLNBoxSizeLengthInBytes + kLNBoxTypeLengthInBytes))
            if shouldResetOffset(calBox.type) {
                calibrateOffset(&offset, boxType: calBox.type)
            }

            while true {
                if offset + UInt64(kLNBoxSizeLengthInBytes + kLNBoxTypeLengthInBytes) > calBox.startIndexInBytes + calBox.length {
                    break
                }

                guard let parsed = readBoxTypeAndLength(offset: offset) else { break }
                let type = parsed.type
                let length = parsed.length
                if length == 0 {
                    // Invalid box length; avoid infinite loop on malformed data.
                    break
                }

                if offset + length > calBox.startIndexInBytes + calBox.length {
                    break
                }

                if !LNMP4BoxFactory.isTypeValueValid(type), offset == calBox.startIndexInBytes + UInt64(kLNBoxSizeLengthInBytes + kLNBoxTypeLengthInBytes) {
                    break
                }

                let subBox = LNMP4BoxFactory.createBox(for: type, startIndex: offset, length: length)
                subBox.superBox = calBox
                calBox.subBoxes.append(subBox)
                bfsQueue.append(subBox)
                didParseBox(subBox)
                offset += length
            }
        }

        didFinishParseFile()
    }

    @objc(readDataForBox:)
    public func readData(for box: LNMP4Box?) -> Data? {
        guard let box, let fileHandle else { return nil }
        fileHandle.seek(toFileOffset: box.startIndexInBytes)
        return fileHandle.readData(ofLength: Int(box.length))
    }

    @objc(readValue:length:)
    public func readValue(_ bytes: UnsafePointer<CChar>, length: Int) -> Int {
        var value = 0
        for i in 0..<length {
            value += (Int(bytes[i]) & 0xff) << ((length - i - 1) * 8)
        }
        return value
    }

    private func readValue(_ bytes: UnsafePointer<UInt8>, length: Int) -> UInt64 {
        var value: UInt64 = 0
        for i in 0..<length {
            value += UInt64(bytes[i] & 0xff) << UInt64((length - i - 1) * 8)
        }
        return value
    }

    private func readBoxTypeAndLength(offset: UInt64) -> (type: LNMP4BoxType, length: UInt64)? {
        guard let fileHandle else { return nil }
        fileHandle.seek(toFileOffset: offset)
        let headerData = fileHandle.readData(ofLength: kLNBoxSizeLengthInBytes + kLNBoxTypeLengthInBytes)
        guard headerData.count >= kLNBoxSizeLengthInBytes + kLNBoxTypeLengthInBytes else {
            return nil
        }

        let length: UInt64 = headerData.withUnsafeBytes { rawPtr in
            guard let bytes = rawPtr.baseAddress?.assumingMemoryBound(to: UInt8.self) else { return 0 }
            return readValue(bytes, length: kLNBoxSizeLengthInBytes)
        }
        let typeRaw: UInt32 = headerData.withUnsafeBytes { rawPtr in
            guard let bytes = rawPtr.baseAddress?.assumingMemoryBound(to: UInt8.self) else { return 0 }
            return UInt32(readValue(bytes.advanced(by: kLNBoxSizeLengthInBytes), length: kLNBoxTypeLengthInBytes))
        }

        var finalLength = length
        let type = LNMP4BoxType(rawValue: typeRaw) ?? .unknown

        if finalLength == UInt64(kLNBoxLargeSizeFlagLengthInBytes) {
            let extendedOffset = offset + UInt64(kLNBoxSizeLengthInBytes + kLNBoxTypeLengthInBytes)
            fileHandle.seek(toFileOffset: extendedOffset)
            let largeSizeData = fileHandle.readData(ofLength: kLNBoxLargeSizeLengthInBytes)
            guard largeSizeData.count >= kLNBoxLargeSizeLengthInBytes else {
                return nil
            }
            finalLength = largeSizeData.withUnsafeBytes { rawPtr in
                guard let bytes = rawPtr.baseAddress?.assumingMemoryBound(to: UInt8.self) else { return 0 }
                return readValue(bytes, length: kLNBoxLargeSizeLengthInBytes)
            }
            if finalLength == 0 { return nil }
        }
        if finalLength == 0 {
            return nil
        }

        return (type, finalLength)
    }

    private func shouldResetOffset(_ type: LNMP4BoxType) -> Bool {
        type == .stsd || type == .avc1 || type == .hvc1
    }

    private func calibrateOffset(_ offset: inout UInt64, boxType type: LNMP4BoxType) {
        switch type {
        case .stsd:
            offset += 8
        case .avc1, .hvc1:
            offset += 78
        default:
            break
        }
    }

    private func didParseBox(_ box: LNMP4Box) {
        box.boxDidParsed { [weak self] in
            self?.readData(for: $0)
        }
        delegate?.didParseMP4Box?(box, parser: self)
    }

    private func didFinishParseFile() {
        delegate?.mp4FileDidFinishParse?(self)
    }
}

@objcMembers
public final class LNMP4ParserProxy: NSObject, LNMP4ParserDelegate {
    private var parser: LNMP4Parser

    private var _picWidth: Int = 0
    private var _picHeight: Int = 0
    private var _fps: Int = 0
    private var _duration: Double = 0
    public var spsData: Data?
    public var ppsData: Data?
    private var _videoSamples: [LNMP4Sample]?
    private var _videoSyncSampleIndexes: [NSNumber]?
    public var rootBox: LNMP4Box?
    public var videoTrackBox: LNMP4TrackBox?
    public var audioTrackBox: LNMP4TrackBox?
    public var vpsData: Data?
    public var videoCodecID: LNMP4VideoStreamCodecID = .unknown

    public var picWidth: Int {
        get {
            if _picWidth == 0 {
                _picWidth = readPicWidth()
            }
            return _picWidth
        }
        set {
            _picWidth = newValue
        }
    }

    public var picHeight: Int {
        get {
            if _picHeight == 0 {
                _picHeight = readPicHeight()
            }
            return _picHeight
        }
        set {
            _picHeight = newValue
        }
    }

    public var duration: Double {
        get {
            if _duration == 0 {
                _duration = readDuration()
            }
            return _duration
        }
        set {
            _duration = newValue
        }
    }

    public var videoSamples: [LNMP4Sample] {
        get {
            if _videoSamples == nil {
                _videoSamples = buildVideoSamples()
            }
            return _videoSamples ?? []
        }
        set {
            _videoSamples = newValue
        }
    }

    public var videoSyncSampleIndexes: [NSNumber] {
        get {
            if _videoSyncSampleIndexes == nil {
                if let stss = videoTrackBox?.subBox(ofType: .stss) as? LNMP4StssBox {
                    _videoSyncSampleIndexes = stss.syncSamples
                } else {
                    _videoSyncSampleIndexes = []
                }
            }
            return _videoSyncSampleIndexes ?? []
        }
        set {
            _videoSyncSampleIndexes = newValue
        }
    }

    public var fps: Int {
        get {
            if _fps == 0 {
                let samples = videoSamples
                let totalDuration = duration
                if samples.isEmpty || totalDuration <= 0 {
                    return 0
                }
                _fps = Int(lround(Double(samples.count) / totalDuration))
            }
            return _fps
        }
        set {
            _fps = newValue
        }
    }

    @objc(initWithFilePath:)
    public init(filePath: String) {
        parser = LNMP4Parser(filePath: filePath)
        super.init()
        parser.delegate = self
    }


    public func parse() {
        _picWidth = 0
        _picHeight = 0
        _fps = 0
        _duration = 0
        _videoSamples = nil
        _videoSyncSampleIndexes = nil
        spsData = nil
        ppsData = nil
        vpsData = nil

        parser.parse()
        rootBox = parser.rootBox
        parseVideoDecoderConfigRecord()
    }

    @objc(readPacketOfSample:)
    public func readPacket(ofSample sampleIndex: Int) -> Data? {
        guard sampleIndex >= 0, sampleIndex < videoSamples.count else { return nil }
        guard let fileHandle = parser.fileHandle else { return nil }
        let sample = videoSamples[sampleIndex]
        fileHandle.seek(toFileOffset: UInt64(sample.streamOffset))
        return fileHandle.readData(ofLength: Int(sample.sampleSize))
    }

    @objc(readDataOfBox:length:offset:)
    public func readData(of box: LNMP4Box, length: Int, offset: Int) -> Data? {
        guard length > 0, offset >= 0 else { return nil }
        let end = UInt64(offset) + UInt64(length)
        guard end <= box.length else { return nil }
        guard let fileHandle = parser.fileHandle else { return nil }
        fileHandle.seek(toFileOffset: box.startIndexInBytes + UInt64(offset))
        return fileHandle.readData(ofLength: length)
    }

    private func parseVideoDecoderConfigRecord() {
        if videoCodecID == .h264 {
            spsData = parseAvccSPSData()
            ppsData = parseAvccPPSData()
        } else if videoCodecID == .h265 {
            parseHvccDecoderConfigRecord()
        }
    }

    private func buildVideoSamples() -> [LNMP4Sample] {
        guard let videoTrackBox,
              let stts = videoTrackBox.subBox(ofType: .stts) as? LNMP4SttsBox,
              let stsz = videoTrackBox.subBox(ofType: .stsz) as? LNMP4StszBox,
              let stsc = videoTrackBox.subBox(ofType: .stsc) as? LNMP4StscBox,
              let stco = videoTrackBox.subBox(ofType: .stco) as? LNMP4StcoBox else {
            return []
        }

        let ctts = videoTrackBox.subBox(ofType: .ctts) as? LNMP4CttsBox
        let keySampleIndexSet = Set(videoSyncSampleIndexes.map { $0.intValue })

        var samples: [LNMP4Sample] = []
        var ptsAccumulator: UInt64 = 0

        var stscEntryIndex: UInt32 = 0
        var stscEntrySampleIndex: UInt32 = 0
        var stscEntrySampleOffset: UInt32 = 0
        var sttsEntryIndex: UInt32 = 0
        var sttsEntrySampleIndex: UInt32 = 0
        var stcoChunkLogicIndex: UInt32 = 0

        for i in 0..<Int(stsz.sampleCount) {
            if Int(stscEntryIndex) >= stsc.entries.count
                || Int(sttsEntryIndex) >= stts.entries.count
                || Int(stcoChunkLogicIndex) >= stco.chunkOffsets.count {
                break
            }

            let stscEntry = stsc.entries[Int(stscEntryIndex)]
            let sttsEntry = stts.entries[Int(sttsEntryIndex)]

            let chunkOffset = stco.chunkOffsets[Int(stcoChunkLogicIndex)].uint32Value
            let sampleOffset = chunkOffset + stscEntrySampleOffset

            var cttsValue: UInt32 = 0
            if let ctts, i < ctts.compositionOffsets.count {
                cttsValue = ctts.compositionOffsets[i].uint32Value
            }

            let sample = LNMP4Sample()
            sample.codecType = .video
            sample.sampleIndex = UInt32(i)
            sample.chunkIndex = stcoChunkLogicIndex
            sample.sampleDelta = sttsEntry.sampleDelta
            sample.sampleSize = stsz.sampleSizes[i].uint32Value
            sample.pts = ptsAccumulator + UInt64(cttsValue)
            sample.dts = ptsAccumulator
            sample.streamOffset = sampleOffset
            sample.isKeySample = keySampleIndexSet.contains(i)
            samples.append(sample)

            stscEntrySampleOffset += sample.sampleSize
            ptsAccumulator += UInt64(sample.sampleDelta)

            stscEntrySampleIndex += 1
            if stscEntrySampleIndex >= stscEntry.samplesPerChunk {
                if stcoChunkLogicIndex + 1 < UInt32(stco.chunkOffsets.count) {
                    stcoChunkLogicIndex += 1
                }
                stscEntrySampleIndex = 0
                stscEntrySampleOffset = 0
            }

            sttsEntrySampleIndex += 1
            if sttsEntrySampleIndex >= sttsEntry.sampleCount {
                sttsEntrySampleIndex = 0
                if sttsEntryIndex + 1 < UInt32(stts.entries.count) {
                    sttsEntryIndex += 1
                }
            }

            if stscEntryIndex + 1 < UInt32(stsc.entries.count) {
                let nextFirstChunk = stsc.entries[Int(stscEntryIndex + 1)].firstChunk
                if stcoChunkLogicIndex >= nextFirstChunk - 1 {
                    stscEntryIndex += 1
                }
            }
        }

        return samples
    }

    private func parseHvccDecoderConfigRecord() {
        guard let hvcc = videoTrackBox?.subBox(ofType: .hvcC),
              let extraData = parser.readData(for: hvcc),
              extraData.count > 8 else { return }

        let bytes = [UInt8](extraData)
        var index = 30
        guard index < bytes.count else { return }
        let arrayNum = Int(bytes[index])
        index += 1

        for _ in 0..<arrayNum {
            guard index + 2 <= bytes.count else { return }
            let value = Int(bytes[index])
            index += 1
            let naluType = value & 0x3F
            let naluNum = (Int(bytes[index]) << 8) + Int(bytes[index + 1])
            index += 2

            for _ in 0..<naluNum {
                guard index + 2 <= bytes.count else { return }
                let naluLength = (Int(bytes[index]) << 8) + Int(bytes[index + 1])
                index += 2
                guard index + naluLength <= bytes.count else { return }
                let paramData = Data(bytes[index..<(index + naluLength)])

                if naluType == 32 {
                    vpsData = paramData
                } else if naluType == 33 {
                    spsData = paramData
                } else if naluType == 34 {
                    ppsData = paramData
                }
                index += naluLength
            }
        }
    }

    private func parseAvccSPSData() -> Data? {
        guard let avcc = videoTrackBox?.subBox(ofType: .avcC),
              let extraData = parser.readData(for: avcc),
              extraData.count > 16 else { return nil }

        let bytes = [UInt8](extraData)
        let spsLength = (Int(bytes[14]) << 8) + Int(bytes[15])
        let naluType = Int(bytes[16] & 0x1F)

        guard spsLength + 16 <= bytes.count, naluType == 7 else { return nil }
        return Data(bytes[16..<(16 + spsLength)])
    }

    private func parseAvccPPSData() -> Data? {
        guard let avcc = videoTrackBox?.subBox(ofType: .avcC),
              let extraData = parser.readData(for: avcc),
              extraData.count > 16 else { return nil }

        let bytes = [UInt8](extraData)
        var spsCount = Int(bytes[13] & 0x1F)
        let spsLength = (Int(bytes[14]) << 8) + Int(bytes[15])
        var prefixLength = 16 + spsLength

        while spsCount > 1 {
            guard prefixLength + 2 < bytes.count else { return nil }
            let nextSpsLength = (Int(bytes[prefixLength]) << 8) + Int(bytes[prefixLength + 1])
            prefixLength += nextSpsLength
            spsCount -= 1
        }

        guard prefixLength + 3 < bytes.count else { return nil }
        let ppsLength = (Int(bytes[prefixLength + 1]) << 8) + Int(bytes[prefixLength + 2])
        let naluType = Int(bytes[prefixLength + 3] & 0x1F)
        guard naluType == 8, ppsLength + prefixLength + 3 <= bytes.count else { return nil }

        return Data(bytes[(prefixLength + 3)..<(prefixLength + 3 + ppsLength)])
    }

    private func readPicWidth() -> Int {
        guard videoCodecID != .unknown,
              let box = videoTrackBox?.subBox(ofType: videoCodecID == .h264 ? .avc1 : .hvc1),
              let fileHandle = parser.fileHandle else { return 0 }

        fileHandle.seek(toFileOffset: box.startIndexInBytes + 32)
        let widthData = fileHandle.readData(ofLength: 2)
        guard widthData.count >= 2 else { return 0 }
        let bytes = [UInt8](widthData)
        return (Int(bytes[0]) << 8) + Int(bytes[1])
    }

    private func readPicHeight() -> Int {
        guard videoCodecID != .unknown,
              let box = videoTrackBox?.subBox(ofType: videoCodecID == .h264 ? .avc1 : .hvc1),
              let fileHandle = parser.fileHandle else { return 0 }

        fileHandle.seek(toFileOffset: box.startIndexInBytes + 34)
        let heightData = fileHandle.readData(ofLength: 2)
        guard heightData.count >= 2 else { return 0 }
        let bytes = [UInt8](heightData)
        return (Int(bytes[0]) << 8) + Int(bytes[1])
    }

    private func readDuration() -> Double {
        guard let mvhd = rootBox?.subBox(ofType: .mvhd),
              let mvhdData = parser.readData(for: mvhd),
              mvhdData.count > 24 else { return 0 }

        let version = Int(mvhdData.lnU32(at: 8))
        var timeScaleIndex = 20
        var durationIndex = 24
        var durationLength = 4
        if version == 1 {
            timeScaleIndex = 28
            durationIndex = 32
            durationLength = 8
        }

        guard mvhdData.count >= durationIndex + durationLength else { return 0 }

        let scale = Int(mvhdData.lnU32(at: timeScaleIndex))
        let durationValue: Int
        if durationLength == 4 {
            durationValue = Int(mvhdData.lnU32(at: durationIndex))
        } else {
            durationValue = Int(mvhdData.lnU64(at: durationIndex))
        }

        guard scale != 0 else { return 0 }
        return Double(durationValue) / Double(scale)
    }

    public func mp4FileDidFinishParse(_ parser: LNMP4Parser) {}

    public func didParseMP4Box(_ box: LNMP4Box, parser: LNMP4Parser) {
        switch box.type {
        case .hdlr:
            if let hdlr = box as? LNMP4HdlrBox,
               let trackBox = box.superBox(ofType: .trak) as? LNMP4TrackBox {
                switch hdlr.trackType {
                case .video:
                    videoTrackBox = trackBox
                case .audio:
                    audioTrackBox = trackBox
                default:
                    break
                }
            }
        case .avc1:
            videoCodecID = .h264
        case .hvc1:
            videoCodecID = .h265
        default:
            break
        }
    }
}
