//
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//

import Foundation
import GRDB
import SignalCoreKit

// NOTE: This file is generated by /Scripts/sds_codegen/sds_generate.py.
// Do not manually edit it, instead run `sds_codegen.sh`.

// MARK: - Record

public struct AttachmentRecord: SDSRecord {
    public weak var delegate: SDSRecordDelegate?

    public var tableMetadata: SDSTableMetadata {
        TSAttachmentSerializer.table
    }

    public static var databaseTableName: String {
        TSAttachmentSerializer.table.tableName
    }

    public var id: Int64?

    // This defines all of the columns used in the table
    // where this model (and any subclasses) are persisted.
    public let recordType: SDSRecordType
    public let uniqueId: String

    // Properties
    public let albumMessageId: String?
    public let attachmentType: TSAttachmentType
    public let blurHash: String?
    public let byteCount: UInt32
    public let caption: String?
    public let contentType: String
    public let encryptionKey: Data?
    public let serverId: UInt64
    public let sourceFilename: String?
    public let cachedAudioDurationSeconds: Double?
    public let cachedImageHeight: Double?
    public let cachedImageWidth: Double?
    public let creationTimestamp: Double?
    public let digest: Data?
    public let isUploaded: Bool?
    public let isValidImageCached: Bool?
    public let isValidVideoCached: Bool?
    public let lazyRestoreFragmentId: String?
    public let localRelativeFilePath: String?
    public let mediaSize: Data?
    public let pointerType: UInt?
    public let state: TSAttachmentPointerState?
    public let uploadTimestamp: UInt64
    public let cdnKey: String
    public let cdnNumber: UInt32
    public let isAnimatedCached: Bool?
    public let attachmentSchemaVersion: UInt
    public let videoDuration: Double?
    public let clientUuid: String?

    public enum CodingKeys: String, CodingKey, ColumnExpression, CaseIterable {
        case id
        case recordType
        case uniqueId
        case albumMessageId
        case attachmentType
        case blurHash
        case byteCount
        case caption
        case contentType
        case encryptionKey
        case serverId
        case sourceFilename
        case cachedAudioDurationSeconds
        case cachedImageHeight
        case cachedImageWidth
        case creationTimestamp
        case digest
        case isUploaded
        case isValidImageCached
        case isValidVideoCached
        case lazyRestoreFragmentId
        case localRelativeFilePath
        case mediaSize
        case pointerType
        case state
        case uploadTimestamp
        case cdnKey
        case cdnNumber
        case isAnimatedCached
        case attachmentSchemaVersion
        case videoDuration
        case clientUuid
    }

    public static func columnName(_ column: AttachmentRecord.CodingKeys, fullyQualified: Bool = false) -> String {
        fullyQualified ? "\(databaseTableName).\(column.rawValue)" : column.rawValue
    }

    public func didInsert(with rowID: Int64, for column: String?) {
        guard let delegate = delegate else {
            owsFailDebug("Missing delegate.")
            return
        }
        delegate.updateRowId(rowID)
    }
}

// MARK: - Row Initializer

public extension AttachmentRecord {
    static var databaseSelection: [SQLSelectable] {
        CodingKeys.allCases
    }

    init(row: Row) {
        id = row[0]
        recordType = row[1]
        uniqueId = row[2]
        albumMessageId = row[3]
        attachmentType = row[4]
        blurHash = row[5]
        byteCount = row[6]
        caption = row[7]
        contentType = row[8]
        encryptionKey = row[9]
        serverId = row[10]
        sourceFilename = row[11]
        cachedAudioDurationSeconds = row[12]
        cachedImageHeight = row[13]
        cachedImageWidth = row[14]
        creationTimestamp = row[15]
        digest = row[16]
        isUploaded = row[17]
        isValidImageCached = row[18]
        isValidVideoCached = row[19]
        lazyRestoreFragmentId = row[20]
        localRelativeFilePath = row[21]
        mediaSize = row[22]
        pointerType = row[23]
        state = row[24]
        uploadTimestamp = row[25]
        cdnKey = row[26]
        cdnNumber = row[27]
        isAnimatedCached = row[28]
        attachmentSchemaVersion = row[29]
        videoDuration = row[30]
        clientUuid = row[31]
    }
}

// MARK: - StringInterpolation

public extension String.StringInterpolation {
    mutating func appendInterpolation(attachmentColumn column: AttachmentRecord.CodingKeys) {
        appendLiteral(AttachmentRecord.columnName(column))
    }
    mutating func appendInterpolation(attachmentColumnFullyQualified column: AttachmentRecord.CodingKeys) {
        appendLiteral(AttachmentRecord.columnName(column, fullyQualified: true))
    }
}

// MARK: - Deserialization

extension TSAttachment {
    // This method defines how to deserialize a model, given a
    // database row.  The recordType column is used to determine
    // the corresponding model class.
    class func fromRecord(_ record: AttachmentRecord) throws -> TSAttachment {

        guard let recordId = record.id else {
            throw SDSError.invalidValue()
        }

