//! SQLite Serialize/Deserialize API
//!
//! Provides functions to serialize a database to a byte array and deserialize
//! a byte array back into a database. This is useful for:
//!
//! - Transferring databases over network connections
//! - Storing databases as BLOBs in other databases
//! - Creating in-memory snapshots of databases
//! - Implementing custom backup solutions
//!
//! ## Quick Start
//!
//! ```zig
//! const sqlite = @import("zsqlite");
//!
//! // Serialize a database to bytes
//! var db = try sqlite.open(":memory:");
//! try db.exec("CREATE TABLE test (x)");
//! try db.exec("INSERT INTO test VALUES (42)");
//!
//! const bytes = try sqlite.serialize.toBytes(&db, allocator, "main");
//! defer allocator.free(bytes);
//!
//! // Deserialize bytes into a new database
//! var db2 = try sqlite.serialize.fromBytes(bytes, ":memory:");
//! defer db2.close();
//!
//! // db2 now contains the same data as db
//! ```
//!
//! ## Notes
//!
//! - Requires SQLite 3.36.0 or later (SQLITE_ENABLE_DESERIALIZE)
//! - Serialized data is in SQLite's native page format
//! - Deserialize creates a resizable in-memory database

const std = @import("std");
const c = @import("c.zig").c;
const errors = @import("errors.zig");
const Database = @import("database.zig").Database;

const Error = errors.Error;
const resultToError = errors.resultToError;

/// Serialization flags for sqlite3_serialize.
pub const SerializeFlags = packed struct {
    /// Return no copy - the returned pointer points to SQLite's internal buffer.
    /// The caller must not modify or free this pointer, and it becomes invalid
    /// when the database changes.
    no_copy: bool = false,

    _padding: u31 = 0,

    pub fn toInt(self: SerializeFlags) c_uint {
        return @bitCast(self);
    }
};

/// Deserialization flags for sqlite3_deserialize.
pub const DeserializeFlags = packed struct {
    /// The deserialized database is read-only.
    read_only: bool = false,
    /// Free the byte array when done (SQLite takes ownership).
    free_on_close: bool = false,
    /// The database can grow by resizing the byte array.
    resizable: bool = false,

    _padding: u29 = 0,

    pub fn toInt(self: DeserializeFlags) c_uint {
        return @bitCast(self);
    }
};

// ============================================================================
// Core Serialize/Deserialize Functions
// ============================================================================

/// Serializes a database to a byte array.
///
/// This function creates a copy of the database in its native SQLite format.
/// The returned slice is owned by the caller and must be freed with the
/// provided allocator.
///
/// Parameters:
/// - `db`: The database to serialize
/// - `allocator`: Allocator for the returned byte array
/// - `schema`: Database schema to serialize ("main", "temp", or attached db name)
///
/// Returns: Slice containing the serialized database, or error
///
/// Example:
/// ```zig
/// const bytes = try serialize.toBytes(&db, allocator, "main");
/// defer allocator.free(bytes);
/// // Save bytes to file, send over network, etc.
/// ```
pub fn toBytes(db: *Database, allocator: std.mem.Allocator, schema: [:0]const u8) ![]u8 {
    var size: i64 = 0;

    const ptr = c.sqlite3_serialize(
        db.handle,
        schema.ptr,
        &size,
        0, // Copy the data
    );

    if (ptr == null) {
        // sqlite3_serialize returns NULL for empty databases or on error
        // Check if the database is empty by querying its size
        if (size == 0) {
            // Empty database - return empty slice
            return &[_]u8{};
        }
        return Error.OutOfMemory;
    }

    defer c.sqlite3_free(ptr);

    const len: usize = @intCast(size);
    const result = try allocator.alloc(u8, len);
    errdefer allocator.free(result);

    const src: [*]const u8 = @ptrCast(ptr);
    @memcpy(result, src[0..len]);

    return result;
}

/// Serializes a database without copying (returns pointer to internal buffer).
///
/// WARNING: The returned slice points to SQLite's internal buffer and:
/// - Must NOT be freed by the caller
/// - Becomes invalid when the database is modified or closed
/// - Is only suitable for immediate read-only operations
///
/// Use `toBytes` for a safe copy that persists independently.
///
/// Parameters:
/// - `db`: The database to serialize
/// - `schema`: Database schema to serialize
///
/// Returns: Slice pointing to internal buffer (do not free!), or null
pub fn toBytesNoCopy(db: *Database, schema: [:0]const u8) ?[]const u8 {
    var size: i64 = 0;

    const ptr = c.sqlite3_serialize(
        db.handle,
        schema.ptr,
        &size,
        c.SQLITE_SERIALIZE_NOCOPY,
    );

    if (ptr == null or size <= 0) return null;

    const len: usize = @intCast(size);
    const src: [*]const u8 = @ptrCast(ptr);
    return src[0..len];
}

/// Deserializes a byte array into a database.
///
/// Creates a new database connection and populates it with the data from
/// the serialized byte array. The new database is resizable and writable
/// by default.
///
/// Parameters:
/// - `data`: Serialized database bytes (from `toBytes` or file)
/// - `path`: Path for the database (use ":memory:" for in-memory)
///
/// Returns: New database connection, or error
///
/// Example:
/// ```zig
/// // Load serialized bytes from file
/// const bytes = try std.fs.cwd().readFileAlloc(allocator, "backup.db", max_size);
/// defer allocator.free(bytes);
///
/// // Create database from bytes
/// var db = try serialize.fromBytes(bytes, ":memory:");
/// defer db.close();
/// ```
pub fn fromBytes(data: []const u8, path: [:0]const u8) Error!Database {
    return fromBytesWithFlags(data, path, .{ .resizable = true });
}

