package org.jetbrains.exposed.sql.statements

import org.jetbrains.exposed.exceptions.ExposedSQLException
import org.jetbrains.exposed.sql.IColumnType
import org.jetbrains.exposed.sql.Table
import org.jetbrains.exposed.sql.Transaction
import org.jetbrains.exposed.sql.statements.api.PreparedStatementApi
import java.sql.ResultSet
import java.sql.SQLException
import java.util.Stack

internal object DefaultValueMarker {
    override fun toString(): String = "DEFAULT"
}

/**
 * Base class representing an SQL statement that can be executed.
 *
 * @param type The specific [StatementType], usually represented by the leading word in the command syntax.
 * @param targets Tables on which to perform the SQL statement.
 */
abstract class Statement<out T>(val type: StatementType, val targets: List<Table>) {

    /**
     * Determines the exact way that an SQL statement is executed in a [transaction] and applies any necessary
     * logic before returning the result generated by the executed statement.
     */
    abstract fun PreparedStatementApi.executeInternal(transaction: Transaction): T?

    /**
     * Returns the string representation of an SQL statement.
     *
     * If necessary, [transaction] can be used to ensure that database-specific syntax is used to generate the string.
     * To return a non-parameterized string, set [prepared] to `false`.
     */
    abstract fun prepareSQL(transaction: Transaction, prepared: Boolean = true): String

    /** Returns all mappings of columns and expression types to their values needed to prepare an SQL statement. */
    abstract fun arguments(): Iterable<Iterable<Pair<IColumnType<*>, Any?>>>

    /**
     * Uses a [transaction] connection and an [sql] string representation to return a precompiled SQL statement,
     * stored as an implementation of [PreparedStatementApi].
     */
    open fun prepared(transaction: Transaction, sql: String): PreparedStatementApi =
        transaction.connection.prepareStatement(sql, false)

    /** Whether the SQL statement is meant to be performed as part of a batch execution. */
    open val isAlwaysBatch: Boolean = false

    /**
     * Executes the SQL statement directly in the provided [transaction] and returns the generated result,
     * or `null` if either no result was retrieved or if the transaction blocked statement execution.
     */
    fun execute(transaction: Transaction): T? = if (transaction.blockStatementExecution) {
        transaction.explainStatement = this
        null
    } else {
        transaction.exec(this)
    }

    internal fun executeIn(transaction: Transaction): Pair<T?, List<StatementContext>> {
        val arguments = arguments()
        val contexts = if (arguments.any()) {
            arguments.map { args ->
                val context = StatementContext(this, args)
                Transaction.globalInterceptors.forEach { it.beforeExecution(transaction, context) }
                transaction.interceptors.forEach { it.beforeExecution(transaction, context) }
                context
            }
        } else {
            val context = StatementContext(this, emptyList())
            Transaction.globalInterceptors.forEach { it.beforeExecution(transaction, context) }
            transaction.interceptors.forEach { it.beforeExecution(transaction, context) }
            listOf(context)
        }

        val statement = try {
            prepared(transaction, prepareSQL(transaction)).apply {
                timeout = transaction.queryTimeout
            }
        } catch (e: SQLException) {
            throw ExposedSQLException(e, contexts, transaction)
        }
        contexts.forEachIndexed { _, context ->
            statement.fillParameters(context.args)
            // REVIEW
            if (contexts.size > 1 || isAlwaysBatch) statement.addBatch()
        }
        if (!transaction.db.supportsMultipleResultSets) {
            transaction.closeExecutedStatements()
        }

        transaction.currentStatement = statement
        transaction.interceptors.forEach { it.afterStatementPrepared(transaction, statement) }
        val result = try {
            statement.executeInternal(transaction)
        } catch (cause: SQLException) {
            throw ExposedSQLException(cause, contexts, transaction)
        }
        transaction.currentStatement = null
        transaction.executedStatements.add(statement)

        Transaction.globalInterceptors.forEach { it.afterExecution(transaction, contexts, statement) }
        transaction.interceptors.forEach { it.afterExecution(transaction, contexts, statement) }
        return result to contexts
    }
}

