//
//  DuckDB
//  https://github.com/duckdb/duckdb-swift
//
//  Copyright © 2018-2024 Stichting DuckDB Foundation
//
//  Permission is hereby granted, free of charge, to any person obtaining a copy
//  of this software and associated documentation files (the "Software"), to
//  deal in the Software without restriction, including without limitation the
//  rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
//  sell copies of the Software, and to permit persons to whom the Software is
//  furnished to do so, subject to the following conditions:
//
//  The above copyright notice and this permission notice shall be included in
//  all copies or substantial portions of the Software.
//
//  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
//  FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
//  IN THE SOFTWARE.

#if compiler(>=6.0)
internal import Cduckdb
#else
@_implementationOnly import Cduckdb
#endif
import Foundation

/// An object that efficiently appends data to a DuckDB table
///
/// Appenders are the most efficient way of loading data into DuckDB from within
/// Swift, and are recommended for fast data loading. The appender is much
/// faster than using prepared statements or individual INSERT INTO statements.
///
/// Appends are made in row-wise format. For every column an `append(_:)` call
/// should be made after which the row should be finished by calling
/// ``endRow()``.
///
/// The following example shows two rows being appended to a table with a two
/// column layout.
///
/// ```swift
/// do {
///   let database = try Database(store: .inMemory)
///   let connection = try database.connect()
///   try connection.execute("CREATE TABLE people(id INTEGER, name VARCHAR)")
///   let appender = try Appender(connection: connection, table: "people")
///   // add first row
///   try appender.append(Int32(1))
///   try appender.append("Mark")
///   try appender.endRow()
///   // add second row
///   try appender.append(Int32(2))
///   try appender.append("Hannes")
///   try appender.endRow()
///   // flush rows to table
///   try appender.flush()
/// }
/// catch {
///   // handle error
/// }
/// ```
public final class Appender {

  private let connection: Connection
  private let ptr = UnsafeMutablePointer<duckdb_appender?>.allocate(capacity: 1)

  /// Creates a new appender
  ///
  /// Instantiates an ``Appender`` through which table rows can be efficiently
  /// added
  ///
  /// - Parameter connection: the connection through which rows should be
  ///   appended
  /// - Parameter schema: the database schema (defaults to `main`)
  /// - Parameter table: the table to append to
  /// - Throws: ``DatabaseError/appenderFailedToInitialize(reason:)`` if the
  ///   appender failed to instantiate
  public init(connection: Connection, schema: String? = nil, table: String) throws {
    self.connection = connection
    let status = schema.withOptionalCString { schemaStrPtr in
      table.withCString { tableStrPtr in
        connection.withCConnection { duckdb_appender_create($0, schemaStrPtr, tableStrPtr, ptr) }
      }
    }
    guard .success == status else {
      throw DatabaseError.appenderFailedToInitialize(reason: appenderError())
    }
  }

  deinit {
    try? flush()
    duckdb_appender_destroy(ptr)
    ptr.deallocate()
  }
  
  /// Signals the end of the current row
  ///
  /// After all columns for a row have been appended, ``endRow()`` must be
  /// called to indicate that the row is ready to be added to the database.
  ///
  /// - Parameter value: the value to append
  /// - Throws: ``DatabaseError/appenderFailedToEndRow(reason:)``
  ///   if the row could not be completed in its current state
  public func endRow() throws {
    let status = duckdb_appender_end_row(ptr.pointee)
    guard .success == status else {
      throw DatabaseError.appenderFailedToEndRow(reason: appenderError())
    }
  }
  
  /// Flushes pending rows to the database
  ///
  /// To optimize performance, the appender writes rows to the database in
  /// batches. Use `flush()` to immediately write any rows that are pending
  /// insertion.
  ///
  /// - Throws: ``DatabaseError/appenderFailedToFlush(reason:)`` if the pending
  ///   rows failed to be written to the database
  public func flush() throws {
    let status = duckdb_appender_flush(ptr.pointee)
    guard .success == status else {
      throw DatabaseError.appenderFailedToFlush(reason: appenderError())
    }
  }
}

public extension Appender {
  