        switch record.recordType {
        case .attachment:

            let uniqueId: String = record.uniqueId
            let albumMessageId: String? = record.albumMessageId
            let attachmentSchemaVersion: UInt = record.attachmentSchemaVersion
            let attachmentType: TSAttachmentType = record.attachmentType
            let blurHash: String? = record.blurHash
            let byteCount: UInt32 = record.byteCount
            let caption: String? = record.caption
            let cdnKey: String = record.cdnKey
            let cdnNumber: UInt32 = record.cdnNumber
            let clientUuid: String? = record.clientUuid
            let contentType: String = record.contentType
            let encryptionKey: Data? = SDSDeserialization.optionalData(record.encryptionKey, name: "encryptionKey")
            let serverId: UInt64 = record.serverId
            let sourceFilename: String? = record.sourceFilename
            let uploadTimestamp: UInt64 = record.uploadTimestamp
            let videoDuration: NSNumber? = SDSDeserialization.optionalNumericAsNSNumber(record.videoDuration, name: "videoDuration", conversion: { NSNumber(value: $0) })

            return TSAttachment(grdbId: recordId,
                                uniqueId: uniqueId,
                                albumMessageId: albumMessageId,
                                attachmentSchemaVersion: attachmentSchemaVersion,
                                attachmentType: attachmentType,
                                blurHash: blurHash,
                                byteCount: byteCount,
                                caption: caption,
                                cdnKey: cdnKey,
                                cdnNumber: cdnNumber,
                                clientUuid: clientUuid,
                                contentType: contentType,
                                encryptionKey: encryptionKey,
                                serverId: serverId,
                                sourceFilename: sourceFilename,
                                uploadTimestamp: uploadTimestamp,
                                videoDuration: videoDuration)

        case .attachmentPointer:

            let uniqueId: String = record.uniqueId
            let albumMessageId: String? = record.albumMessageId
            let attachmentSchemaVersion: UInt = record.attachmentSchemaVersion
            let attachmentType: TSAttachmentType = record.attachmentType
            let blurHash: String? = record.blurHash
            let byteCount: UInt32 = record.byteCount
            let caption: String? = record.caption
            let cdnKey: String = record.cdnKey
            let cdnNumber: UInt32 = record.cdnNumber
            let clientUuid: String? = record.clientUuid
            let contentType: String = record.contentType
            let encryptionKey: Data? = SDSDeserialization.optionalData(record.encryptionKey, name: "encryptionKey")
            let serverId: UInt64 = record.serverId
            let sourceFilename: String? = record.sourceFilename
            let uploadTimestamp: UInt64 = record.uploadTimestamp
            let videoDuration: NSNumber? = SDSDeserialization.optionalNumericAsNSNumber(record.videoDuration, name: "videoDuration", conversion: { NSNumber(value: $0) })
            let digest: Data? = SDSDeserialization.optionalData(record.digest, name: "digest")
            let lazyRestoreFragmentId: String? = record.lazyRestoreFragmentId
            let mediaSizeSerialized: Data? = record.mediaSize
            let mediaSize: CGSize = try SDSDeserialization.unarchive(mediaSizeSerialized, name: "mediaSize")
            let pointerType: UInt = try SDSDeserialization.required(record.pointerType, name: "pointerType")
            guard let state: TSAttachmentPointerState = record.state else {
               throw SDSError.missingRequiredField()
            }

            return TSAttachmentPointer(grdbId: recordId,
                                       uniqueId: uniqueId,
                                       albumMessageId: albumMessageId,
                                       attachmentSchemaVersion: attachmentSchemaVersion,
                                       attachmentType: attachmentType,
                                       blurHash: blurHash,
                                       byteCount: byteCount,
                                       caption: caption,
                                       cdnKey: cdnKey,
                                       cdnNumber: cdnNumber,
                                       clientUuid: clientUuid,
                                       contentType: contentType,
                                       encryptionKey: encryptionKey,
                                       serverId: serverId,
                                       sourceFilename: sourceFilename,
                                       uploadTimestamp: uploadTimestamp,
                                       videoDuration: videoDuration,
                                       digest: digest,
                                       lazyRestoreFragmentId: lazyRestoreFragmentId,
                                       mediaSize: mediaSize,
                                       pointerType: pointerType,
                                       state: state)

        case .attachmentStream:

            let uniqueId: String = record.uniqueId
            let albumMessageId: String? = record.albumMessageId
            let attachmentSchemaVersion: UInt = record.attachmentSchemaVersion
            let attachmentType: TSAttachmentType = record.attachmentType
            let blurHash: String? = record.blurHash
            let byteCount: UInt32 = record.byteCount
            let caption: String? = record.caption
            let cdnKey: String = record.cdnKey
            let cdnNumber: UInt32 = record.cdnNumber
            let clientUuid: String? = record.clientUuid
            let contentType: String = record.contentType
            let encryptionKey: Data? = SDSDeserialization.optionalData(record.encryptionKey, name: "encryptionKey")
            let serverId: UInt64 = record.serverId
            let sourceFilename: String? = record.sourceFilename
            let uploadTimestamp: UInt64 = record.uploadTimestamp
            let videoDuration: NSNumber? = SDSDeserialization.optionalNumericAsNSNumber(record.videoDuration, name: "videoDuration", conversion: { NSNumber(value: $0) })
            let cachedAudioDurationSeconds: NSNumber? = SDSDeserialization.optionalNumericAsNSNumber(record.cachedAudioDurationSeconds, name: "cachedAudioDurationSeconds", conversion: { NSNumber(value: $0) })
            let cachedImageHeight: NSNumber? = SDSDeserialization.optionalNumericAsNSNumber(record.cachedImageHeight, name: "cachedImageHeight", conversion: { NSNumber(value: $0) })
            let cachedImageWidth: NSNumber? = SDSDeserialization.optionalNumericAsNSNumber(record.cachedImageWidth, name: "cachedImageWidth", conversion: { NSNumber(value: $0) })
            guard let creationTimestampInterval: Double = record.creationTimestamp else {
               throw SDSError.missingRequiredField()
            }
            let creationTimestamp: Date = SDSDeserialization.requiredDoubleAsDate(creationTimestampInterval, name: "creationTimestamp")
            let digest: Data? = SDSDeserialization.optionalData(record.digest, name: "digest")
            let isAnimatedCached: NSNumber? = SDSDeserialization.optionalNumericAsNSNumber(record.isAnimatedCached, name: "isAnimatedCached", conversion: { NSNumber(value: $0) })
            let isUploaded: Bool = try SDSDeserialization.required(record.isUploaded, name: "isUploaded")
            let isValidImageCached: NSNumber? = SDSDeserialization.optionalNumericAsNSNumber(record.isValidImageCached, name: "isValidImageCached", conversion: { NSNumber(value: $0) })
            let isValidVideoCached: NSNumber? = SDSDeserialization.optionalNumericAsNSNumber(record.isValidVideoCached, name: "isValidVideoCached", conversion: { NSNumber(value: $0) })
            let localRelativeFilePath: String? = record.localRelativeFilePath

            return TSAttachmentStream(grdbId: recordId,
                                      uniqueId: uniqueId,
                                      albumMessageId: albumMessageId,
                                      attachmentSchemaVersion: attachmentSchemaVersion,
                                      attachmentType: attachmentType,
                                      blurHash: blurHash,
                                      byteCount: byteCount,
                                      caption: caption,
                                      cdnKey: cdnKey,
                                      cdnNumber: cdnNumber,
                                      clientUuid: clientUuid,
                                      contentType: contentType,
                                      encryptionKey: encryptionKey,
                                      serverId: serverId,
                                      sourceFilename: sourceFilename,
                                      uploadTimestamp: uploadTimestamp,
                                      videoDuration: videoDuration,
                                      cachedAudioDurationSeconds: cachedAudioDurationSeconds,
                                      cachedImageHeight: cachedImageHeight,
                                      cachedImageWidth: cachedImageWidth,
                                      creationTimestamp: creationTimestamp,
                                      digest: digest,
                                      isAnimatedCached: isAnimatedCached,
                                      isUploaded: isUploaded,
                                      isValidImageCached: isValidImageCached,
                                      isValidVideoCached: isValidVideoCached,
                                      localRelativeFilePath: localRelativeFilePath)

        default:
            owsFailDebug("Unexpected record type: \(record.recordType)")
            throw SDSError.invalidValue()
        }
    }
}