/** Holds information related to a particular [statement] and the [args] needed to prepare it for execution. */
class StatementContext(val statement: Statement<*>, val args: Iterable<Pair<IColumnType<*>, Any?>>) {
    /** Returns the string representation of the SQL statement associated with this [StatementContext]. */
    fun sql(transaction: Transaction) = statement.prepareSQL(transaction)
}

/**
 * Returns the string representation of [this] context's [Statement] with its argument values included
 * directly instead of parameter placeholders.
 */
fun StatementContext.expandArgs(transaction: Transaction): String {
    val sql = sql(transaction)
    val iterator = args.iterator()

    if (!iterator.hasNext()) return sql

    return buildString {
        val quoteStack = Stack<Char>()
        var lastPos = 0

        var i = -1
        while (++i < sql.length) {
            val char = sql[i]
            when {
                char == '?' && quoteStack.isEmpty() -> {
                    if (sql.getOrNull(i + 1) == '?') {
                        i++
                        continue
                    }
                    append(sql.substring(lastPos, i))
                    lastPos = i + 1
                    val (col, value) = iterator.next()
                    append((col as IColumnType<Any>).valueToString(value))
                }
                char == '\'' || char == '\"' -> {
                    when {
                        quoteStack.isEmpty() -> quoteStack.push(char)
                        quoteStack.peek() == char -> quoteStack.pop()
                        else -> quoteStack.push(char)
                    }
                }
            }
        }

        if (lastPos < sql.length) {
            append(sql.substring(lastPos))
        }
    }
}

/** Represents the groups that are used to classify the purpose of an SQL statement. */
enum class StatementGroup {
    /** Data definition language group. */
    DDL,

    /** Data manipulation language group. */
    DML
}

/**
 * Possible SQL statement types, most often represented by the leading word in the command syntax.
 *
 * @property group The [StatementGroup] associated with the SQL statement.
 */
enum class StatementType(val group: StatementGroup) {
    /** A SELECT statement to query data. */
    SELECT(StatementGroup.DML),

    /** An INSERT statement to insert new records. */
    INSERT(StatementGroup.DML),

    /** An UPDATE statement to modify existing records. */
    UPDATE(StatementGroup.DML),

    /** A DELETE statement to delete existing records. */
    DELETE(StatementGroup.DML),

    /** A GRANT statement to provide privileges on database objects. */
    GRANT(StatementGroup.DDL),

    /** A CREATE statement to create database objects. */
    CREATE(StatementGroup.DDL),

    /** An ALTER statement to modify database objects. */
    ALTER(StatementGroup.DDL),

    /** A TRUNCATE statement to delete data in a database object. */
    TRUNCATE(StatementGroup.DDL),

    /** A DROP statement to delete database objects. */
    DROP(StatementGroup.DDL),

    /** An EXEC statement to execute a stored procedure or command. */
    EXEC(StatementGroup.DML),

    /** A PRAGMA statement to configure or query the internal database state. */
    PRAGMA(StatementGroup.DML),

    /** A SHOW statement to provide information about database objects. */
    SHOW(StatementGroup.DML),

    /** Represents multiple statements of mixed types concatenated in a single string. */
    MULTI(StatementGroup.DML),

    /** Represents statements not covered by existing constants. */
    OTHER(StatementGroup.DDL),

    /** A MERGE statement to insert, update, or delete values by comparing data between source and destination tables. */
    MERGE(StatementGroup.DML),
}

/** Stores the result generated by a database after statement execution and indicates the form of the result. */
sealed class StatementResult {
    /** Stores the affected row [count] (or update count) retrieved on statement execution. */
    data class Count(val count: Int) : StatementResult()

    /** Stores the [resultSet] retrieved on statement execution. */
    data class Object(val resultSet: ResultSet) : StatementResult()
}
