package jp.juggler.util

import android.content.Context
import android.net.Uri
import android.os.Build
import android.text.Spannable
import android.text.SpannableString
import android.text.SpannableStringBuilder
import android.util.Base64
import java.security.MessageDigest
import java.util.*
import java.util.regex.Matcher
import java.util.regex.Pattern

object StringUtils {

    val log = LogCategory("StringUtils")

    val hexLower =
        charArrayOf('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f')

    // BDI制御文字からその制御文字を閉じる文字を得るためのマップ
    val sanitizeBdiMap = HashMap<Char, Char>().apply {

        val PDF = 0x202C.toChar() // Pop directional formatting (PDF)
        this[0x202A.toChar()] = PDF // Left-to-right embedding (LRE)
        this[0x202B.toChar()] = PDF // Right-to-left embedding (RLE)
        this[0x202D.toChar()] = PDF // Left-to-right override (LRO)
        this[0x202E.toChar()] = PDF // Right-to-left override (RLO)

        val PDI = 0x2069.toChar() // Pop directional isolate (PDI)
        this[0x2066.toChar()] = PDI // Left-to-right isolate (LRI)
        this[0x2067.toChar()] = PDI // Right-to-left isolate (RLI)
        this[0x2068.toChar()] = PDI // First strong isolate (FSI)

        //	private const val ALM = 0x061c.toChar() // Arabic letter mark (ALM)
        //	private const val LRM = 0x200E.toChar() //	Left-to-right mark (LRM)
        //	private const val RLM = 0x200F.toChar() //	Right-to-left mark (RLM)
    }
}

////////////////////////////////////////////////////////////////////
// ByteArray

fun ByteArray.encodeBase64Url(): String =
    Base64.encodeToString(this, Base64.URL_SAFE or Base64.NO_PADDING or Base64.NO_WRAP)

fun ByteArray.digestSHA256(): ByteArray {
    val digest = MessageDigest.getInstance("SHA-256")
    digest.reset()
    return digest.digest(this)
}

fun ByteArray.startWith(
    key: ByteArray,
    thisOffset: Int = 0,
    keyOffset: Int = 0,
    length: Int = key.size - keyOffset,
): Boolean {
    if (this.size - thisOffset >= length && key.size - keyOffset >= length) {
        for (i in 0 until length) {
            if (this[i + thisOffset] != key[i + keyOffset]) return false
        }
        return true
    }
    return false
}

// 各要素の下位8ビットを使ってバイト配列を作る
fun IntArray.toByteArray(): ByteArray {
    val dst = ByteArray(this.size)
    for (i in this.indices) {
        dst[i] = this[i].toByte()
    }
    return dst
}

// 各要素の下位8ビットを使ってバイト配列を作る
fun CharArray.toLowerByteArray(): ByteArray {
    val dst = ByteArray(this.size)
    for (i in this.indices) {
        dst[i] = this[i].code.toByte()
    }
    return dst
}

////////////////////////////////////////////////////////////////////
// CharSequence

fun CharSequence.replaceFirst(pattern: Pattern, replacement: String): String =
    pattern.matcher(this).replaceFirst(replacement)

fun CharSequence.replaceAll(pattern: Pattern, replacement: String): String =
    pattern.matcher(this).replaceAll(replacement)

// %1$s を含む文字列リソースを利用して装飾テキストの前後に文字列を追加する
fun CharSequence?.intoStringResource(context: Context, stringId: Int): Spannable {

    val s = context.getString(stringId)
    val end = s.length
    val pos = s.indexOf("%1\$s")
    if (pos == -1) return SpannableString(s)

    val sb = SpannableStringBuilder()
    if (pos > 0) sb.append(s.substring(0, pos))
    if (this != null) sb.append(this)
    if (pos + 4 < end) sb.append(s.substring(pos + 4, end))
    return sb
}

//fun Char.hex2int() : Int {
//	if( '0' <= this && this <= '9') return ((this-'0'))
//	if( 'A' <= this && this <= 'F') return ((this-'A')+0xa)
//	if( 'a' <= this && this <= 'f') return ((this-'a')+0xa)
//	return 0
//}

fun CharSequence.codePointBefore(index: Int): Int {
    if (index <= 0) return -1
    val c2 = this[index - 1]
    if (Character.isLowSurrogate(c2) && index > 1) {
        val c1 = this[index - 2]
        if (Character.isHighSurrogate(c1)) {
            return Character.toCodePoint(c1, c2)
        }
    }
    return c2.code
}

inline fun <S : CharSequence, Z : Any?> S?.letNotEmpty(block: (S) -> Z?): Z? =
    if (this?.isNotEmpty() == true) {
        block(this)
    } else {
        null
    }

// usage: str.notEmpty() ?: fallback
// equivalent: if(this.isNotEmpty() ) this else null
fun <S : CharSequence> S?.notEmpty(): S? = if (this?.isNotEmpty() == true) this else null

fun <S : CharSequence> S?.notBlank(): S? = if (this?.isNotBlank() == true) this else null

fun CharSequence.toUri(): Uri = Uri.parse(toString())

fun CharSequence?.mayUri(): Uri? = try {
    this?.notEmpty()?.toUri()
} catch (ignored: Throwable) {
    null
}

////////////////////////////////////////////////////////////////////
// string

val charsetUTF8 = Charsets.UTF_8

fun String.appendIf(text: String, flag: Boolean) = if (flag) "$this$text" else this