  /// Appends a value for the current row of the given type
  ///
  /// Appends are made in row-wise format. For every column, an `append(_:)`
  /// call should be made, after which the row should be finished by calling
  /// ``endRow()``.
  ///
  /// - Parameter value: the value to append
  /// - Throws: ``DatabaseError/appenderFailedToAppendItem(reason:)``
  ///   if a value of this type was not expected in the appender's current state
  func append(_ value: Bool?) throws {
    guard let value = try unwrapValueOrAppendNull(value) else { return }
    try withThrowingCommand { duckdb_append_bool(ptr.pointee, value) }
  }
  
  /// Appends a value for the current row of the given type
  ///
  /// Appends are made in row-wise format. For every column, an `append(_:)`
  /// call should be made, after which the row should be finished by calling
  /// ``endRow()``.
  ///
  /// - Parameter value: the value to append
  /// - Throws: ``DatabaseError/appenderFailedToAppendItem(reason:)``
  ///   if a value of this type was not expected in the appender's current state
  func append(_ value: Int8?) throws {
    guard let value = try unwrapValueOrAppendNull(value) else { return }
    try withThrowingCommand { duckdb_append_int8(ptr.pointee, value) }
  }
  
  /// Appends a value for the current row of the given type
  ///
  /// Appends are made in row-wise format. For every column, an `append(_:)`
  /// call should be made, after which the row should be finished by calling
  /// ``endRow()``.
  ///
  /// - Parameter value: the value to append
  /// - Throws: ``DatabaseError/appenderFailedToAppendItem(reason:)``
  ///   if a value of this type was not expected in the appender's current state
  func append(_ value: Int16?) throws {
    guard let value = try unwrapValueOrAppendNull(value) else { return }
    try withThrowingCommand { duckdb_append_int16(ptr.pointee, value) }
  }
  
  /// Appends a value for the current row of the given type
  ///
  /// Appends are made in row-wise format. For every column, an `append(_:)`
  /// call should be made, after which the row should be finished by calling
  /// ``endRow()``.
  ///
  /// - Parameter value: the value to append
  /// - Throws: ``DatabaseError/appenderFailedToAppendItem(reason:)``
  ///   if a value of this type was not expected in the appender's current state
  func append(_ value: Int32?) throws {
    guard let value = try unwrapValueOrAppendNull(value) else { return }
    try withThrowingCommand { duckdb_append_int32(ptr.pointee, value) }
  }
  
  /// Appends a value for the current row of the given type
  ///
  /// Appends are made in row-wise format. For every column, an `append(_:)`
  /// call should be made, after which the row should be finished by calling
  /// ``endRow()``.
  ///
  /// - Parameter value: the value to append
  /// - Throws: ``DatabaseError/appenderFailedToAppendItem(reason:)``
  ///   if a value of this type was not expected in the appender's current state
  func append(_ value: Int64?) throws {
    guard let value = try unwrapValueOrAppendNull(value) else { return }
    try withThrowingCommand { duckdb_append_int64(ptr.pointee, value) }
  }
  
  /// Appends a value for the current row of the given type
  ///
  /// Appends are made in row-wise format. For every column, an `append(_:)`
  /// call should be made, after which the row should be finished by calling
  /// ``endRow()``.
  ///
  /// - Parameter value: the value to append
  /// - Throws: ``DatabaseError/appenderFailedToAppendItem(reason:)``
  ///   if a value of this type was not expected in the appender's current state
  func append(_ value: IntHuge?) throws {
    guard let value = try unwrapValueOrAppendNull(value) else { return }
    try withThrowingCommand { duckdb_append_hugeint(ptr.pointee, .init(value)) }
  }

  /// Appends a value for the current row of the given type
  ///
  /// Appends are made in row-wise format. For every column, an `append(_:)`
  /// call should be made, after which the row should be finished by calling
  /// ``endRow()``.
  ///
  /// - Parameter value: the value to append
  /// - Throws: ``DatabaseError/appenderFailedToAppendItem(reason:)``
  ///   if a value of this type was not expected in the appender's current state
  func append(_ value: UIntHuge?) throws {
    guard let value = try unwrapValueOrAppendNull(value) else { return }
    try withThrowingCommand { duckdb_append_uhugeint(ptr.pointee, .init(value)) }
  }
  
