/*
 * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
 */

package at.bitfire.davdroid.util

import at.bitfire.dav4jvm.ktor.toUrlOrNull
import at.bitfire.davdroid.util.DavUtils.generateUidIfNecessary
import at.bitfire.davdroid.util.DavUtils.toUrlOrNull
import io.ktor.http.ContentType
import io.ktor.http.Url
import org.jetbrains.annotations.TestOnly
import java.net.URI
import java.net.URISyntaxException
import java.util.Locale
import java.util.UUID

/**
 * Some WebDAV and HTTP network utility methods.
  */
object DavUtils {

    const val MIME_TYPE_ACCEPT_ALL = "*/*"

    val MEDIA_TYPE_OCTET_STREAM = ContentType.Application.OctetStream
    val MEDIA_TYPE_VCARD = ContentType.Text.VCard

    /**
     * Builds an HTTP `Accept` header that accepts anything (&#42;/&#42;), but optionally
     * specifies a preference.
     *
     * @param preferred  preferred MIME type (optional)
     *
     * @return `media-range` for `Accept` header that accepts anything, but prefers [preferred] (if it was specified)
     */
    fun acceptAnything(preferred: ContentType?): String =
        if (preferred != null)
            "$preferred, $MIME_TYPE_ACCEPT_ALL;q=0.8"
        else
            MIME_TYPE_ACCEPT_ALL

    /**
     * Converts an Android (A)RGB color to a standard hex code color like it's used by CalDAV
     * `calendar-color`.
     *
     * @param colorWithAlpha    color code, with or without alpha value (ARGB)
     *
     * @return hex color with prefix, like `#FAC308` (RGB)
     */
    fun argbToHexColor(colorWithAlpha: Int): String {
        val color = colorWithAlpha and 0xFFFFFF
        return String.format(Locale.ROOT, "#%06X", color)
    }

    /**
     * Generates a usable WebDAV resource name (file name) from an UID.
     *
     * If the UID contains only characters that are usually not problematic in file names,
     * the returned value is `<uid>.<suffix>`. If there are problematic characters,
     * the file name will be generated from a random UUID plus suffix instead.
     *
     * @param uid           UID of the iCalendar or vCard
     * @param suffix        suffix to use (without dot, for instance `ics` for iCalendar files)
     * @param generateUuid  generator that generates a random UUID
     *
     * @return file name that can be used to upload the resource
     */
    fun fileNameFromUid(
        uid: String,
        suffix: String,
        generateUuid: () -> String = { UUID.randomUUID().toString() }
    ): String {
        val uidIsGoodBaseName: Boolean = uid.all { char ->
            // see RFC 2396 2.2
            char.isLetterOrDigit() || arrayOf(                  // allow letters and digits
                ';', ':', '@', '&', '=', '+', '$', ',',         // allow reserved characters except '/' and '?'
                '-', '_', '.', '!', '~', '*', '\'', '(', ')'    // allow unreserved characters
            ).contains(char)
        }
        val baseName = if (uidIsGoodBaseName)
            uid
        else
            generateUuid()
        return "$baseName.$suffix"
    }

    /**
     * Result of [generateUidIfNecessary].
     *
     * @param uid           resulting UID (either from existing or generated)
     * @param generated     *true*: [uid] was generated by [generateUidIfNecessary]; *false*: [uid] was taken from existing UID
     */
    data class UidGenerationResult(
        val uid: String,
        val generated: Boolean
    )
    /**
     * Generates a UID for an iCalendar/vCard if there is no existing UID.
     *
     * @param existingUid       existing UID (may be null)
     * @param generateUuid      generator that generates a random UUID
     *
     * @return decomposable result that contains either the existing or the generated UID and whether it was generated
     */
    fun generateUidIfNecessary(
        existingUid: String?,
        generateUuid: () -> String = { UUID.randomUUID().toString() }
    ): UidGenerationResult =
        if (existingUid == null) {
            // generate new UID
            UidGenerationResult(generateUuid(), generated = true)
        } else {
            // use existing UID
            UidGenerationResult(existingUid, generated = false)
        }


    // extension methods

    /**
     * Safely gets the last segment of the URL, or returns `"/"` if none could be obtained.
     */
    val Url.lastSegment: String
        get() = this.segments.lastOrNull { it.isNotEmpty() } ?: "/"

    fun String.toURIorNull(): URI? = try {
        URI(this)
    } catch (_: URISyntaxException) {
        null
    }

    fun URI.toUrlOrNull(): Url? = toString().toUrlOrNull()

    /**
     * An unsafe call to convert a [String] to a [Url].
     *
     * Highly preferred to use [toUrlOrNull] instead, and handle nullability.
     * @throws IllegalArgumentException If the source string is not a valid URL.
     */
    @TestOnly
    fun String.toUrl(): Url = toUrlOrNull() ?: throw IllegalArgumentException("The source string ($this) is not a valid URL")

}