// MARK: - SDSModel

extension TSAttachment: SDSModel {
    public var serializer: SDSSerializer {
        // Any subclass can be cast to it's superclass,
        // so the order of this switch statement matters.
        // We need to do a "depth first" search by type.
        switch self {
        case let model as TSAttachmentStream:
            assert(type(of: model) == TSAttachmentStream.self)
            return TSAttachmentStreamSerializer(model: model)
        case let model as TSAttachmentPointer:
            assert(type(of: model) == TSAttachmentPointer.self)
            return TSAttachmentPointerSerializer(model: model)
        default:
            return TSAttachmentSerializer(model: self)
        }
    }

    public func asRecord() throws -> SDSRecord {
        try serializer.asRecord()
    }

    public var sdsTableName: String {
        AttachmentRecord.databaseTableName
    }

    public static var table: SDSTableMetadata {
        TSAttachmentSerializer.table
    }
}

// MARK: - DeepCopyable

extension TSAttachment: DeepCopyable {

    public func deepCopy() throws -> AnyObject {
        // Any subclass can be cast to it's superclass,
        // so the order of this switch statement matters.
        // We need to do a "depth first" search by type.
        guard let id = self.grdbId?.int64Value else {
            throw OWSAssertionError("Model missing grdbId.")
        }

        if let modelToCopy = self as? TSAttachmentStream {
            assert(type(of: modelToCopy) == TSAttachmentStream.self)
            let uniqueId: String = modelToCopy.uniqueId
            let albumMessageId: String? = modelToCopy.albumMessageId
            let attachmentSchemaVersion: UInt = modelToCopy.attachmentSchemaVersion
            let attachmentType: TSAttachmentType = modelToCopy.attachmentType
            let blurHash: String? = modelToCopy.blurHash
            let byteCount: UInt32 = modelToCopy.byteCount
            let caption: String? = modelToCopy.caption
            let cdnKey: String = modelToCopy.cdnKey
            let cdnNumber: UInt32 = modelToCopy.cdnNumber
            let clientUuid: String? = modelToCopy.clientUuid
            let contentType: String = modelToCopy.contentType
            let encryptionKey: Data? = modelToCopy.encryptionKey
            let serverId: UInt64 = modelToCopy.serverId
            let sourceFilename: String? = modelToCopy.sourceFilename
            let uploadTimestamp: UInt64 = modelToCopy.uploadTimestamp
            let videoDuration: NSNumber? = modelToCopy.videoDuration
            let cachedAudioDurationSeconds: NSNumber? = modelToCopy.cachedAudioDurationSeconds
            let cachedImageHeight: NSNumber? = modelToCopy.cachedImageHeight
            let cachedImageWidth: NSNumber? = modelToCopy.cachedImageWidth
            let creationTimestamp: Date = modelToCopy.creationTimestamp
            let digest: Data? = modelToCopy.digest
            let isAnimatedCached: NSNumber? = modelToCopy.isAnimatedCached
            let isUploaded: Bool = modelToCopy.isUploaded
            let isValidImageCached: NSNumber? = modelToCopy.isValidImageCached
            let isValidVideoCached: NSNumber? = modelToCopy.isValidVideoCached
            let localRelativeFilePath: String? = modelToCopy.localRelativeFilePath

            return TSAttachmentStream(grdbId: id,
                                      uniqueId: uniqueId,
                                      albumMessageId: albumMessageId,
                                      attachmentSchemaVersion: attachmentSchemaVersion,
                                      attachmentType: attachmentType,
                                      blurHash: blurHash,
                                      byteCount: byteCount,
                                      caption: caption,
                                      cdnKey: cdnKey,
                                      cdnNumber: cdnNumber,
                                      clientUuid: clientUuid,
                                      contentType: contentType,
                                      encryptionKey: encryptionKey,
                                      serverId: serverId,
                                      sourceFilename: sourceFilename,
                                      uploadTimestamp: uploadTimestamp,
                                      videoDuration: videoDuration,
                                      cachedAudioDurationSeconds: cachedAudioDurationSeconds,
                                      cachedImageHeight: cachedImageHeight,
                                      cachedImageWidth: cachedImageWidth,
                                      creationTimestamp: creationTimestamp,
                                      digest: digest,
                                      isAnimatedCached: isAnimatedCached,
                                      isUploaded: isUploaded,
                                      isValidImageCached: isValidImageCached,
                                      isValidVideoCached: isValidVideoCached,
                                      localRelativeFilePath: localRelativeFilePath)
        }

        if let modelToCopy = self as? TSAttachmentPointer {
            assert(type(of: modelToCopy) == TSAttachmentPointer.self)
            let uniqueId: String = modelToCopy.uniqueId
            let albumMessageId: String? = modelToCopy.albumMessageId
            let attachmentSchemaVersion: UInt = modelToCopy.attachmentSchemaVersion
            let attachmentType: TSAttachmentType = modelToCopy.attachmentType
            let blurHash: String? = modelToCopy.blurHash
            let byteCount: UInt32 = modelToCopy.byteCount
            let caption: String? = modelToCopy.caption
            let cdnKey: String = modelToCopy.cdnKey
            let cdnNumber: UInt32 = modelToCopy.cdnNumber
            let clientUuid: String? = modelToCopy.clientUuid
            let contentType: String = modelToCopy.contentType
            let encryptionKey: Data? = modelToCopy.encryptionKey
            let serverId: UInt64 = modelToCopy.serverId
            let sourceFilename: String? = modelToCopy.sourceFilename
            let uploadTimestamp: UInt64 = modelToCopy.uploadTimestamp
            let videoDuration: NSNumber? = modelToCopy.videoDuration
            let digest: Data? = modelToCopy.digest
            let lazyRestoreFragmentId: String? = modelToCopy.lazyRestoreFragmentId
            let mediaSize: CGSize = modelToCopy.mediaSize
            let pointerType: UInt = modelToCopy.pointerType
            let state: TSAttachmentPointerState = modelToCopy.state

            return TSAttachmentPointer(grdbId: id,
                                       uniqueId: uniqueId,
                                       albumMessageId: albumMessageId,
                                       attachmentSchemaVersion: attachmentSchemaVersion,
                                       attachmentType: attachmentType,
                                       blurHash: blurHash,
                                       byteCount: byteCount,
                                       caption: caption,
                                       cdnKey: cdnKey,
                                       cdnNumber: cdnNumber,
                                       clientUuid: clientUuid,
                                       contentType: contentType,
                                       encryptionKey: encryptionKey,
                                       serverId: serverId,
                                       sourceFilename: sourceFilename,
                                       uploadTimestamp: uploadTimestamp,
                                       videoDuration: videoDuration,
                                       digest: digest,
                                       lazyRestoreFragmentId: lazyRestoreFragmentId,
                                       mediaSize: mediaSize,
                                       pointerType: pointerType,
                                       state: state)
        }

        do {
            let modelToCopy = self
            assert(type(of: modelToCopy) == TSAttachment.self)
            let uniqueId: String = modelToCopy.uniqueId
            let albumMessageId: String? = modelToCopy.albumMessageId
            let attachmentSchemaVersion: UInt = modelToCopy.attachmentSchemaVersion
            let attachmentType: TSAttachmentType = modelToCopy.attachmentType
            let blurHash: String? = modelToCopy.blurHash
            let byteCount: UInt32 = modelToCopy.byteCount
            let caption: String? = modelToCopy.caption
            let cdnKey: String = modelToCopy.cdnKey
            let cdnNumber: UInt32 = modelToCopy.cdnNumber
            let clientUuid: String? = modelToCopy.clientUuid
            let contentType: String = modelToCopy.contentType
            let encryptionKey: Data? = modelToCopy.encryptionKey
            let serverId: UInt64 = modelToCopy.serverId
            let sourceFilename: String? = modelToCopy.sourceFilename
            let uploadTimestamp: UInt64 = modelToCopy.uploadTimestamp
            let videoDuration: NSNumber? = modelToCopy.videoDuration

            return TSAttachment(grdbId: id,
                                uniqueId: uniqueId,
                                albumMessageId: albumMessageId,
                                attachmentSchemaVersion: attachmentSchemaVersion,
                                attachmentType: attachmentType,
                                blurHash: blurHash,
                                byteCount: byteCount,
                                caption: caption,
                                cdnKey: cdnKey,
                                cdnNumber: cdnNumber,
                                clientUuid: clientUuid,
                                contentType: contentType,
                                encryptionKey: encryptionKey,
                                serverId: serverId,
                                sourceFilename: sourceFilename,
                                uploadTimestamp: uploadTimestamp,
                                videoDuration: videoDuration)
        }

    }
}