  /// Appends a value for the current row of the given type
  ///
  /// Appends are made in row-wise format. For every column, an `append(_:)`
  /// call should be made, after which the row should be finished by calling
  /// ``endRow()``.
  ///
  /// - Parameter value: the value to append
  /// - Throws: ``DatabaseError/appenderFailedToAppendItem(reason:)``
  ///   if a value of this type was not expected in the appender's current state
  func append(_ value: UInt8?) throws {
    guard let value = try unwrapValueOrAppendNull(value) else { return }
    try withThrowingCommand { duckdb_append_uint8(ptr.pointee, value) }
  }
  
  /// Appends a value for the current row of the given type
  ///
  /// Appends are made in row-wise format. For every column, an `append(_:)`
  /// call should be made, after which the row should be finished by calling
  /// ``endRow()``.
  ///
  /// - Parameter value: the value to append
  /// - Throws: ``DatabaseError/appenderFailedToAppendItem(reason:)``
  ///   if a value of this type was not expected in the appender's current state
  func append(_ value: UInt16?) throws {
    guard let value = try unwrapValueOrAppendNull(value) else { return }
    try withThrowingCommand { duckdb_append_uint16(ptr.pointee, value) }
  }
  
  /// Appends a value for the current row of the given type
  ///
  /// Appends are made in row-wise format. For every column, an `append(_:)`
  /// call should be made, after which the row should be finished by calling
  /// ``endRow()``.
  ///
  /// - Parameter value: the value to append
  /// - Throws: ``DatabaseError/appenderFailedToAppendItem(reason:)``
  ///   if a value of this type was not expected in the appender's current state
  func append(_ value: UInt32?) throws {
    guard let value = try unwrapValueOrAppendNull(value) else { return }
    try withThrowingCommand { duckdb_append_uint32(ptr.pointee, value) }
  }
  
  /// Appends a value for the current row of the given type
  ///
  /// Appends are made in row-wise format. For every column, an `append(_:)`
  /// call should be made, after which the row should be finished by calling
  /// ``endRow()``.
  ///
  /// - Parameter value: the value to append
  /// - Throws: ``DatabaseError/appenderFailedToAppendItem(reason:)``
  ///   if a value of this type was not expected in the appender's current state
  func append(_ value: UInt64?) throws {
    guard let value = try unwrapValueOrAppendNull(value) else { return }
    try withThrowingCommand { duckdb_append_uint64(ptr.pointee, value) }
  }
  
  /// Appends a value for the current row of the given type
  ///
  /// Appends are made in row-wise format. For every column, an `append(_:)`
  /// call should be made, after which the row should be finished by calling
  /// ``endRow()``.
  ///
  /// - Parameter value: the value to append
  /// - Throws: ``DatabaseError/appenderFailedToAppendItem(reason:)``
  ///   if a value of this type was not expected in the appender's current state
  func append(_ value: Float?) throws {
    guard let value = try unwrapValueOrAppendNull(value) else { return }
    try withThrowingCommand { duckdb_append_float(ptr.pointee, value) }
  }
  
  /// Appends a value for the current row of the given type
  ///
  /// Appends are made in row-wise format. For every column, an `append(_:)`
  /// call should be made, after which the row should be finished by calling
  /// ``endRow()``.
  ///
  /// - Parameter value: the value to append
  /// - Throws: ``DatabaseError/appenderFailedToAppendItem(reason:)``
  ///   if a value of this type was not expected in the appender's current state
  func append(_ value: Double?) throws {
    guard let value = try unwrapValueOrAppendNull(value) else { return }
    try withThrowingCommand { duckdb_append_double(ptr.pointee, value) }
  }
  
  /// Appends a value for the current row of the given type
  ///
  /// Appends are made in row-wise format. For every column, an `append(_:)`
  /// call should be made, after which the row should be finished by calling
  /// ``endRow()``.
  ///
  /// - Parameter value: the value to append
  /// - Throws: ``DatabaseError/appenderFailedToAppendItem(reason:)``
  ///   if a value of this type was not expected in the appender's current state
  func append(_ value: Date?) throws {
    guard let value = try unwrapValueOrAppendNull(value) else { return }
    try withThrowingCommand { duckdb_append_date(ptr.pointee, .init(date: value)) }
  }

