package org.tasks.data

import androidx.paging.DataSource
import androidx.room.*
import androidx.sqlite.db.SimpleSQLiteQuery
import com.todoroo.andlib.sql.Criterion
import com.todoroo.andlib.sql.Field
import com.todoroo.andlib.sql.Functions
import com.todoroo.andlib.utility.DateUtilities.now
import com.todoroo.astrid.api.Filter
import com.todoroo.astrid.api.PermaSql
import com.todoroo.astrid.dao.Database
import com.todoroo.astrid.data.Task
import com.todoroo.astrid.data.Task.Companion.NO_ID
import com.todoroo.astrid.helper.UUIDHelper
import org.tasks.BuildConfig
import org.tasks.data.Alarm.Companion.TYPE_SNOOZE
import org.tasks.db.SuspendDbUtils.chunkedMap
import org.tasks.db.SuspendDbUtils.eachChunk
import org.tasks.preferences.Preferences
import org.tasks.time.DateTimeUtils.currentTimeMillis
import timber.log.Timber

@Dao
abstract class TaskDao(private val database: Database) {

    @Query("SELECT * FROM tasks WHERE completed = 0 AND deleted = 0 AND (hideUntil > :now OR dueDate > :now)")
    internal abstract suspend fun needsRefresh(now: Long = now()): List<Task>

    @Query("SELECT * FROM tasks WHERE _id = :id LIMIT 1")
    abstract suspend fun fetch(id: Long): Task?

    suspend fun fetch(ids: List<Long>): List<Task> = ids.chunkedMap(this::fetchInternal)

    @Query("SELECT * FROM tasks WHERE _id IN (:ids)")
    internal abstract suspend fun fetchInternal(ids: List<Long>): List<Task>

    @Query("SELECT COUNT(1) FROM tasks WHERE timerStart > 0 AND deleted = 0")
    abstract suspend fun activeTimers(): Int

    @Query("SELECT COUNT(1) FROM tasks INNER JOIN alarms ON tasks._id = alarms.task WHERE type = $TYPE_SNOOZE")
    abstract suspend fun snoozedReminders(): Int

    @Query("SELECT COUNT(1) FROM tasks INNER JOIN notification ON tasks._id = notification.task")
    abstract suspend fun hasNotifications(): Int

    @Query("SELECT tasks.* FROM tasks INNER JOIN notification ON tasks._id = notification.task")
    abstract suspend fun activeNotifications(): List<Task>

    @Query("SELECT * FROM tasks WHERE remoteId = :remoteId")
    abstract suspend fun fetch(remoteId: String): Task?

    @Query("SELECT * FROM tasks WHERE completed = 0 AND deleted = 0")
    abstract suspend fun getActiveTasks(): List<Task>

    @Query("SELECT * FROM tasks WHERE remoteId IN (:remoteIds) "
            + "AND recurrence IS NOT NULL AND LENGTH(recurrence) > 0")
    abstract suspend fun getRecurringTasks(remoteIds: List<String>): List<Task>

    @Query("UPDATE tasks SET completed = :completionDate, modified = :updateTime WHERE remoteId IN (:remoteIds)")
    abstract suspend fun setCompletionDate(remoteIds: List<String>, completionDate: Long, updateTime: Long = now())

    @Query("SELECT tasks.* FROM tasks "
            + "LEFT JOIN caldav_tasks ON tasks._id = caldav_tasks.cd_task "
            + "LEFT JOIN caldav_lists ON caldav_tasks.cd_calendar = caldav_lists.cdl_uuid "
            + "WHERE cdl_account = :account "
            + "AND (tasks.modified > caldav_tasks.cd_last_sync OR caldav_tasks.cd_remote_id = '' OR caldav_tasks.cd_remote_id IS NULL OR caldav_tasks.cd_deleted > 0) "
            + "ORDER BY CASE WHEN parent = 0 THEN 0 ELSE 1 END, `order` ASC")
    abstract suspend fun getGoogleTasksToPush(account: String): List<Task>

