import Foundation import SQLite3 // SQLITE_TRANSIENT tells SQLite to make its own copy of bound text data private let SQLITE_TRANSIENT = unsafeBitCast(-1, to: sqlite3_destructor_type.self) final class DatabaseManager { static let shared = DatabaseManager() private var db: OpaquePointer? private init() { openDatabase() createTable() } deinit { if db != nil { sqlite3_close(db) } } // MARK: - Database Setup private func openDatabase() { let fileURL = getDatabasePath() if sqlite3_open(fileURL.path, &db) != SQLITE_OK { print("Error opening database: \(String(cString: sqlite3_errmsg(db)))") db = nil } } private func getDatabasePath() -> URL { let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask) return paths[0].appendingPathComponent("todos.db") } private func createTable() { let sql = """ CREATE TABLE IF NOT EXISTS todos ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, completed INTEGER DEFAULT 0, category TEXT DEFAULT 'Personal', priority TEXT DEFAULT 'medium', created_at INTEGER DEFAULT (strftime('%s','now')) ) """ var statement: OpaquePointer? defer { sqlite3_finalize(statement) } if sqlite3_prepare_v2(db, sql, -1, &statement, nil) == SQLITE_OK { if sqlite3_step(statement) != SQLITE_DONE { print("Error creating table: \(String(cString: sqlite3_errmsg(db)))") } } else { print("Error preparing create table: \(String(cString: sqlite3_errmsg(db)))") } } // MARK: - Helper private func bindText(_ statement: OpaquePointer?, index: Int32, value: String) { sqlite3_bind_text(statement, index, value, -1, SQLITE_TRANSIENT) } // MARK: - CRUD Operations func loadTodos() -> [Todo] { let sql = "SELECT id, title, completed, category, priority, created_at FROM todos ORDER BY created_at DESC" var statement: OpaquePointer? defer { sqlite3_finalize(statement) } var todos: [Todo] = [] if sqlite3_prepare_v2(db, sql, -1, &statement, nil) == SQLITE_OK { while sqlite3_step(statement) == SQLITE_ROW { let id = sqlite3_column_int64(statement, 0) let title = String(cString: sqlite3_column_text(statement, 1)) let completed = sqlite3_column_int(statement, 2) == 1 let category = String(cString: sqlite3_column_text(statement, 3)) let priorityStr = String(cString: sqlite3_column_text(statement, 4)) let priority = Priority(rawValue: priorityStr) ?? .medium let createdAt = sqlite3_column_int64(statement, 5) let todo = Todo( id: id, title: title, completed: completed, category: category, priority: priority, createdAt: createdAt ) todos.append(todo) } } else { print("Error loading todos: \(String(cString: sqlite3_errmsg(db)))") } return todos } @discardableResult func insertTodo(title: String, category: String, priority: Priority) -> Todo? { let sql = "INSERT INTO todos (title, category, priority, completed) VALUES (?, ?, ?, 0)" var statement: OpaquePointer? defer { sqlite3_finalize(statement) } guard sqlite3_prepare_v2(db, sql, -1, &statement, nil) == SQLITE_OK else { print("Error preparing insert: \(String(cString: sqlite3_errmsg(db)))") return nil } bindText(statement, index: 1, value: title) bindText(statement, index: 2, value: category) bindText(statement, index: 3, value: priority.rawValue) guard sqlite3_step(statement) == SQLITE_DONE else { print("Error inserting todo: \(String(cString: sqlite3_errmsg(db)))") return nil } let id = sqlite3_last_insert_rowid(db) let createdAt = Int64(Date().timeIntervalSince1970) return Todo( id: id, title: title, completed: false, category: category, priority: priority, createdAt: createdAt ) } func updateTodo(id: Int64, title: String, category: String, priority: Priority) { let sql = "UPDATE todos SET title = ?, category = ?, priority = ? WHERE id = ?" var statement: OpaquePointer? defer { sqlite3_finalize(statement) } guard sqlite3_prepare_v2(db, sql, -1, &statement, nil) == SQLITE_OK else { print("Error preparing update: \(String(cString: sqlite3_errmsg(db)))") return } bindText(statement, index: 1, value: title) bindText(statement, index: 2, value: category) bindText(statement, index: 3, value: priority.rawValue) sqlite3_bind_int64(statement, 4, id) if sqlite3_step(statement) != SQLITE_DONE { print("Error updating todo: \(String(cString: sqlite3_errmsg(db)))") } } func toggleTodo(id: Int64, completed: Bool) { let sql = "UPDATE todos SET completed = ? WHERE id = ?" var statement: OpaquePointer? defer { sqlite3_finalize(statement) } guard sqlite3_prepare_v2(db, sql, -1, &statement, nil) == SQLITE_OK else { print("Error preparing toggle: \(String(cString: sqlite3_errmsg(db)))") return } sqlite3_bind_int(statement, 1, completed ? 1 : 0) sqlite3_bind_int64(statement, 2, id) if sqlite3_step(statement) != SQLITE_DONE { print("Error toggling todo: \(String(cString: sqlite3_errmsg(db)))") } } func deleteTodo(id: Int64) { let sql = "DELETE FROM todos WHERE id = ?" var statement: OpaquePointer? defer { sqlite3_finalize(statement) } guard sqlite3_prepare_v2(db, sql, -1, &statement, nil) == SQLITE_OK else { print("Error preparing delete: \(String(cString: sqlite3_errmsg(db)))") return } sqlite3_bind_int64(statement, 1, id) if sqlite3_step(statement) != SQLITE_DONE { print("Error deleting todo: \(String(cString: sqlite3_errmsg(db)))") } } }