// MARK: - Table Metadata

extension TSAttachmentSerializer {

    // This defines all of the columns used in the table
    // where this model (and any subclasses) are persisted.
    static var idColumn: SDSColumnMetadata { SDSColumnMetadata(columnName: "id", columnType: .primaryKey) }
    static var recordTypeColumn: SDSColumnMetadata { SDSColumnMetadata(columnName: "recordType", columnType: .int64) }
    static var uniqueIdColumn: SDSColumnMetadata { SDSColumnMetadata(columnName: "uniqueId", columnType: .unicodeString, isUnique: true) }
    // Properties
    static var albumMessageIdColumn: SDSColumnMetadata { SDSColumnMetadata(columnName: "albumMessageId", columnType: .unicodeString, isOptional: true) }
    static var attachmentTypeColumn: SDSColumnMetadata { SDSColumnMetadata(columnName: "attachmentType", columnType: .int) }
    static var blurHashColumn: SDSColumnMetadata { SDSColumnMetadata(columnName: "blurHash", columnType: .unicodeString, isOptional: true) }
    static var byteCountColumn: SDSColumnMetadata { SDSColumnMetadata(columnName: "byteCount", columnType: .int64) }
    static var captionColumn: SDSColumnMetadata { SDSColumnMetadata(columnName: "caption", columnType: .unicodeString, isOptional: true) }
    static var contentTypeColumn: SDSColumnMetadata { SDSColumnMetadata(columnName: "contentType", columnType: .unicodeString) }
    static var encryptionKeyColumn: SDSColumnMetadata { SDSColumnMetadata(columnName: "encryptionKey", columnType: .blob, isOptional: true) }
    static var serverIdColumn: SDSColumnMetadata { SDSColumnMetadata(columnName: "serverId", columnType: .int64) }
    static var sourceFilenameColumn: SDSColumnMetadata { SDSColumnMetadata(columnName: "sourceFilename", columnType: .unicodeString, isOptional: true) }
    static var cachedAudioDurationSecondsColumn: SDSColumnMetadata { SDSColumnMetadata(columnName: "cachedAudioDurationSeconds", columnType: .double, isOptional: true) }
    static var cachedImageHeightColumn: SDSColumnMetadata { SDSColumnMetadata(columnName: "cachedImageHeight", columnType: .double, isOptional: true) }
    static var cachedImageWidthColumn: SDSColumnMetadata { SDSColumnMetadata(columnName: "cachedImageWidth", columnType: .double, isOptional: true) }
    static var creationTimestampColumn: SDSColumnMetadata { SDSColumnMetadata(columnName: "creationTimestamp", columnType: .double, isOptional: true) }
    static var digestColumn: SDSColumnMetadata { SDSColumnMetadata(columnName: "digest", columnType: .blob, isOptional: true) }
    static var isUploadedColumn: SDSColumnMetadata { SDSColumnMetadata(columnName: "isUploaded", columnType: .int, isOptional: true) }
    static var isValidImageCachedColumn: SDSColumnMetadata { SDSColumnMetadata(columnName: "isValidImageCached", columnType: .int, isOptional: true) }
    static var isValidVideoCachedColumn: SDSColumnMetadata { SDSColumnMetadata(columnName: "isValidVideoCached", columnType: .int, isOptional: true) }
    static var lazyRestoreFragmentIdColumn: SDSColumnMetadata { SDSColumnMetadata(columnName: "lazyRestoreFragmentId", columnType: .unicodeString, isOptional: true) }
    static var localRelativeFilePathColumn: SDSColumnMetadata { SDSColumnMetadata(columnName: "localRelativeFilePath", columnType: .unicodeString, isOptional: true) }
    static var mediaSizeColumn: SDSColumnMetadata { SDSColumnMetadata(columnName: "mediaSize", columnType: .blob, isOptional: true) }
    static var pointerTypeColumn: SDSColumnMetadata { SDSColumnMetadata(columnName: "pointerType", columnType: .int64, isOptional: true) }
    static var stateColumn: SDSColumnMetadata { SDSColumnMetadata(columnName: "state", columnType: .int, isOptional: true) }
    static var uploadTimestampColumn: SDSColumnMetadata { SDSColumnMetadata(columnName: "uploadTimestamp", columnType: .int64) }
    static var cdnKeyColumn: SDSColumnMetadata { SDSColumnMetadata(columnName: "cdnKey", columnType: .unicodeString) }
    static var cdnNumberColumn: SDSColumnMetadata { SDSColumnMetadata(columnName: "cdnNumber", columnType: .int64) }
    static var isAnimatedCachedColumn: SDSColumnMetadata { SDSColumnMetadata(columnName: "isAnimatedCached", columnType: .int, isOptional: true) }
    static var attachmentSchemaVersionColumn: SDSColumnMetadata { SDSColumnMetadata(columnName: "attachmentSchemaVersion", columnType: .int64) }
    static var videoDurationColumn: SDSColumnMetadata { SDSColumnMetadata(columnName: "videoDuration", columnType: .double, isOptional: true) }
    static var clientUuidColumn: SDSColumnMetadata { SDSColumnMetadata(columnName: "clientUuid", columnType: .unicodeString, isOptional: true) }

