/**
 * An implementation of an auto-suggestion algorithm suitable for Eve items.
 */

package theorycrafter.utils


/**
 * An auto-suggest function type.
 *
 * A `null` result indicates no search has been performed (perhaps the query is too short).
 * An empty result indicates there are no matches.
 */
typealias AutoSuggest<T> = suspend (String) -> List<T>?


/**
 * Converts a lambda into an [AutoSuggest], which is sometimes useful to get the compiler understand the type.
 */
fun <T> AutoSuggest(func: suspend (String) -> List<T>?): AutoSuggest<T> = func


/**
 * The delimiters by which we split the search query into terms.
 */
private val IS_TERM_DELIMITER = " -".containsFunction()


/**
 * The characters we filter out from the search terms.
 */
private val IS_FILTERED_CHARACTER = "'".containsFunction()


/**
 * Alternate "spellings" of certain terms which we match to the original "spelling" in an acronym search.
 */
private val ALTERNATE_ACRONYM_TERMS = mapOf(
    // The terms are matched after converting them to lowercase, and the query is also matched after converting it
    // to lowercase. So both keys and values here need to be lowercase.
    "i" to "1",
    "ii" to "2"
)


/**
 * Returns a function that can be used to auto-suggest eve-related values from the given list.
 */
fun <T: Any> autoSuggest(


    /**
     * The items to search in.
     */
    items: Iterable<T>,


    /**
     * The minimum number of significant input characters that causes an actual search.
     */
    minCharacters: Int = 1,


    /**
     * The function to use to obtain the string by which the item can be searched.
     */
    itemToSearchString: ((T) -> String) = Any::toString,


    /**
     * An optional function to sort suggestions with identical scores.
     */
    suggestionComparator: Comparator<in T>? = null,


    /**
     * Whether to place items that match earlier terms earlier in the suggestions.
     */
    preferEarlierTerms: Boolean = false,


): AutoSuggest<T> {
    val search = StringSearch(
        minCharacters = minCharacters,
        suggestionComparator = suggestionComparator,
        isTermDelimiter = IS_TERM_DELIMITER,
        isFiltered = IS_FILTERED_CHARACTER
    )

    val config = SearchConfig(
        includeAcronyms = true,
        alternateAcronymTerms = ALTERNATE_ACRONYM_TERMS,
        earlierTermsPreferenceFactor =
            if (preferEarlierTerms) DefaultSearchConfig.earlierTermsPreferenceFactor else 0,
        shorterTermsPreferenceFactor = 0
    )

    for (item in items) {
        search.addItem(
            item = item,
            text = itemToSearchString(item),
            config = config
        )
    }

    return search::query
}


/**
 * Returns an [AutoSuggest] that calls the given function to provide the items to return when the query text is empty.
 */
fun <T> AutoSuggest<T>.onEmptyQueryReturn(items: () -> List<T>?): AutoSuggest<T> =
    AutoSuggest { query ->
        if (query.isEmpty())
            items()
        else
            this(query)
    }


/**
 * Returns an [AutoSuggest] that filters results using the given predicate.
 */
fun <T> AutoSuggest<T>.filterResults(predicate: (T) -> Boolean): AutoSuggest<T> {
    return AutoSuggest {
        this@filterResults(it)?.filter(predicate)
    }
}


/**
 * Returns an [AutoSuggest] that concatenates the results of [this] and [autoSuggest].
 */
operator fun <T1, T2, T> AutoSuggest<T1>.plus(autoSuggest: AutoSuggest<T2>): AutoSuggest<T> where T1: T, T2: T {
    return AutoSuggest {
        val thisResult = this(it)
        val thatResult = autoSuggest(it)
        if ((thisResult == null) && (thatResult == null))
            return@AutoSuggest null
        else
            (thisResult ?: emptyList()) + (thatResult ?: emptyList())
    }
}