    @Query("""
        SELECT tasks.*
        FROM tasks
                 INNER JOIN caldav_tasks ON tasks._id = caldav_tasks.cd_task
        WHERE caldav_tasks.cd_calendar = :calendar
          AND cd_deleted = 0
          AND (tasks.modified > caldav_tasks.cd_last_sync OR caldav_tasks.cd_last_sync = 0)
        ORDER BY created""")
    abstract suspend fun getCaldavTasksToPush(calendar: String): List<Task>

    // --- SQL clause generators
    @Query("SELECT * FROM tasks")
    abstract suspend fun getAll(): List<Task>

    @Query("SELECT calendarUri FROM tasks " + "WHERE calendarUri IS NOT NULL AND calendarUri != ''")
    abstract suspend fun getAllCalendarEvents(): List<String>

    @Query("UPDATE tasks SET calendarUri = '' " + "WHERE calendarUri IS NOT NULL AND calendarUri != ''")
    abstract suspend fun clearAllCalendarEvents(): Int

    @Query("SELECT calendarUri FROM tasks "
            + "WHERE completed > 0 AND calendarUri IS NOT NULL AND calendarUri != ''")
    abstract suspend fun getCompletedCalendarEvents(): List<String>

    @Query("UPDATE tasks SET calendarUri = '' "
            + "WHERE completed > 0 AND calendarUri IS NOT NULL AND calendarUri != ''")
    abstract suspend fun clearCompletedCalendarEvents(): Int

    open suspend fun fetchTasks(callback: suspend (SubtaskInfo) -> List<String>): List<TaskContainer> {
        return fetchTasks(getSubtaskInfo(), callback)
    }

    open suspend fun fetchTasks(subtasks: SubtaskInfo, callback: suspend (SubtaskInfo) -> List<String>): List<TaskContainer> =
            database.withTransaction {
                val start = if (BuildConfig.DEBUG) now() else 0
                val queries = callback(subtasks)
                val last = queries.size - 1
                for (i in 0 until last) {
                    query(SimpleSQLiteQuery(queries[i]))
                }
                val result = fetchTasks(SimpleSQLiteQuery(queries[last]))
                Timber.v("%sms: %s", now() - start, queries.joinToString(";\n"))
                result
            }

    suspend fun fetchTasks(preferences: Preferences, filter: Filter): List<TaskContainer> =
            fetchTasks {
                TaskListQuery.getQuery(preferences, filter, it)
            }

    @RawQuery
    internal abstract suspend fun query(query: SimpleSQLiteQuery): Int

    @RawQuery
    internal abstract suspend fun fetchTasks(query: SimpleSQLiteQuery): List<TaskContainer>

    @RawQuery
    abstract suspend fun count(query: SimpleSQLiteQuery): Int

    @Query("SELECT EXISTS(SELECT 1 FROM tasks WHERE parent > 0 AND deleted = 0) AS hasSubtasks")
    abstract suspend fun getSubtaskInfo(): SubtaskInfo

    @RawQuery(observedEntities = [Place::class])
    abstract fun getTaskFactory(query: SimpleSQLiteQuery): DataSource.Factory<Int, TaskContainer>

    suspend fun touch(ids: List<Long>, now: Long = currentTimeMillis()) =
        ids.eachChunk { internalTouch(it, now) }

    @Query("UPDATE tasks SET modified = :now WHERE _id in (:ids)")
    internal abstract suspend fun internalTouch(ids: List<Long>, now: Long = currentTimeMillis())

    @Query("UPDATE tasks SET `order` = :order WHERE _id = :id")
    internal abstract suspend fun setOrder(id: Long, order: Long?)

    suspend fun setParent(parent: Long, tasks: List<Long>) =
            tasks.eachChunk { setParentInternal(parent, it) }

    @Query("UPDATE tasks SET parent = :parent WHERE _id IN (:children) AND _id != :parent")
    internal abstract suspend fun setParentInternal(parent: Long, children: List<Long>)

    @Query("UPDATE tasks SET lastNotified = :timestamp WHERE _id = :id AND lastNotified != :timestamp")
    abstract suspend fun setLastNotified(id: Long, timestamp: Long)

    suspend fun getChildren(id: Long): List<Long> = getChildren(listOf(id))