    public static var table: SDSTableMetadata {
        SDSTableMetadata(collection: TSAttachment.collection(),
                         tableName: "model_TSAttachment",
                         columns: [
        idColumn,
        recordTypeColumn,
        uniqueIdColumn,
        albumMessageIdColumn,
        attachmentTypeColumn,
        blurHashColumn,
        byteCountColumn,
        captionColumn,
        contentTypeColumn,
        encryptionKeyColumn,
        serverIdColumn,
        sourceFilenameColumn,
        cachedAudioDurationSecondsColumn,
        cachedImageHeightColumn,
        cachedImageWidthColumn,
        creationTimestampColumn,
        digestColumn,
        isUploadedColumn,
        isValidImageCachedColumn,
        isValidVideoCachedColumn,
        lazyRestoreFragmentIdColumn,
        localRelativeFilePathColumn,
        mediaSizeColumn,
        pointerTypeColumn,
        stateColumn,
        uploadTimestampColumn,
        cdnKeyColumn,
        cdnNumberColumn,
        isAnimatedCachedColumn,
        attachmentSchemaVersionColumn,
        videoDurationColumn,
        clientUuidColumn
        ])
    }
}

// MARK: - Save/Remove/Update

@objc
public extension TSAttachment {
    func anyInsert(transaction: SDSAnyWriteTransaction) {
        sdsSave(saveMode: .insert, transaction: transaction)
    }

