package de.arxsilex.maptools.converter.desktop

import de.arxsilex.maptools.converter.core.preview.PreviewDataset
import de.arxsilex.maptools.converter.core.preview.PreviewLabel
import de.arxsilex.maptools.converter.core.preview.PreviewLabelClass
import java.text.Normalizer
import java.nio.file.Files
import java.sql.DriverManager

internal object PreviewSearch {
    fun find(dataset: PreviewDataset, query: String, limit: Int = 12): List<PreviewLabel> {
        val needle = query.normalizedSearchText()
        if (needle.length < 2) return emptyList()
        return dataset.labels.asSequence()
            .map { it to it.text.normalizedSearchText() }
            .filter { (_, text) -> needle in text }
            .sortedWith(compareBy<Pair<PreviewLabel, String>> { (_, text) -> !text.startsWith(needle) }
                .thenByDescending { (label, _) -> label.importance }
                .thenBy { (label, _) -> label.text })
            .map(Pair<PreviewLabel, String>::first)
            .distinctBy { listOf(it.labelClass, it.text, it.latitude, it.longitude) }
            .take(limit)
            .toList()
    }

    fun findPoiDatabases(sources: List<PreviewDetailSource>, query: String, limit: Int = 24): List<PreviewLabel> {
        val needle = query.normalizedSearchText()
        if (needle.length < 2) return emptyList()
        val fullTextQuery = Regex("[\\p{L}\\p{N}]+").findAll(needle)
            .map(MatchResult::value)
            .joinToString(" AND ") { "\"$it\"*" }
        if (fullTextQuery.isBlank()) return emptyList()
        Class.forName("org.sqlite.JDBC")
        return sources.asSequence().mapNotNull(PreviewDetailSource::poiPath).filter(Files::isRegularFile).flatMap { path ->
            val uri = path.toAbsolutePath().toUri()
            DriverManager.getConnection("jdbc:sqlite:$uri?mode=ro").use { connection ->
                val columns = connection.createStatement().use { statement ->
                    statement.executeQuery("SELECT name FROM pragma_table_info('poi_index')").use { result ->
                        buildSet { while (result.next()) add(result.getString(1)) }
                    }
                }
                if ("minLat" in columns) {
                    connection.prepareStatement(
                        "SELECT d.data, (i.minLat + i.maxLat) / 2.0, (i.minLon + i.maxLon) / 2.0 " +
                            "FROM poi_data_fts JOIN poi_data d ON d.id = poi_data_fts.rowid " +
                            "JOIN poi_index i ON i.id = d.id WHERE poi_data_fts MATCH ? LIMIT ?",
                    ).use { statement ->
                        statement.setString(1, fullTextQuery)
                        statement.setInt(2, limit)
                        statement.executeQuery().use { result -> poiSearchResults(result, limit) }
                    }
                } else {
                    // POI v3 has no FTS index. Stream its compact data table and apply the same
                    // accent-independent matching in-process so "Cafe" also finds "Café".
                    connection.prepareStatement(
                        "SELECT d.data, i.lat, i.lon FROM poi_data d JOIN poi_index i ON i.id = d.id",
                    ).use { statement ->
                        statement.executeQuery().use { result -> poiSearchResults(result, limit, needle) }
                    }
                }
            }.asSequence()
        }.distinctBy { listOf(it.text, it.latitude, it.longitude) }.take(limit).toList()
    }

    private fun poiSearchResults(
        result: java.sql.ResultSet,
        limit: Int,
        normalizedNeedle: String? = null,
    ): List<PreviewLabel> = buildList {
        while (result.next() && size < limit) {
            val data = result.getString(1).orEmpty()
            val name = poiName(data) ?: continue
            if (normalizedNeedle != null && normalizedNeedle !in data.normalizedSearchText()) continue
            add(
                PreviewLabel(
                    latitude = result.getDouble(2),
                    longitude = result.getDouble(3),
                    text = name.take(80),
                    labelClass = PreviewLabelClass.POI,
                    importance = 40,
                ),
            )
        }
    }

    private fun poiName(data: String): String? {
        val tags = data.split('\r', '\n', '\u0000', ';').mapNotNull { entry ->
            val separator = entry.indexOf('=')
            if (separator <= 0) null else entry.substring(0, separator) to entry.substring(separator + 1)
        }.toMap()
        return (tags["name:de"] ?: tags["name"] ?: tags["ref"])
            ?.trim()
            ?.takeIf(String::isNotBlank)
    }

    private fun String.normalizedSearchText(): String = Normalizer.normalize(trim(), Normalizer.Form.NFD)
        .replace(Regex("\\p{M}+"), "")
        .lowercase()
}