// 文字列とバイト列の変換
fun String.encodeUTF8() = this.toByteArray(charsetUTF8)

fun ByteArray.decodeUTF8() = this.toString(charsetUTF8)

fun String.codePointCount(beginIndex: Int = 0): Int = this.codePointCount(beginIndex, this.length)

// 16進ダンプ
fun ByteArray.encodeHex(): String {
    val sb = StringBuilder()
    for (b in this) {
        sb.appendHex2(b.toInt())
    }
    return sb.toString()
}

fun StringBuilder.appendHex2(value: Int): StringBuilder {
    this.append(StringUtils.hexLower[(value shr 4) and 15])
    this.append(StringUtils.hexLower[value and 15])
    return this
}

fun ByteArray.encodeHexLower(): String {
    val size = this.size
    val sb = StringBuilder(size * 2)
    for (i in 0 until size) {
        val value = this[i].toInt()
        sb.append(StringUtils.hexLower[(value shr 4) and 15])
        sb.append(StringUtils.hexLower[value and 15])
    }
    return sb.toString()
}

fun String?.optInt(): Int? {
    return try {
        this?.toInt(10)
    } catch (ignored: Throwable) {
        null
    }
}

fun String?.filterNotEmpty(): String? = when {
    this == null -> null
    this.isEmpty() -> null
    else -> this
}

fun String.ellipsizeDot3(max: Int) = when {
    this.length > max -> this.substring(0, max - 1) + "…"
    else -> this
}

//fun String.toCamelCase() : String {
//	val sb = StringBuilder()
//	for(s in this.split("_".toRegex())) {
//		if(s.isEmpty()) continue
//		sb.append(Character.toUpperCase(s[0]))
//		sb.append(s.substring(1, s.length).toLowerCase())
//	}
//	return sb.toString()
//}

fun ellipsize(src: String, limit: Int): String =
    if (src.codePointCount(0, src.length) <= limit) {
        src
    } else {
        "${src.substring(0, src.offsetByCodePoints(0, limit))}…"
    }

fun String.sanitizeBDI(): String {

    // 文字列をスキャンしてBDI制御文字をスタックに入れていく
    var stack: LinkedList<Char>? = null
    for (c in this) {
        val closer = StringUtils.sanitizeBdiMap[c]
        if (closer != null) {
            if (stack == null) stack = LinkedList()
            stack.add(closer)
        } else if (stack?.isNotEmpty() == true && stack.last == c) {
            stack.removeLast()
        }
    }

    if (stack?.isNotEmpty() == true) {
        val sb = StringBuilder(this.length + stack.size)
        sb.append(this)
        while (!stack.isEmpty()) {
            sb.append(stack.removeLast())
        }
        return sb.toString()
    }

    return this
}

//fun String.dumpCodePoints() : CharSequence {
//	val sb = StringBuilder()
//	val length = this.length
//	var i=0
//	while(i<length) {
//		val cp = codePointAt(i)
//		sb.append(String.format("0x%x,", cp))
//		i += Character.charCount(cp)
//	}
//	return sb
//}

// 指定した文字数までの部分文字列を返す
// 文字列の長さが足りない場合は指定オフセットから終端までの長さを返す
//fun String.safeSubstring(count : Int, offset : Int = 0) = when {
//	offset + count <= length -> this.substring(offset, count)
//	else -> this.substring(offset, length)
//}

//// MD5ハッシュの作成
//@Suppress("unused")
//fun String.digestMD5() : String {
//	val md = MessageDigest.getInstance("MD5")
//	md.reset()
//	return md.digest(this.encodeUTF8()).encodeHex()
//}

fun String.digestSHA256Hex(): String {
    return this.encodeUTF8().digestSHA256().encodeHex()
}

fun String.digestSHA256Base64Url(): String {
    return this.encodeUTF8().digestSHA256().encodeBase64Url()
}

// Uri.encode(s:Nullable) だと nullチェックができないので、簡単なラッパーを用意する
fun String.encodePercent(allow: String? = null): String = Uri.encode(this, allow)

// replace + to %20, then decode it.
fun String.decodePercent(): String =
    Uri.decode(replace("+", "%20"))

////////////////////////////////////////////////////////////////////
// Bundle

//fun Bundle.parseString(key : String) : String? {
//	return try {
//		this.getString(key)
//	} catch(ignored : Throwable) {
//		null
//	}
//}

////////////////////////////////////////////////////////////////
// Pattern

fun Matcher.groupEx(g: Int): String? =
    try {
        group(g)
    } catch (ignored: Throwable) {
        null
    }

// make Array<String> to HashSet<String>
fun <T> Array<T>.toHashSet() = HashSet<T>().also { it.addAll(this) }
//fun <T> Collection<T>.toHashSet() = HashSet<T>().also { it.addAll(this) }
//fun <T> Iterable<T>.toHashSet() = HashSet<T>().also { it.addAll(this) }
//fun <T> Sequence<T>.toHashSet() = HashSet<T>().also { it.addAll(this) }

fun defaultLocale(context: Context): Locale =
    if (Build.VERSION.SDK_INT >= 24) {
        context.resources.configuration.locales[0]
    } else {
        @Suppress("DEPRECATION")
        context.resources.configuration.locale
    }

fun Matcher.findOrNull() = if (find()) this else null

fun String.formatEx(vararg args: Any?): String =
    java.lang.String.format(Locale.JAPANESE, this, *args)

fun Float.toString(format: String): String =
    java.lang.String.format(Locale.JAPANESE, format, this)