/// Deserializes a byte array into a database with custom flags.
///
/// Parameters:
/// - `data`: Serialized database bytes
/// - `path`: Path for the database
/// - `flags`: Deserialization options
///
/// Returns: New database connection, or error
pub fn fromBytesWithFlags(data: []const u8, path: [:0]const u8, flags: DeserializeFlags) Error!Database {
    // Open a new database connection
    var db = try Database.open(path);
    errdefer db.close();

    // Deserialize into the connection
    try deserializeInto(&db, "main", data, flags);

    return db;
}

/// Deserializes a byte array into an existing database connection.
///
/// This replaces the contents of the specified schema in the database
/// with the deserialized data.
///
/// Parameters:
/// - `db`: Target database connection
/// - `schema`: Schema name ("main", "temp", or attached db)
/// - `data`: Serialized database bytes
/// - `flags`: Deserialization options
///
/// Example:
/// ```zig
/// var db = try sqlite.open(":memory:");
/// defer db.close();
///
/// // Load data from bytes
/// try serialize.deserializeInto(&db, "main", bytes, .{ .resizable = true });
/// ```
pub fn deserializeInto(
    db: *Database,
    schema: [:0]const u8,
    data: []const u8,
    flags: DeserializeFlags,
) Error!void {
    if (data.len == 0) {
        // Nothing to deserialize
        return;
    }

    // SQLite needs the buffer to be allocated with sqlite3_malloc for FREEONCLOSE
    // We'll always make a copy to be safe
    const buf_ptr = c.sqlite3_malloc64(@intCast(data.len));
    if (buf_ptr == null) return Error.OutOfMemory;

    const buf: [*]u8 = @ptrCast(buf_ptr);
    @memcpy(buf[0..data.len], data);

    // Combine flags - always free the buffer we allocated
    var combined_flags = flags;
    combined_flags.free_on_close = true;

    const result = c.sqlite3_deserialize(
        db.handle,
        schema.ptr,
        buf,
        @intCast(data.len),
        @intCast(data.len),
        combined_flags.toInt(),
    );

    if (result != c.SQLITE_OK) {
        c.sqlite3_free(buf_ptr);
        return resultToError(result);
    }
}

/// Deserializes a byte array as a read-only database.
///
/// The database cannot be modified after deserialization.
/// This is slightly more efficient than a writable database.
pub fn fromBytesReadOnly(data: []const u8, path: [:0]const u8) Error!Database {
    return fromBytesWithFlags(data, path, .{ .read_only = true });
}

// ============================================================================
// High-Level Convenience Functions
// ============================================================================

/// Saves a database to a file using serialization.
///
/// This is an alternative to the Backup API that works by serializing
/// the entire database to memory first. Better for small databases.
///
/// Example:
/// ```zig
/// try serialize.saveToFile(&db, allocator, "backup.sqlite");
/// ```
pub fn saveToFile(db: *Database, allocator: std.mem.Allocator, path: []const u8) !void {
    const bytes = try toBytes(db, allocator, "main");
    defer allocator.free(bytes);

    const file = try std.fs.cwd().createFile(path, .{});
    defer file.close();

    try file.writeAll(bytes);
}

/// Loads a database from a file using deserialization.
///
/// Creates an in-memory database from a file's contents.
///
/// Example:
/// ```zig
/// var db = try serialize.loadFromFile(allocator, "backup.sqlite");
/// defer db.close();
/// ```
pub fn loadFromFile(allocator: std.mem.Allocator, path: []const u8) !Database {
    const file = try std.fs.cwd().openFile(path, .{});
    defer file.close();

    const stat = try file.stat();
    const bytes = try allocator.alloc(u8, stat.size);
    defer allocator.free(bytes);

    const read = try file.readAll(bytes);
    if (read != stat.size) return error.UnexpectedEndOfFile;

    return try fromBytes(bytes, ":memory:");
}

/// Creates an in-memory clone of a database.
///
/// This is useful for creating a snapshot that can be modified without
/// affecting the original database.
///
/// Example:
/// ```zig
/// var clone = try serialize.cloneToMemory(&db, allocator);
/// defer clone.close();
/// // Modify clone without affecting original db
/// ```
pub fn cloneToMemory(db: *Database, allocator: std.mem.Allocator) !Database {
    const bytes = try toBytes(db, allocator, "main");
    defer allocator.free(bytes);

    return try fromBytes(bytes, ":memory:");
}

/// Compares two databases for equality by comparing their serialized forms.
///
/// Note: This compares the raw page data, so databases with the same
/// logical content but different physical layouts will compare as different.
/// Use VACUUM on both databases first for a reliable comparison.
pub fn equals(db1: *Database, db2: *Database, allocator: std.mem.Allocator) !bool {
    const bytes1 = try toBytes(db1, allocator, "main");
    defer allocator.free(bytes1);

    const bytes2 = try toBytes(db2, allocator, "main");
    defer allocator.free(bytes2);

    return std.mem.eql(u8, bytes1, bytes2);
}

/// Returns the serialized size of a database without creating a copy.
///
/// Useful for pre-allocating buffers or checking database size.
pub fn serializedSize(db: *Database, schema: [:0]const u8) i64 {
    var size: i64 = 0;

    const ptr = c.sqlite3_serialize(
        db.handle,
        schema.ptr,
        &size,
        c.SQLITE_SERIALIZE_NOCOPY,
    );

    // We don't need to free since we used NOCOPY
    _ = ptr;

    return size;
}
