@file:Suppress("ktlint:filename")

package cash.z.ecc.android.sdk.internal.db

import android.database.sqlite.SQLiteDatabase
import androidx.sqlite.db.SupportSQLiteDatabase
import androidx.sqlite.db.SupportSQLiteQueryBuilder
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn
import java.util.Locale
import kotlin.coroutines.CoroutineContext

/**
 * Performs a query on a background thread.
 *
 * Note that this method is best for small queries, as Cursor has an in-memory window of cached data.  If iterating
 * through a large number of items that exceeds the window, the Cursor may perform additional IO.
 */
@Suppress("LongParameterList")
internal fun <T> SQLiteDatabase.queryAndMap(
    table: String,
    columns: Array<String>? = null,
    selection: String? = null,
    selectionArgs: Array<Any>? = null,
    groupBy: String? = null,
    having: String? = null,
    orderBy: String? = null,
    limit: String? = null,
    offset: String? = null,
    coroutineContext: CoroutineContext = Dispatchers.IO,
    cursorParser: CursorParser<T>
) = flow<T> {
    // TODO [#703]: Support blobs for argument binding
    // TODO [#703]: https://github.com/zcash/zcash-android-wallet-sdk/issues/703
    val mappedSelectionArgs = selectionArgs?.onEach {
        require(it !is ByteArray) {
            "ByteArray is not supported"
        }
    }?.map { it.toString() }?.toTypedArray()

    // Counterintuitive but correct. When using the comma syntax, offset comes first.
    // When using the keyword syntax, "LIMIT 1 OFFSET 2" then the offset comes second.
    val limitAndOffset = if (null == offset) {
        limit
    } else {
        String.format(Locale.ROOT, "%s,%s", offset, limit) // NON-NLS
    }

    query(
        table,
        columns,
        selection,
        mappedSelectionArgs,
        groupBy,
        having,
        orderBy,
        limitAndOffset
    ).use {
        it.moveToPosition(-1)
        while (it.moveToNext()) {
            emit(cursorParser.newObject(it))
        }
    }
}.flowOn(coroutineContext)

/**
 * Performs a query on a background thread.
 *
 * Note that this method is best for small queries, as Cursor has an in-memory window of cached data.  If iterating
 * through a large number of items that exceeds the window, the Cursor may perform additional IO.
 */
@Suppress("LongParameterList")
internal fun <T> SupportSQLiteDatabase.queryAndMap(
    table: String,
    columns: Array<String>? = null,
    selection: String? = null,
    selectionArgs: Array<Any>? = null,
    groupBy: String? = null,
    having: String? = null,
    orderBy: String? = null,
    limit: String? = null,
    offset: String? = null,
    coroutineContext: CoroutineContext = Dispatchers.IO,
    cursorParser: CursorParser<T>
) = flow<T> {
    val qb = SupportSQLiteQueryBuilder.builder(table).apply {
        columns(columns)
        selection(selection, selectionArgs)
        having(having)
        groupBy(groupBy)
        orderBy(orderBy)

        // Counterintuitive but correct. When using the comma syntax, offset comes first.
        // When using the keyword syntax, "LIMIT 1 OFFSET 2" then the offset comes second.
        if (null == offset) {
            limit(limit)
        } else {
            limit(String.format(Locale.ROOT, "%s,%s", offset, limit)) // NON-NLS
        }
    }

    query(qb.create()).use {
        it.moveToPosition(-1)
        while (it.moveToNext()) {
            emit(cursorParser.newObject(it))
        }
    }
}.flowOn(coroutineContext)