    @Query("""
WITH RECURSIVE recursive_tasks (task) AS (
    SELECT _id
    FROM tasks
    WHERE parent IN (:ids)
    UNION ALL
    SELECT _id
    FROM tasks
             INNER JOIN recursive_tasks ON recursive_tasks.task = tasks.parent
    WHERE tasks.deleted = 0)
SELECT task
FROM recursive_tasks
    """)
    abstract suspend fun getChildren(ids: List<Long>): List<Long>

    @Query("""
WITH RECURSIVE recursive_tasks (task, parent) AS (
    SELECT _id, parent FROM tasks WHERE _id = :parent
    UNION ALL
    SELECT _id, tasks.parent FROM tasks
        INNER JOIN recursive_tasks ON recursive_tasks.parent = tasks._id
    WHERE tasks.deleted = 0
)
SELECT task
FROM recursive_tasks
""")
    abstract suspend fun getParents(parent: Long): List<Long>

    internal suspend fun setCollapsed(preferences: Preferences, filter: Filter, collapsed: Boolean) {
        fetchTasks(preferences, filter)
                .filter(TaskContainer::hasChildren)
                .map(TaskContainer::getId)
                .eachChunk { setCollapsed(it, collapsed) }
    }

    @Query("UPDATE tasks SET collapsed = :collapsed, modified = :now WHERE _id IN (:ids)")
    internal abstract suspend fun setCollapsed(ids: List<Long>, collapsed: Boolean, now: Long = now())

    @Insert
    abstract suspend fun insert(task: Task): Long

    suspend fun update(task: Task, original: Task? = null): Boolean {
        if (!task.insignificantChange(original)) {
            task.modificationDate = now()
        }
        return updateInternal(task) == 1
    }

    @Update
    internal abstract suspend fun updateInternal(task: Task): Int

    suspend fun createNew(task: Task): Long {
        task.id = NO_ID
        if (task.creationDate == 0L) {
            task.creationDate = now()
        }
        if (Task.isUuidEmpty(task.remoteId)) {
            task.remoteId = UUIDHelper.newUUID()
        }
        if (BuildConfig.DEBUG) {
            require(task.remoteId?.isNotBlank() == true && task.remoteId != "0")
        }
        val insert = insert(task)
        task.id = insert
        return task.id
    }

    suspend fun count(filter: Filter): Int {
        val query = getQuery(filter.sqlQuery, Field.COUNT)
        val start = if (BuildConfig.DEBUG) now() else 0
        val count = count(query)
        Timber.v("%sms: %s", now() - start, query.sql)
        return count
    }

    suspend fun fetchFiltered(filter: Filter): List<Task> = fetchFiltered(filter.getSqlQuery())

    suspend fun fetchFiltered(queryTemplate: String): List<Task> {
        val query = getQuery(queryTemplate, Task.FIELDS)
        val start = if (BuildConfig.DEBUG) now() else 0
        val tasks = fetchTasks(query)
        Timber.v("%sms: %s", now() - start, query.sql)
        return tasks.map(TaskContainer::getTask)
    }

    @Query("""
SELECT _id
FROM tasks
         LEFT JOIN caldav_tasks ON _id = cd_task AND cd_deleted = 0
WHERE cd_id IS NULL
  AND parent = 0
    """)
    abstract suspend fun getLocalTasks(): List<Long>

    /** Generates SQL clauses  */
    object TaskCriteria {
        /** @return tasks that have not yet been completed or deleted
         */
        @JvmStatic fun activeAndVisible(): Criterion = Criterion.and(
                Task.COMPLETION_DATE.lte(0),
                Task.DELETION_DATE.lte(0),
                Task.HIDE_UNTIL.lte(Functions.now()))
    }

    companion object {
        fun getQuery(queryTemplate: String, vararg fields: Field): SimpleSQLiteQuery =
                SimpleSQLiteQuery(
                        com.todoroo.andlib.sql.Query.select(*fields)
                                .withQueryTemplate(PermaSql.replacePlaceholdersForQuery(queryTemplate))
                                .from(Task.TABLE)
                                .toString())
    }
}