package eu.kanade.tachiyomi.ui.library

import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Category
import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.LibraryManga
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.models.MangaCategory
import eu.kanade.tachiyomi.data.download.DownloadManager
import eu.kanade.tachiyomi.data.preference.DelayedLibrarySuggestionsJob
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.getOrDefault
import eu.kanade.tachiyomi.data.preference.minusAssign
import eu.kanade.tachiyomi.data.preference.plusAssign
import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.source.LocalSource
import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.ui.base.presenter.BaseCoroutinePresenter
import eu.kanade.tachiyomi.ui.library.LibraryGroup.BY_DEFAULT
import eu.kanade.tachiyomi.ui.library.LibraryGroup.BY_SOURCE
import eu.kanade.tachiyomi.ui.library.LibraryGroup.BY_TAG
import eu.kanade.tachiyomi.ui.library.LibraryGroup.BY_TRACK_STATUS
import eu.kanade.tachiyomi.ui.library.LibraryGroup.UNGROUPED
import eu.kanade.tachiyomi.ui.library.filter.FilterBottomSheet
import eu.kanade.tachiyomi.ui.library.filter.FilterBottomSheet.Companion.STATE_EXCLUDE
import eu.kanade.tachiyomi.ui.library.filter.FilterBottomSheet.Companion.STATE_IGNORE
import eu.kanade.tachiyomi.ui.library.filter.FilterBottomSheet.Companion.STATE_INCLUDE
import eu.kanade.tachiyomi.ui.recents.RecentsPresenter
import eu.kanade.tachiyomi.util.chapter.ChapterFilter
import eu.kanade.tachiyomi.util.chapter.ChapterSort
import eu.kanade.tachiyomi.util.lang.capitalizeWords
import eu.kanade.tachiyomi.util.lang.chopByWords
import eu.kanade.tachiyomi.util.lang.removeArticles
import eu.kanade.tachiyomi.util.system.executeOnIO
import eu.kanade.tachiyomi.util.system.launchIO
import eu.kanade.tachiyomi.util.system.withUIContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.util.ArrayList
import java.util.Calendar
import java.util.Comparator
import java.util.Date
import java.util.Locale
import java.util.concurrent.TimeUnit
import kotlin.random.Random

/**
 * Presenter of [LibraryController].
 */