    // Avoid this method whenever feasible.
    //
    // If the record has previously been saved, this method does an overwriting
    // update of the corresponding row, otherwise if it's a new record, this
    // method inserts a new row.
    //
    // For performance, when possible, you should explicitly specify whether
    // you are inserting or updating rather than calling this method.
    func anyUpsert(transaction: SDSAnyWriteTransaction) {
        let isInserting: Bool
        if TSAttachment.anyFetch(uniqueId: uniqueId, transaction: transaction) != nil {
            isInserting = false
        } else {
            isInserting = true
        }
        sdsSave(saveMode: isInserting ? .insert : .update, transaction: transaction)
    }

    // This method is used by "updateWith..." methods.
    //
    // This model may be updated from many threads. We don't want to save
    // our local copy (this instance) since it may be out of date.  We also
    // want to avoid re-saving a model that has been deleted.  Therefore, we
    // use "updateWith..." methods to:
    //
    // a) Update a property of this instance.
    // b) If a copy of this model exists in the database, load an up-to-date copy,
    //    and update and save that copy.
    // b) If a copy of this model _DOES NOT_ exist in the database, do _NOT_ save
    //    this local instance.
    //
    // After "updateWith...":
    //
    // a) Any copy of this model in the database will have been updated.
    // b) The local property on this instance will always have been updated.
    // c) Other properties on this instance may be out of date.
    //
    // All mutable properties of this class have been made read-only to
    // prevent accidentally modifying them directly.
    //
    // This isn't a perfect arrangement, but in practice this will prevent
    // data loss and will resolve all known issues.
    func anyUpdate(transaction: SDSAnyWriteTransaction, block: (TSAttachment) -> Void) {

        block(self)

        guard let dbCopy = type(of: self).anyFetch(uniqueId: uniqueId,
                                                   transaction: transaction) else {
            return
        }

        // Don't apply the block twice to the same instance.
        // It's at least unnecessary and actually wrong for some blocks.
        // e.g. `block: { $0 in $0.someField++ }`
        if dbCopy !== self {
            block(dbCopy)
        }

        dbCopy.sdsSave(saveMode: .update, transaction: transaction)
    }

    // This method is an alternative to `anyUpdate(transaction:block:)` methods.
    //
    // We should generally use `anyUpdate` to ensure we're not unintentionally
    // clobbering other columns in the database when another concurrent update
    // has occurred.
    //
    // There are cases when this doesn't make sense, e.g. when  we know we've
    // just loaded the model in the same transaction. In those cases it is
    // safe and faster to do a "overwriting" update
    func anyOverwritingUpdate(transaction: SDSAnyWriteTransaction) {
        sdsSave(saveMode: .update, transaction: transaction)
    }

    func anyRemove(transaction: SDSAnyWriteTransaction) {
        sdsRemove(transaction: transaction)
    }

    func anyReload(transaction: SDSAnyReadTransaction) {
        anyReload(transaction: transaction, ignoreMissing: false)
    }

    func anyReload(transaction: SDSAnyReadTransaction, ignoreMissing: Bool) {
        guard let latestVersion = type(of: self).anyFetch(uniqueId: uniqueId, transaction: transaction) else {
            if !ignoreMissing {
                owsFailDebug("`latest` was unexpectedly nil")
            }
            return
        }

        setValuesForKeys(latestVersion.dictionaryValue)
    }
}

// MARK: - TSAttachmentCursor

@objc
public class TSAttachmentCursor: NSObject, SDSCursor {
    private let transaction: GRDBReadTransaction
    private let cursor: RecordCursor<AttachmentRecord>?

    init(transaction: GRDBReadTransaction, cursor: RecordCursor<AttachmentRecord>?) {
        self.transaction = transaction
        self.cursor = cursor
    }

    public func next() throws -> TSAttachment? {
        guard let cursor = cursor else {
            return nil
        }
        guard let record = try cursor.next() else {
            return nil
        }
        let value = try TSAttachment.fromRecord(record)
        Self.modelReadCaches.attachmentReadCache.didReadAttachment(value, transaction: transaction.asAnyRead)
        return value
    }

    public func all() throws -> [TSAttachment] {
        var result = [TSAttachment]()
        while true {
            guard let model = try next() else {
                break
            }
            result.append(model)
        }
        return result
    }
}

// MARK: - Obj-C Fetch

@objc
public extension TSAttachment {
    class func grdbFetchCursor(transaction: GRDBReadTransaction) -> TSAttachmentCursor {
        let database = transaction.database
        do {
            let cursor = try AttachmentRecord.fetchCursor(database)
            return TSAttachmentCursor(transaction: transaction, cursor: cursor)
        } catch {
            DatabaseCorruptionState.flagDatabaseReadCorruptionIfNecessary(
                userDefaults: CurrentAppContext().appUserDefaults(),
                error: error
            )
            owsFailDebug("Read failed: \(error)")
            return TSAttachmentCursor(transaction: transaction, cursor: nil)
        }
    }

    // Fetches a single model by "unique id".
    class func anyFetch(uniqueId: String,
                        transaction: SDSAnyReadTransaction) -> TSAttachment? {
        assert(!uniqueId.isEmpty)

        return anyFetch(uniqueId: uniqueId, transaction: transaction, ignoreCache: false)
    }