  /// Appends a value for the current row of the given type
  ///
  /// Appends are made in row-wise format. For every column, an `append(_:)`
  /// call should be made, after which the row should be finished by calling
  /// ``endRow()``.
  ///
  /// - Parameter value: the value to append
  /// - Throws: ``DatabaseError/appenderFailedToAppendItem(reason:)``
  ///   if a value of this type was not expected in the appender's current state
  func append(_ value: Time?) throws {
    guard let value = try unwrapValueOrAppendNull(value) else { return }
    try withThrowingCommand { duckdb_append_time(ptr.pointee, .init(time: value)) }
  }
  
  /// Appends a value for the current row of the given type
  ///
  /// Appends are made in row-wise format. For every column, an `append(_:)`
  /// call should be made, after which the row should be finished by calling
  /// ``endRow()``.
  ///
  /// - Parameter value: the value to append
  /// - Throws: ``DatabaseError/appenderFailedToAppendItem(reason:)``
  ///   if a value of this type was not expected in the appender's current state
  func append(_ value: Timestamp?) throws {
    guard let value = try unwrapValueOrAppendNull(value) else { return }
    try withThrowingCommand {
      duckdb_append_timestamp(ptr.pointee, .init(timestamp: value))
    }
  }
  
  /// Appends a value for the current row of the given type
  ///
  /// Appends are made in row-wise format. For every column, an `append(_:)`
  /// call should be made, after which the row should be finished by calling
  /// ``endRow()``.
  ///
  /// - Parameter value: the value to append
  /// - Throws: ``DatabaseError/appenderFailedToAppendItem(reason:)``
  ///   if a value of this type was not expected in the appender's current state
  func append(_ value: Interval?) throws {
    guard let value = try unwrapValueOrAppendNull(value) else { return }
    try withThrowingCommand {
      duckdb_append_interval(ptr.pointee, .init(interval: value))
    }
  }
  
  /// Appends a value for the current row of the given type
  ///
  /// Appends are made in row-wise format. For every column, an `append(_:)`
  /// call should be made, after which the row should be finished by calling
  /// ``endRow()``.
  ///
  /// - Parameter value: the value to append
  /// - Throws: ``DatabaseError/appenderFailedToAppendItem(reason:)``
  ///   if a value of this type was not expected in the appender's current state
  func append(_ value: String?) throws {
    guard let value = try unwrapValueOrAppendNull(value) else { return }
    let data = value.data(using: .utf8)!
    try withThrowingCommand {
      data.withUnsafeBytes { dataPtr in
        duckdb_append_varchar_length(ptr.pointee, dataPtr.baseAddress, .init(dataPtr.count))
      }
    }
  }
  
  /// Appends a value for the current row of the given type
  ///
  /// Appends are made in row-wise format. For every column, an `append(_:)`
  /// call should be made, after which the row should be finished by calling
  /// ``endRow()``.
  ///
  /// - Parameter value: the value to append
  /// - Throws: ``DatabaseError/appenderFailedToAppendItem(reason:)``
  ///   if a value of this type was not expected in the appender's current state
  func append(_ value: Data?) throws {
    guard let value = try unwrapValueOrAppendNull(value) else { return }
    try withThrowingCommand {
      value.withUnsafeBytes { dataPtr in
        duckdb_append_blob( ptr.pointee, dataPtr.baseAddress, .init(dataPtr.count)) }
    }
  }
}

private extension Appender {
  
  func appendNullValue() throws {
    try withThrowingCommand { duckdb_append_null(ptr.pointee) }
  }
  
  func unwrapValueOrAppendNull<T>(_ value: T?) throws -> T? {
    guard let value else {
      try appendNullValue()
      return nil
    }
    return value
  }
  
  func withThrowingCommand(_ body: () throws -> duckdb_state) throws {
    let state = try body()
    guard state == .success else {
      throw DatabaseError.appenderFailedToAppendItem(reason: appenderError())
    }
  }
  
  func appenderError() -> String? {
    duckdb_appender_error(ptr.pointee).map(String.init(cString:))
  }
}
