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

import Wire

extension MessageBackup {
    public enum ProtoOutputStreamWriteResult {
        case success
        /// Unable to serialize the provided proto object.
        /// Should never happen, and catastrophic if it does.
        case protoSerializationError(Swift.Error)
        /// Failure writing at file I/O level.
        case fileIOError(Swift.Error)
    }
}

/**
 * Output stream for reading and writing a backup file on disk.
 *
 * The backup file is just a sequence of serialized proto bytes, back to back, delimited by varint
 * byte sizes so we know how much to read into memory to deserialize the next proto.
 * The output stream abstracts over this, and allows callers to just think in terms of "frames",
 * the individual proto objects that we write one at a time.
 */
public protocol MessageBackupProtoOutputStream {

    /// Write a header (BakckupInfo) to the backup file.
    /// It is the caller's responsibility to ensure this is always written, and is the first thing written,
    /// in order to produce a valid backup file.
    func writeHeader(_ header: BackupProto.BackupInfo) -> MessageBackup.ProtoOutputStreamWriteResult

    /// Write a frame to the backup file.
    func writeFrame(_ frame: BackupProto.Frame) -> MessageBackup.ProtoOutputStreamWriteResult

    /// Closes the output stream.
    func closeFileStream() throws
}

internal class MessageBackupProtoOutputStreamImpl: MessageBackupProtoOutputStream {

    private let outputStream: OutputStreamable

    internal init(outputStream: OutputStreamable) {
        self.outputStream = outputStream
    }

    internal func writeHeader(_ header: BackupProto.BackupInfo) -> MessageBackup.ProtoOutputStreamWriteResult {
        let bytes: Data
        do {
            bytes = try ProtoEncoder().encode(header)
        } catch {
            return .protoSerializationError(error)
        }
        do {
            try outputStream.write(data: bytes)
        } catch {
            return .fileIOError(error)
        }
        return .success
    }

    internal func writeFrame(_ frame: BackupProto.Frame) -> MessageBackup.ProtoOutputStreamWriteResult {
        let bytes: Data
        do {
            bytes = try ProtoEncoder().encode(frame)
        } catch {
            return .protoSerializationError(error)
        }
        do {
            try outputStream.write(data: bytes)
        } catch {
            return .fileIOError(error)
        }
        return .success
    }

    public func closeFileStream() throws {
        try? outputStream.close()
    }
}