    // Fetches a single model by "unique id".
    class func anyFetch(uniqueId: String,
                        transaction: SDSAnyReadTransaction,
                        ignoreCache: Bool) -> TSAttachment? {
        assert(!uniqueId.isEmpty)

        if !ignoreCache,
            let cachedCopy = Self.modelReadCaches.attachmentReadCache.getAttachment(uniqueId: uniqueId, transaction: transaction) {
            return cachedCopy
        }

        switch transaction.readTransaction {
        case .grdbRead(let grdbTransaction):
            let sql = "SELECT * FROM \(AttachmentRecord.databaseTableName) WHERE \(attachmentColumn: .uniqueId) = ?"
            return grdbFetchOne(sql: sql, arguments: [uniqueId], transaction: grdbTransaction)
        }
    }

    // Traverses all records.
    // Records are not visited in any particular order.
    class func anyEnumerate(
        transaction: SDSAnyReadTransaction,
        block: (TSAttachment, UnsafeMutablePointer<ObjCBool>) -> Void
    ) {
        anyEnumerate(transaction: transaction, batched: false, block: block)
    }

    // Traverses all records.
    // Records are not visited in any particular order.
    class func anyEnumerate(
        transaction: SDSAnyReadTransaction,
        batched: Bool = false,
        block: (TSAttachment, UnsafeMutablePointer<ObjCBool>) -> Void
    ) {
        let batchSize = batched ? Batching.kDefaultBatchSize : 0
        anyEnumerate(transaction: transaction, batchSize: batchSize, block: block)
    }

    // Traverses all records.
    // Records are not visited in any particular order.
    //
    // If batchSize > 0, the enumeration is performed in autoreleased batches.
    class func anyEnumerate(
        transaction: SDSAnyReadTransaction,
        batchSize: UInt,
        block: (TSAttachment, UnsafeMutablePointer<ObjCBool>) -> Void
    ) {
        switch transaction.readTransaction {
        case .grdbRead(let grdbTransaction):
            let cursor = TSAttachment.grdbFetchCursor(transaction: grdbTransaction)
            Batching.loop(batchSize: batchSize,
                          loopBlock: { stop in
                                do {
                                    guard let value = try cursor.next() else {
                                        stop.pointee = true
                                        return
                                    }
                                    block(value, stop)
                                } catch let error {
                                    owsFailDebug("Couldn't fetch model: \(error)")
                                }
                              })
        }
    }

    // Traverses all records' unique ids.
    // Records are not visited in any particular order.
    class func anyEnumerateUniqueIds(
        transaction: SDSAnyReadTransaction,
        block: (String, UnsafeMutablePointer<ObjCBool>) -> Void
    ) {
        anyEnumerateUniqueIds(transaction: transaction, batched: false, block: block)
    }

    // Traverses all records' unique ids.
    // Records are not visited in any particular order.
    class func anyEnumerateUniqueIds(
        transaction: SDSAnyReadTransaction,
        batched: Bool = false,
        block: (String, UnsafeMutablePointer<ObjCBool>) -> Void
    ) {
        let batchSize = batched ? Batching.kDefaultBatchSize : 0
        anyEnumerateUniqueIds(transaction: transaction, batchSize: batchSize, block: block)
    }

    // Traverses all records' unique ids.
    // Records are not visited in any particular order.
    //
    // If batchSize > 0, the enumeration is performed in autoreleased batches.
    class func anyEnumerateUniqueIds(
        transaction: SDSAnyReadTransaction,
        batchSize: UInt,
        block: (String, UnsafeMutablePointer<ObjCBool>) -> Void
    ) {
        switch transaction.readTransaction {
        case .grdbRead(let grdbTransaction):
            grdbEnumerateUniqueIds(transaction: grdbTransaction,
                                   sql: """
                    SELECT \(attachmentColumn: .uniqueId)
                    FROM \(AttachmentRecord.databaseTableName)
                """,
                batchSize: batchSize,
                block: block)
        }
    }

    // Does not order the results.
    class func anyFetchAll(transaction: SDSAnyReadTransaction) -> [TSAttachment] {
        var result = [TSAttachment]()
        anyEnumerate(transaction: transaction) { (model, _) in
            result.append(model)
        }
        return result
    }

    // Does not order the results.
    class func anyAllUniqueIds(transaction: SDSAnyReadTransaction) -> [String] {
        var result = [String]()
        anyEnumerateUniqueIds(transaction: transaction) { (uniqueId, _) in
            result.append(uniqueId)
        }
        return result
    }

    class func anyCount(transaction: SDSAnyReadTransaction) -> UInt {
        switch transaction.readTransaction {
        case .grdbRead(let grdbTransaction):
            return AttachmentRecord.ows_fetchCount(grdbTransaction.database)
        }
    }

    class func anyRemoveAllWithInstantiation(transaction: SDSAnyWriteTransaction) {
        // To avoid mutationDuringEnumerationException, we need to remove the
        // instances outside the enumeration.
        let uniqueIds = anyAllUniqueIds(transaction: transaction)

        for uniqueId in uniqueIds {
            autoreleasepool {
                guard let instance = anyFetch(uniqueId: uniqueId, transaction: transaction) else {
                    owsFailDebug("Missing instance.")
                    return
                }
                instance.anyRemove(transaction: transaction)
            }
        }
    }