class LibraryPresenter(
    private val view: LibraryController,
    val db: DatabaseHelper = Injekt.get(),
    private val preferences: PreferencesHelper = Injekt.get(),
    private val coverCache: CoverCache = Injekt.get(),
    private val sourceManager: SourceManager = Injekt.get(),
    private val downloadManager: DownloadManager = Injekt.get(),
    private val chapterFilter: ChapterFilter = Injekt.get()
) : BaseCoroutinePresenter() {

    private val context = preferences.context

    private val loggedServices by lazy { Injekt.get<TrackManager>().services.filter { it.isLogged } }

    var groupType = preferences.groupLibraryBy().get()

    val isLoggedIntoTracking
        get() = loggedServices.isNotEmpty()

    /** Current categories of the library. */
    var categories: List<Category> = emptyList()
        private set

    var removeArticles: Boolean = preferences.removeArticles().getOrDefault()

    /** All categories of the library, in case they are hidden because of hide categories is on */
    var allCategories: List<Category> = emptyList()
        private set

    /** List of all manga to update the */
    var libraryItems: List<LibraryItem> = emptyList()
    private var sectionedLibraryItems: MutableMap<Int, List<LibraryItem>> = mutableMapOf()
    var currentCategory = -1
        private set
    private var allLibraryItems: List<LibraryItem> = emptyList()
    val showAllCategories
        get() = preferences.showAllCategories().get()

    private val libraryIsGrouped
        get() = groupType != UNGROUPED

    /** Save the current list to speed up loading later */
    override fun onDestroy() {
        super.onDestroy()
        lastLibraryItems = libraryItems
        lastCategories = categories
    }

    override fun onCreate() {
        super.onCreate()
        lastLibraryItems?.let { libraryItems = it }
        lastCategories?.let { categories = it }
        lastCategories = null
        lastLibraryItems = null
        getLibrary()
        if (preferences.showLibrarySearchSuggestions().isNotSet()) {
            DelayedLibrarySuggestionsJob.setupTask(context, true)
        } else if (preferences.showLibrarySearchSuggestions().get() &&
            Date().time >= preferences.lastLibrarySuggestion().get() + TimeUnit.HOURS.toMillis(2)
        ) {
            // Doing this instead of a job in case the app isn't used often
            presenterScope.launchIO {
                setSearchSuggestion(preferences, db, sourceManager)
                withUIContext { view.setTitle() }
            }
        }
    }

    /** Get favorited manga for library and sort and filter it */
    fun getLibrary() {
        if (categories.isEmpty()) {
            val dbCategories = db.getCategories().executeAsBlocking()
            if ((dbCategories + Category.createDefault(context)).distinctBy { it.order }.size != dbCategories.size + 1) {
                reorderCategories(dbCategories)
            }
            categories = lastCategories ?: db.getCategories().executeAsBlocking().toMutableList()
        }
        presenterScope.launch {
            val library = withContext(Dispatchers.IO) { getLibraryFromDB() }
            library.apply {
                setDownloadCount(library)
                setUnreadBadge(library)
            }
            allLibraryItems = library
            var mangaMap = library
            mangaMap = applyFilters(mangaMap)
            mangaMap = applySort(mangaMap)
            val freshStart = libraryItems.isEmpty()
            sectionLibrary(mangaMap, freshStart)
        }
    }

    private fun reorderCategories(categories: List<Category>) {
        val sortedCategories = categories.sortedBy { it.order }
        sortedCategories.forEachIndexed { i, category -> category.order = i }
        db.insertCategories(sortedCategories).executeAsBlocking()
    }

    fun getCurrentCategory() = categories.find { it.id == currentCategory }

    fun switchSection(order: Int) {
        preferences.lastUsedCategory().set(order)
        val category = categories.find { it.order == order }?.id ?: return
        currentCategory = category
        view.onNextLibraryUpdate(
            sectionedLibraryItems[currentCategory] ?: blankItem()
        )
    }

    private fun blankItem(id: Int = currentCategory): List<LibraryItem> {
        return listOf(
            LibraryItem(
                LibraryManga.createBlank(id),
                LibraryHeaderItem({ getCategory(id) }, id)
            )
        )
    }

    fun restoreLibrary() {
        val items = libraryItems
        val show = showAllCategories || !libraryIsGrouped || categories.size == 1
        sectionedLibraryItems = items.groupBy { it.header.category.id!! }.toMutableMap()
        if (!show && currentCategory == -1) currentCategory = categories.find {
            it.order == preferences.lastUsedCategory().getOrDefault()
        }?.id ?: 0
        view.onNextLibraryUpdate(
            if (!show) sectionedLibraryItems[currentCategory]
                ?: sectionedLibraryItems[categories.first().id] ?: blankItem()
            else libraryItems,
            true
        )
    }

    fun getMangaInCategories(catId: Int?): List<LibraryManga>? {
        catId ?: return null
        return allLibraryItems.filter { it.header.category.id == catId }.map { it.manga }
    }

    private suspend fun sectionLibrary(items: List<LibraryItem>, freshStart: Boolean = false) {
        libraryItems = items
        val showAll = showAllCategories || !libraryIsGrouped ||
            categories.size <= 1
        sectionedLibraryItems = items.groupBy { it.header.category.id ?: 0 }.toMutableMap()
        if (!showAll && currentCategory == -1) currentCategory = categories.find {
            it.order == preferences.lastUsedCategory().getOrDefault()
        }?.id ?: 0
        withUIContext {
            view.onNextLibraryUpdate(
                if (!showAll) sectionedLibraryItems[currentCategory]
                    ?: sectionedLibraryItems[categories.first().id] ?: blankItem()
                else libraryItems,
                freshStart
            )
        }
    }

    /**
     * Applies library filters to the given list of manga.
     *
     * @param items the items to filter.
     */
    private fun applyFilters(items: List<LibraryItem>): List<LibraryItem> {
        val filterDownloaded = preferences.filterDownloaded().getOrDefault()

        val filterUnread = preferences.filterUnread().getOrDefault()

        val filterCompleted = preferences.filterCompleted().getOrDefault()

        val filterTracked = preferences.filterTracked().getOrDefault()

        val filterMangaType = preferences.filterMangaType().getOrDefault()

        val filterTrackers = FilterBottomSheet.FILTER_TRACKER

        val filtersOff =
            filterDownloaded == 0 && filterUnread == 0 && filterCompleted == 0 && filterTracked == 0 && filterMangaType == 0
        return items.filter f@{ item ->
            if (item.manga.status == -1) {
                val subItems = sectionedLibraryItems[item.manga.category]
                if (subItems.isNullOrEmpty()) return@f filtersOff
                else {
                    return@f subItems.any {
                        matchesFilters(
                            it,
                            filterDownloaded,
                            filterUnread,
                            filterCompleted,
                            filterTracked,
                            filterMangaType,
                            filterTrackers
                        )
                    }
                }
            } else if (item.manga.isBlank()) {
                return@f if (showAllCategories) {
                    filtersOff
                } else {
                    true
                }
            }
            matchesFilters(
                item,
                filterDownloaded,
                filterUnread,
                filterCompleted,
                filterTracked,
                filterMangaType,
                filterTrackers
            )
        }
    }

    private fun matchesFilters(
        item: LibraryItem,
        filterDownloaded: Int,
        filterUnread: Int,
        filterCompleted: Int,
        filterTracked: Int,
        filterMangaType: Int,
        filterTrackers: String
    ): Boolean {
        if (filterUnread == STATE_INCLUDE && item.manga.unread == 0) return false
        if (filterUnread == STATE_EXCLUDE && item.manga.unread > 0) return false

        // Filter for unread chapters
        if (filterUnread == 3 && !(item.manga.unread > 0 && !item.manga.hasRead)) return false
        if (filterUnread == 4 && !(item.manga.unread > 0 && item.manga.hasRead)) return false

        if (filterMangaType > 0) {
            if (if (filterMangaType == Manga.TYPE_MANHWA) {
                (filterMangaType != item.manga.seriesType(sourceManager = sourceManager) && filterMangaType != Manga.TYPE_WEBTOON)
            } else {
                    filterMangaType != item.manga.seriesType(sourceManager = sourceManager)
                }
            ) return false
        }

        // Filter for completed status of manga
        if (filterCompleted == STATE_INCLUDE && item.manga.status != SManga.COMPLETED) return false
        if (filterCompleted == STATE_EXCLUDE && item.manga.status == SManga.COMPLETED) return false

        // Filter for tracked (or per tracked service)
        if (filterTracked != STATE_IGNORE) {
            val tracks = db.getTracks(item.manga).executeAsBlocking()

            val hasTrack = loggedServices.any { service ->
                tracks.any { it.sync_id == service.id }
            }
            val service = if (filterTrackers.isNotEmpty()) loggedServices.find {
                context.getString(it.nameRes()) == filterTrackers
            } else null
            if (filterTracked == STATE_INCLUDE) {
                if (!hasTrack) return false
                if (filterTrackers.isNotEmpty()) {
                    if (service != null) {
                        val hasServiceTrack = tracks.any { it.sync_id == service.id }
                        if (!hasServiceTrack) return false
                        if (filterTracked == STATE_EXCLUDE && hasServiceTrack) return false
                    }
                }
            } else if (filterTracked == STATE_EXCLUDE) {
                if (hasTrack && filterTrackers.isEmpty()) return false
                if (filterTrackers.isNotEmpty()) {
                    if (service != null) {
                        val hasServiceTrack = tracks.any { it.sync_id == service.id }
                        if (hasServiceTrack) return false
                    }
                }
            }
        }
        // Filter for downloaded manga
        if (filterDownloaded != STATE_IGNORE) {
            val isDownloaded = when {
                item.manga.source == LocalSource.ID -> true
                item.downloadCount != -1 -> item.downloadCount > 0
                else -> downloadManager.getDownloadCount(item.manga) > 0
            }
            return if (filterDownloaded == STATE_INCLUDE) isDownloaded else !isDownloaded
        }
        return true
    }

    /**
     * Sets downloaded chapter count to each manga.
     *
     * @param itemList the map of manga.
     */
    private fun setDownloadCount(itemList: List<LibraryItem>) {
        if (!preferences.downloadBadge().getOrDefault()) {
            // Unset download count if the preference is not enabled.
            for (item in itemList) {
                item.downloadCount = -1
            }
            return
        }

        for (item in itemList) {
            item.downloadCount = downloadManager.getDownloadCount(item.manga)
        }
    }

    private fun setUnreadBadge(itemList: List<LibraryItem>) {
        val unreadType = preferences.unreadBadgeType().get()
        for (item in itemList) {
            item.unreadType = unreadType
        }
    }

    /**
     * Applies library sorting to the given list of manga.
     *
     * @param itemList the map to sort.
     */
    private fun applySort(itemList: List<LibraryItem>): List<LibraryItem> {
        val lastReadManga by lazy {
            var counter = 0
            db.getLastReadManga().executeAsBlocking().associate { it.id!! to counter++ }
        }

        val lastFetchedManga by lazy {
            var counter = 0
            db.getLastFetchedManga().executeAsBlocking().associate { it.id!! to counter++ }
        }

        val sortFn: (LibraryItem, LibraryItem) -> Int = { i1, i2 ->
            if (i1.header.category.id == i2.header.category.id) {
                val category = i1.header.category
                if (category.mangaOrder.isNullOrEmpty() && category.mangaSort == null) {
                    category.changeSortTo(preferences.librarySortingMode().getOrDefault())
                    if (category.id == 0) preferences.defaultMangaOrder()
                        .set(category.mangaSort.toString())
                    else if (!category.isDynamic) db.insertCategory(category).executeAsBlocking()
                }
                val compare = when {
                    category.mangaSort != null -> {
                        var sort = when (category.sortingMode() ?: LibrarySort.Title) {
                            LibrarySort.Title -> sortAlphabetical(i1, i2)
                            LibrarySort.LatestChapter -> i2.manga.last_update.compareTo(i1.manga.last_update)
                            LibrarySort.Unread -> when {
                                i1.manga.unread == i2.manga.unread -> 0
                                i1.manga.unread == 0 -> if (category.isAscending()) 1 else -1
                                i2.manga.unread == 0 -> if (category.isAscending()) -1 else 1
                                else -> i1.manga.unread.compareTo(i2.manga.unread)
                            }
                            LibrarySort.LastRead -> {
                                val manga1LastRead =
                                    lastReadManga[i1.manga.id!!] ?: lastReadManga.size
                                val manga2LastRead =
                                    lastReadManga[i2.manga.id!!] ?: lastReadManga.size
                                manga1LastRead.compareTo(manga2LastRead)
                            }
                            LibrarySort.TotalChapters -> {
                                i1.manga.totalChapters.compareTo(i2.manga.totalChapters)
                            }
                            LibrarySort.DateFetched -> {
                                val manga1LastRead =
                                    lastFetchedManga[i1.manga.id!!] ?: lastFetchedManga.size
                                val manga2LastRead =
                                    lastFetchedManga[i2.manga.id!!] ?: lastFetchedManga.size
                                manga1LastRead.compareTo(manga2LastRead)
                            }
                            LibrarySort.DateAdded -> i2.manga.date_added.compareTo(i1.manga.date_added)
                            LibrarySort.DragAndDrop -> {
                                if (category.isDynamic) {
                                    val category1 =
                                        allCategories.find { i1.manga.category == it.id }?.order
                                            ?: 0
                                    val category2 =
                                        allCategories.find { i2.manga.category == it.id }?.order
                                            ?: 0
                                    category1.compareTo(category2)
                                } else {
                                    sortAlphabetical(i1, i2)
                                }
                            }
                        }
                        if (!category.isAscending()) sort *= -1
                        sort
                    }
                    category.mangaOrder.isNotEmpty() -> {
                        val order = category.mangaOrder
                        val index1 = order.indexOf(i1.manga.id!!)
                        val index2 = order.indexOf(i2.manga.id!!)
                        when {
                            index1 == index2 -> 0
                            index1 == -1 -> -1
                            index2 == -1 -> 1
                            else -> index1.compareTo(index2)
                        }
                    }
                    else -> 0
                }
                if (compare == 0) {
                    sortAlphabetical(i1, i2)
                } else compare
            } else {
                val category = i1.header.category.order
                val category2 = i2.header.category.order
                category.compareTo(category2)
            }
        }

        return itemList.sortedWith(Comparator(sortFn))
    }

    /** Gets the category by id
     *
     * @param categoryId id of the categoty to get
     */
    private fun getCategory(categoryId: Int): Category {
        val category = categories.find { categoryId == it.id } ?: createDefaultCategory()
        category.isAlone = categories.size <= 1
        return category
    }

    /**
     * Sort 2 manga by the their title (and remove articles if need be)
     *
     * @param i1 the first manga
     * @param i2 the second manga to compare
     */
    private fun sortAlphabetical(i1: LibraryItem, i2: LibraryItem): Int {
        return if (removeArticles) {
            i1.manga.title.removeArticles().compareTo(i2.manga.title.removeArticles(), true)
        } else {
            i1.manga.title.compareTo(i2.manga.title, true)
        }
    }

    /**
     * Get the categories and all its manga from the database.
     *
     * @return an list of all the manga in a itemized form.
     */
    private fun getLibraryFromDB(): List<LibraryItem> {
        removeArticles = preferences.removeArticles().getOrDefault()
        val categories = db.getCategories().executeAsBlocking().toMutableList()
        var libraryManga = db.getLibraryMangas().executeAsBlocking()
        val showAll = showAllCategories
        if (groupType > BY_DEFAULT) {
            libraryManga = libraryManga.distinctBy { it.id }
        }

        val items = if (groupType <= BY_DEFAULT || !libraryIsGrouped) {
            val categoryAll = Category.createAll(
                context,
                preferences.librarySortingMode().getOrDefault(),
                preferences.librarySortingAscending().getOrDefault()
            )
            val catItemAll = LibraryHeaderItem({ categoryAll }, -1)
            val categorySet = mutableSetOf<Int>()
            val headerItems = (
                categories.mapNotNull { category ->
                    val id = category.id
                    if (id == null) null
                    else id to LibraryHeaderItem({ getCategory(id) }, id)
                } + (-1 to catItemAll) + (0 to LibraryHeaderItem({ getCategory(0) }, 0))
                ).toMap()

            val items = libraryManga.mapNotNull {
                val headerItem = (
                    if (!libraryIsGrouped) catItemAll
                    else headerItems[it.category]
                    ) ?: return@mapNotNull null
                categorySet.add(it.category)
                LibraryItem(it, headerItem)
            }.toMutableList()

            val categoriesHidden = preferences.collapsedCategories().getOrDefault().mapNotNull {
                it.toIntOrNull()
            }.toMutableSet()

            if (categorySet.contains(0)) categories.add(0, createDefaultCategory())
            if (libraryIsGrouped) {
                categories.forEach { category ->
                    val catId = category.id ?: return@forEach
                    if (catId > 0 && !categorySet.contains(catId) && (
                        catId !in categoriesHidden ||
                            !showAll
                        )
                    ) {
                        val headerItem = headerItems[catId]
                        if (headerItem != null) items.add(
                            LibraryItem(LibraryManga.createBlank(catId), headerItem)
                        )
                    } else if (catId in categoriesHidden && showAll && categories.size > 1) {
                        val mangaToRemove = items.filter { it.manga.category == catId }
                        val mergedTitle = mangaToRemove.joinToString("-") {
                            it.manga.title + "-" + it.manga.author
                        }
                        sectionedLibraryItems[catId] = mangaToRemove
                        items.removeAll(mangaToRemove)
                        val headerItem = headerItems[catId]
                        if (headerItem != null) items.add(
                            LibraryItem(LibraryManga.createHide(catId, mergedTitle), headerItem)
                        )
                    }
                }
            }

            categories.forEach {
                it.isHidden = it.id in categoriesHidden && showAll && categories.size > 1
            }
            this.categories = if (!libraryIsGrouped) {
                arrayListOf(categoryAll)
            } else {
                categories
            }

            items
        } else {
            val (items, customCategories) = getCustomMangaItems(libraryManga)
            this.categories = customCategories
            items
        }

        this.allCategories = categories

        return items
    }

    private fun getCustomMangaItems(libraryManga: List<LibraryManga>): Pair<List<LibraryItem>,
        List<Category>> {
        val tagItems: MutableMap<String, LibraryHeaderItem> = mutableMapOf()

        // internal function to make headers
        fun makeOrGetHeader(name: String): LibraryHeaderItem {
            return if (tagItems.containsKey(name)) {
                tagItems[name]!!
            } else {
                val headerItem = LibraryHeaderItem({ getCategory(it) }, tagItems.count())
                tagItems[name] = headerItem
                headerItem
            }
        }

        val items = libraryManga.map { manga ->
            when (groupType) {
                BY_TAG -> {
                    val tags = if (manga.genre.isNullOrBlank()) {
                        listOf("Unknown")
                    } else {
                        manga.genre?.split(",")?.mapNotNull {
                            val tag = it.trim().capitalizeWords()
                            if (tag.isBlank()) null else tag
                        } ?: listOf("Unknown")
                    }
                    tags.map {
                        LibraryItem(manga, makeOrGetHeader(it))
                    }
                }
                BY_TRACK_STATUS -> {
                    val status: String = {
                        val tracks = db.getTracks(manga).executeAsBlocking()
                        val track = tracks.find { track ->
                            loggedServices.any { it.id == track?.sync_id }
                        }
                        val service = loggedServices.find { it.id == track?.sync_id }
                        if (track != null && service != null) {
                            if (loggedServices.size > 1) {
                                service.getGlobalStatus(track.status)
                            } else {
                                service.getStatus(track.status)
                            }
                        } else {
                            context.getString(R.string.not_tracked)
                        }
                    }()
                    listOf(LibraryItem(manga, makeOrGetHeader(status)))
                }
                BY_SOURCE -> {
                    val source = sourceManager.getOrStub(manga.source)
                    listOf(
                        LibraryItem(
                            manga,
                            makeOrGetHeader("${source.name}$sourceSplitter${source.id}")
                        )
                    )
                }
                else -> listOf(LibraryItem(manga, makeOrGetHeader(mapStatus(manga.status))))
            }
        }.flatten().toMutableList()

        val hiddenDynamics = preferences.collapsedDynamicCategories().get()
        var headers = tagItems.map { item ->
            Category.createCustom(
                item.key,
                preferences.librarySortingMode().getOrDefault(),
                preferences.librarySortingAscending().getOrDefault()
            ).apply {
                id = item.value.catId
                if (name.contains(sourceSplitter)) {
                    val split = name.split(sourceSplitter)
                    name = split.first()
                    sourceId = split.last().toLongOrNull()
                }
                isHidden = getDynamicCategoryName(this) in hiddenDynamics
            }
        }.sortedBy {
            if (groupType == BY_TRACK_STATUS) {
                mapTrackingOrder(it.name)
            } else {
                it.name
            }
        }
        if (preferences.collapsedDynamicAtBottom().get()) {
            headers = headers.filterNot { it.isHidden } + headers.filter { it.isHidden }
        }
        headers.forEach { category ->
            val catId = category.id ?: return@forEach
            val headerItem =
                tagItems[if (category.sourceId != null) "${category.name}$sourceSplitter${category.sourceId}" else category.name]
            if (category.isHidden) {
                val mangaToRemove = items.filter { it.header.catId == catId }
                val mergedTitle = mangaToRemove.joinToString("-") {
                    it.manga.title + "-" + it.manga.author
                }
                sectionedLibraryItems[catId] = mangaToRemove
                items.removeAll { it.header.catId == catId }
                if (headerItem != null) items.add(
                    LibraryItem(LibraryManga.createHide(catId, mergedTitle), headerItem)
                )
            }
        }

        headers.forEachIndexed { index, category -> category.order = index }
        return items to headers
    }

    private fun mapStatus(status: Int): String {
        return context.getString(
            when (status) {
                SManga.LICENSED -> R.string.licensed
                SManga.ONGOING -> R.string.ongoing
                SManga.COMPLETED -> R.string.completed
                else -> R.string.unknown
            }
        )
    }

    private fun mapTrackingOrder(status: String): String {
        with(context) {
            return when (status) {
                getString(R.string.reading), getString(R.string.currently_reading) -> "1"
                getString(R.string.rereading) -> "2"
                getString(R.string.plan_to_read), getString(R.string.want_to_read) -> "3"
                getString(R.string.on_hold), getString(R.string.paused) -> "4"
                getString(R.string.completed) -> "5"
                getString(R.string.dropped) -> "6"
                else -> "7"
            }
        }
    }

    /** Create a default category with the sort set */
    private fun createDefaultCategory(): Category {
        val default = Category.createDefault(view.applicationContext ?: context)
        default.order = -1
        val defOrder = preferences.defaultMangaOrder().getOrDefault()
        if (defOrder.firstOrNull()?.isLetter() == true) default.mangaSort = defOrder.first()
        else default.mangaOrder = defOrder.split("/").mapNotNull { it.toLongOrNull() }
        return default
    }

    /** Requests the library to be filtered. */
    fun requestFilterUpdate() {
        presenterScope.launch {
            var mangaMap = allLibraryItems
            mangaMap = applyFilters(mangaMap)
            mangaMap = applySort(mangaMap)
            sectionLibrary(mangaMap)
        }
    }

    /** Requests the library to have download badges added/removed. */
    fun requestDownloadBadgesUpdate() {
        presenterScope.launch {
            val mangaMap = allLibraryItems
            setDownloadCount(mangaMap)
            allLibraryItems = mangaMap
            val current = libraryItems
            setDownloadCount(current)
            sectionLibrary(current)
        }
    }

    /** Requests the library to have unread badges changed. */
    fun requestUnreadBadgesUpdate() {
        presenterScope.launch {
            val mangaMap = allLibraryItems
            setUnreadBadge(mangaMap)
            allLibraryItems = mangaMap
            val current = libraryItems
            setUnreadBadge(current)
            sectionLibrary(current)
        }
    }

    /** Requests the library to be sorted. */
    private fun requestSortUpdate() {
        presenterScope.launch {
            var mangaMap = libraryItems
            mangaMap = applySort(mangaMap)
            sectionLibrary(mangaMap)
        }
    }

    /**
     * Returns the common categories for the given list of manga.
     *
     * @param mangas the list of manga.
     */
    fun getCommonCategories(mangas: List<Manga>): Collection<Category> {
        if (mangas.isEmpty()) return emptyList()
        return mangas.toSet()
            .map { db.getCategoriesForManga(it).executeAsBlocking() }
            .reduce { set1: Iterable<Category>, set2 -> set1.intersect(set2).toMutableList() }
    }

    fun getMangaUrls(mangas: List<Manga>): List<String> {
        return mangas.mapNotNull { manga ->
            val source = sourceManager.get(manga.source) as? HttpSource ?: return@mapNotNull null
            source.mangaDetailsRequest(manga).url.toString()
        }
    }

    /**
     * Remove the selected manga from the library.
     *
     * @param mangas the list of manga to delete.
     */
    fun removeMangaFromLibrary(mangas: List<Manga>) {
        presenterScope.launch {
            // Create a set of the list
            val mangaToDelete = mangas.distinctBy { it.id }
            mangaToDelete.forEach { it.favorite = false }

            db.insertMangas(mangaToDelete).executeOnIO()
            getLibrary()
        }
    }

    /** Remove manga from the library and delete the downloads */
    fun confirmDeletion(mangas: List<Manga>) {
        launchIO {
            val mangaToDelete = mangas.distinctBy { it.id }
            mangaToDelete.forEach { manga ->
                coverCache.deleteFromCache(manga)
                val source = sourceManager.get(manga.source) as? HttpSource
                if (source != null) {
                    downloadManager.deleteManga(manga, source)
                }
            }
        }
    }

    /** Called when Library Service updates a manga, update the item as well */
    fun updateManga() {
        presenterScope.launch {
            getLibrary()
        }
    }

    /** Undo the removal of the manga once in library */
    fun reAddMangas(mangas: List<Manga>) {
        presenterScope.launch {
            val mangaToAdd = mangas.distinctBy { it.id }
            mangaToAdd.forEach { it.favorite = true }
            db.insertMangas(mangaToAdd).executeOnIO()
            getLibrary()
            mangaToAdd.forEach { db.insertManga(it).executeAsBlocking() }
        }
    }

    /**
     * Move the given list of manga to categories.
     *
     * @param categories the selected categories.
     * @param mangas the list of manga to move.
     */
    fun moveMangasToCategories(categories: List<Category>, mangas: List<Manga>) {
        val mc = ArrayList<MangaCategory>()

        for (manga in mangas) {
            for (cat in categories) {
                mc.add(MangaCategory.create(manga, cat))
            }
        }
        db.setMangaCategories(mc, mangas)
        getLibrary()
    }

    /** Returns first unread chapter of a manga */
    fun getFirstUnread(manga: Manga): Chapter? {
        val chapters = db.getChapters(manga).executeAsBlocking()
        return ChapterSort(manga, chapterFilter, preferences).getNextUnreadChapter(chapters, false)
    }

    /** Update a category's sorting */
    fun sortCategory(catId: Int, order: Char) {
        val category = categories.find { catId == it.id } ?: return
        category.mangaSort = order
        if (catId == -1 || category.isDynamic) {
            val sort = category.sortingMode() ?: LibrarySort.Title
            preferences.librarySortingMode().set(sort.mainValue)
            preferences.librarySortingAscending().set(category.isAscending())
            categories.forEach {
                it.mangaSort = category.mangaSort
            }
        } else if (catId >= 0) {
            if (category.id == 0) preferences.defaultMangaOrder().set(category.mangaSort.toString())
            else Injekt.get<DatabaseHelper>().insertCategory(category).executeAsBlocking()
        }
        requestSortUpdate()
    }

    /** Update a category's order */
    fun rearrangeCategory(catId: Int?, mangaIds: List<Long>) {
        presenterScope.launch {
            val category = categories.find { catId == it.id } ?: return@launch
            if (category.isDynamic) return@launch
            category.mangaSort = null
            category.mangaOrder = mangaIds
            if (category.id == 0) preferences.defaultMangaOrder().set(mangaIds.joinToString("/"))
            else db.insertCategory(category).executeOnIO()
            requestSortUpdate()
        }
    }

    /** Shift a manga's category via drag & drop */
    fun moveMangaToCategory(
        manga: LibraryManga,
        catId: Int?,
        mangaIds: List<Long>
    ) {
        presenterScope.launch {
            val categoryId = catId ?: return@launch
            val category = categories.find { catId == it.id } ?: return@launch
            if (category.isDynamic) return@launch

            val oldCatId = manga.category
            manga.category = categoryId

            val mc = ArrayList<MangaCategory>()
            val categories =
                if (catId == 0) emptyList()
                else {
                    db.getCategoriesForManga(manga).executeOnIO()
                        .filter { it.id != oldCatId } + listOf(category)
                }

            for (cat in categories) {
                mc.add(MangaCategory.create(manga, cat))
            }

            db.setMangaCategories(mc, listOf(manga))

            if (category.mangaSort == null) {
                val ids = mangaIds.toMutableList()
                if (!ids.contains(manga.id!!)) ids.add(manga.id!!)
                category.mangaOrder = ids
                if (category.id == 0) preferences.defaultMangaOrder()
                    .set(mangaIds.joinToString("/"))
                else db.insertCategory(category).executeAsBlocking()
            }
            getLibrary()
        }
    }

    /** Returns if manga is in a category by id */
    fun mangaIsInCategory(manga: LibraryManga, catId: Int?): Boolean {
        val categories = db.getCategoriesForManga(manga).executeAsBlocking().map { it.id }
        return catId in categories
    }

    fun toggleCategoryVisibility(categoryId: Int) {
        // if (categories.find { it.id == categoryId }?.isDynamic == true) return
        if (groupType == BY_DEFAULT) {
            val categoriesHidden = preferences.collapsedCategories().getOrDefault().mapNotNull {
                it.toIntOrNull()
            }.toMutableSet()
            if (categoryId in categoriesHidden) {
                categoriesHidden.remove(categoryId)
            } else {
                categoriesHidden.add(categoryId)
            }
            preferences.collapsedCategories()
                .set(categoriesHidden.map { it.toString() }.toMutableSet())
        } else {
            val categoriesHidden = preferences.collapsedDynamicCategories().get().toMutableSet()
            val category = getCategory(categoryId)
            val dynamicName = getDynamicCategoryName(category)
            if (dynamicName in categoriesHidden) {
                categoriesHidden.remove(dynamicName)
            } else {
                categoriesHidden.add(dynamicName)
            }
            preferences.collapsedDynamicCategories().set(categoriesHidden)
        }
        getLibrary()
    }

    private fun getDynamicCategoryName(category: Category): String =
        groupType.toString() + dynamicCategorySplitter + (
            category.sourceId?.toString()
                ?: category.name
            )

    fun toggleAllCategoryVisibility() {
        if (groupType == BY_DEFAULT) {
            if (allCategoriesExpanded()) {
                preferences.collapsedCategories()
                    .set(allCategories.map { it.id.toString() }.toMutableSet())
            } else {
                preferences.collapsedCategories().set(mutableSetOf())
            }
        } else {
            if (allCategoriesExpanded()) {
                preferences.collapsedDynamicCategories() += categories.map {
                    getDynamicCategoryName(
                        it
                    )
                }
            } else {
                preferences.collapsedDynamicCategories() -= categories.map {
                    getDynamicCategoryName(
                        it
                    )
                }
            }
        }
        getLibrary()
    }

    fun allCategoriesExpanded(): Boolean {
        return if (groupType == BY_DEFAULT) {
            preferences.collapsedCategories().getOrDefault().isEmpty()
        } else {
            categories.none { it.isHidden }
        }
    }

    /** download All unread */
    fun downloadUnread(mangaList: List<Manga>) {
        presenterScope.launch {
            withContext(Dispatchers.IO) {
                mangaList.forEach {
                    val chapters = db.getChapters(it).executeAsBlocking().filter { !it.read }
                    downloadManager.downloadChapters(it, chapters)
                }
            }
            if (preferences.downloadBadge().getOrDefault()) {
                requestDownloadBadgesUpdate()
            }
        }
    }

    fun markReadStatus(mangaList: List<Manga>, markRead: Boolean) {
        presenterScope.launch {
            withContext(Dispatchers.IO) {
                mangaList.forEach {
                    withContext(Dispatchers.IO) {
                        val chapters = db.getChapters(it).executeAsBlocking()
                        chapters.forEach {
                            it.read = markRead
                            it.last_page_read = 0
                        }
                        db.updateChaptersProgress(chapters).executeAsBlocking()
                        if (markRead && preferences.removeAfterMarkedAsRead()) {
                            deleteChapters(it, chapters)
                        }
                    }
                }
                getLibrary()
            }
        }
    }

    private fun deleteChapters(manga: Manga, chapters: List<Chapter>) {
        sourceManager.get(manga.source)?.let { source ->
            downloadManager.deleteChapters(chapters, manga, source)
        }
    }

    companion object {
        private var lastLibraryItems: List<LibraryItem>? = null
        private var lastCategories: List<Category>? = null
        private const val sourceSplitter = "◘•◘"
        private const val dynamicCategorySplitter = "▄╪\t▄╪\t▄"

        private val randomTags = arrayOf(0, 1, 2)
        private const val randomSource = 4
        private const val randomTitle = 3
        private const val randomTag = 0
        private val randomGroupOfTags = arrayOf(1, 2)
        private const val randomGroupOfTagsNormal = 1
        private const val randomGroupOfTagsNegate = 2

        suspend fun setSearchSuggestion(
            preferences: PreferencesHelper,
            db: DatabaseHelper,
            sourceManager: SourceManager
        ) {
            val random: Random = {
                val cal = Calendar.getInstance()
                cal.time = Date()
                cal[Calendar.MINUTE] = 0
                cal[Calendar.SECOND] = 0
                cal[Calendar.MILLISECOND] = 0
                Random(cal.time.time)
            }()

            val recentManga by lazy {
                runBlocking {
                    RecentsPresenter.getRecentManga(true).map { it.first }
                }
            }
            val libraryManga by lazy { db.getLibraryMangas().executeAsBlocking() }
            preferences.librarySearchSuggestion().set(
                when (val value = random.nextInt(0, 5)) {
                    randomSource -> {
                        val distinctSources = libraryManga.distinctBy { it.source }
                        val randomSource =
                            sourceManager.get(
                                distinctSources.randomOrNull(random)?.source ?: 0L
                            )?.name
                        randomSource?.chopByWords(15)
                    }
                    randomTitle -> {
                        libraryManga.randomOrNull(random)?.title?.chopByWords(15)
                    }
                    in randomTags -> {
                        val tags = recentManga.map {
                            it.genre.orEmpty().split(",").map(String::trim)
                        }
                            .flatten()
                            .filter { it.isNotBlank() }
                        val distinctTags = tags.distinct()
                        if (value in randomGroupOfTags && distinctTags.size > 6) {
                            val shortestTagsSort = distinctTags.sortedBy { it.length }
                            val offset = random.nextInt(0, distinctTags.size / 2 - 2)
                            var offset2 = random.nextInt(0, distinctTags.size / 2 - 2)
                            while (offset2 == offset) {
                                offset2 = random.nextInt(0, distinctTags.size / 2 - 2)
                            }
                            if (value == randomGroupOfTagsNormal) {
                                "${shortestTagsSort[offset]}, " + shortestTagsSort[offset2]
                            } else {
                                "${shortestTagsSort[offset]}, -" + shortestTagsSort[offset2]
                            }
                        } else {
                            val group = tags.groupingBy { it }.eachCount()
                            val groupedTags = distinctTags.sortedByDescending { group[it] }
                            groupedTags.take(8).randomOrNull(random)
                        }
                    }
                    else -> ""
                } ?: ""
            )

            if (preferences.showLibrarySearchSuggestions().isNotSet()) {
                preferences.showLibrarySearchSuggestions().set(true)
            }
            preferences.lastLibrarySuggestion().set(Date().time)
        }

        /** Give library manga to a date added based on min chapter fetch */
        fun updateDB() {
            val db: DatabaseHelper = Injekt.get()
            db.inTransaction {
                val libraryManga = db.getLibraryMangas().executeAsBlocking()
                libraryManga.forEach { manga ->
                    if (manga.date_added == 0L) {
                        val chapters = db.getChapters(manga).executeAsBlocking()
                        manga.date_added = chapters.minByOrNull { it.date_fetch }?.date_fetch ?: 0L
                        db.insertManga(manga).executeAsBlocking()
                    }
                }
            }
        }

        fun updateCustoms() {
            val db: DatabaseHelper = Injekt.get()
            val cc: CoverCache = Injekt.get()
            db.inTransaction {
                val libraryManga = db.getLibraryMangas().executeAsBlocking()
                libraryManga.forEach { manga ->
                    if (manga.thumbnail_url?.startsWith("custom", ignoreCase = true) == true) {
                        val file = cc.getCoverFile(manga)
                        if (file.exists()) {
                            file.renameTo(cc.getCustomCoverFile(manga))
                        }
                        manga.thumbnail_url =
                            manga.thumbnail_url!!.toLowerCase(Locale.ROOT).substringAfter("custom-")
                        db.insertManga(manga).executeAsBlocking()
                    }
                }
            }
        }
    }
}