    class func anyExists(
        uniqueId: String,
        transaction: SDSAnyReadTransaction
    ) -> Bool {
        assert(!uniqueId.isEmpty)

        switch transaction.readTransaction {
        case .grdbRead(let grdbTransaction):
            let sql = "SELECT EXISTS ( SELECT 1 FROM \(AttachmentRecord.databaseTableName) WHERE \(attachmentColumn: .uniqueId) = ? )"
            let arguments: StatementArguments = [uniqueId]
            do {
                return try Bool.fetchOne(grdbTransaction.database, sql: sql, arguments: arguments) ?? false
            } catch {
                DatabaseCorruptionState.flagDatabaseReadCorruptionIfNecessary(
                    userDefaults: CurrentAppContext().appUserDefaults(),
                    error: error
                )
                owsFail("Missing instance.")
            }
        }
    }
}

// MARK: - Swift Fetch

public extension TSAttachment {
    class func grdbFetchCursor(sql: String,
                               arguments: StatementArguments = StatementArguments(),
                               transaction: GRDBReadTransaction) -> TSAttachmentCursor {
        do {
            let sqlRequest = SQLRequest<Void>(sql: sql, arguments: arguments, cached: true)
            let cursor = try AttachmentRecord.fetchCursor(transaction.database, sqlRequest)
            return TSAttachmentCursor(transaction: transaction, cursor: cursor)
        } catch {
            DatabaseCorruptionState.flagDatabaseReadCorruptionIfNecessary(
                userDefaults: CurrentAppContext().appUserDefaults(),
                error: error
            )
            owsFailDebug("Read failed: \(error)")
            return TSAttachmentCursor(transaction: transaction, cursor: nil)
        }
    }

    class func grdbFetchOne(sql: String,
                            arguments: StatementArguments = StatementArguments(),
                            transaction: GRDBReadTransaction) -> TSAttachment? {
        assert(!sql.isEmpty)

        do {
            let sqlRequest = SQLRequest<Void>(sql: sql, arguments: arguments, cached: true)
            guard let record = try AttachmentRecord.fetchOne(transaction.database, sqlRequest) else {
                return nil
            }

            let value = try TSAttachment.fromRecord(record)
            Self.modelReadCaches.attachmentReadCache.didReadAttachment(value, transaction: transaction.asAnyRead)
            return value
        } catch {
            owsFailDebug("error: \(error)")
            return nil
        }
    }
}

// MARK: - SDSSerializer

// The SDSSerializer protocol specifies how to insert and update the
// row that corresponds to this model.
class TSAttachmentSerializer: SDSSerializer {

    private let model: TSAttachment
    public init(model: TSAttachment) {
        self.model = model
    }

    // MARK: - Record

    func asRecord() throws -> SDSRecord {
        let id: Int64? = model.grdbId?.int64Value

        let recordType: SDSRecordType = .attachment
        let uniqueId: String = model.uniqueId

        // Properties
        let albumMessageId: String? = model.albumMessageId
        let attachmentType: TSAttachmentType = model.attachmentType
        let blurHash: String? = model.blurHash
        let byteCount: UInt32 = model.byteCount
        let caption: String? = model.caption
        let contentType: String = model.contentType
        let encryptionKey: Data? = model.encryptionKey
        let serverId: UInt64 = model.serverId
        let sourceFilename: String? = model.sourceFilename
        let cachedAudioDurationSeconds: Double? = nil
        let cachedImageHeight: Double? = nil
        let cachedImageWidth: Double? = nil
        let creationTimestamp: Double? = nil
        let digest: Data? = nil
        let isUploaded: Bool? = nil
        let isValidImageCached: Bool? = nil
        let isValidVideoCached: Bool? = nil
        let lazyRestoreFragmentId: String? = nil
        let localRelativeFilePath: String? = nil
        let mediaSize: Data? = nil
        let pointerType: UInt? = nil
        let state: TSAttachmentPointerState? = nil
        let uploadTimestamp: UInt64 = model.uploadTimestamp
        let cdnKey: String = model.cdnKey
        let cdnNumber: UInt32 = model.cdnNumber
        let isAnimatedCached: Bool? = nil
        let attachmentSchemaVersion: UInt = model.attachmentSchemaVersion
        let videoDuration: Double? = archiveOptionalNSNumber(model.videoDuration, conversion: { $0.doubleValue })
        let clientUuid: String? = model.clientUuid

        return AttachmentRecord(delegate: model, id: id, recordType: recordType, uniqueId: uniqueId, albumMessageId: albumMessageId, attachmentType: attachmentType, blurHash: blurHash, byteCount: byteCount, caption: caption, contentType: contentType, encryptionKey: encryptionKey, serverId: serverId, sourceFilename: sourceFilename, cachedAudioDurationSeconds: cachedAudioDurationSeconds, cachedImageHeight: cachedImageHeight, cachedImageWidth: cachedImageWidth, creationTimestamp: creationTimestamp, digest: digest, isUploaded: isUploaded, isValidImageCached: isValidImageCached, isValidVideoCached: isValidVideoCached, lazyRestoreFragmentId: lazyRestoreFragmentId, localRelativeFilePath: localRelativeFilePath, mediaSize: mediaSize, pointerType: pointerType, state: state, uploadTimestamp: uploadTimestamp, cdnKey: cdnKey, cdnNumber: cdnNumber, isAnimatedCached: isAnimatedCached, attachmentSchemaVersion: attachmentSchemaVersion, videoDuration: videoDuration, clientUuid: clientUuid)
    }
}

// MARK: - Deep Copy

#if TESTABLE_BUILD
@objc
public extension TSAttachment {
    // We're not using this method at the moment,
    // but we might use it for validation of
    // other deep copy methods.
    func deepCopyUsingRecord() throws -> TSAttachment {
        guard let record = try asRecord() as? AttachmentRecord else {
            throw OWSAssertionError("Could not convert to record.")
        }
        return try TSAttachment.fromRecord(record)
    }
}
#endif
