package codes

import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import content.Category
import content.Language
import content.NetworkInformation
import content.ScheduleDay
import content.VoiceType
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.tinylog.Logger
import oshi.SystemInfo
import oshi.hardware.CentralProcessor
import oshi.hardware.GlobalMemory
import oshi.hardware.NetworkIF
import java.nio.file.Files
import java.nio.file.Path
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
import java.util.function.Consumer
import kotlin.io.path.name


@Suppress("unused")
class Somecodes {
    companion object {
        val current_directory : String = System.getProperty("user.dir")
        var Soundbank_directory : Path = Path.of(current_directory,"Soundbank")

        val SoundbankResult_directory : Path = Path.of(current_directory,"SoundbankResult")
        val PagingResult_directory : Path = Path.of(current_directory,"PagingResult")

        val si = SystemInfo()
        val processor: CentralProcessor = si.hardware.processor
        val memory : GlobalMemory = si.hardware.memory
        val NetworkInfoMap = mutableMapOf<String, NetworkInformation>()

        val datetimeformat1: DateTimeFormatter = DateTimeFormatter.ofPattern("dd/MM/yyyy HH:mm:ss")
        val dateformat1: DateTimeFormatter = DateTimeFormatter.ofPattern("dd/MM/yyyy")
        val dateformat2: DateTimeFormatter = DateTimeFormatter.ofPattern("dd-MM-yyyy")
        val timeformat1: DateTimeFormatter = DateTimeFormatter.ofPattern("hh:mm:ss")
        val timeformat2: DateTimeFormatter = DateTimeFormatter.ofPattern("hh:mm")
        val filenameformat: DateTimeFormatter = DateTimeFormatter.ofPattern("ddMMyyyy_HHmmss")
        const val KB_threshold = 1024.0
        const val MB_threshold = KB_threshold * 1024.0
        const val GB_threshold = MB_threshold * 1024.0
        const val TB_threshold = GB_threshold * 1024.0
        val objectmapper = jacksonObjectMapper()

        // regex for getting ann_id from Message, which is the number inside []
        private val ann_id_regex = Regex("\\[(\\d+)]")

        /**
         * Get the directory path for a specific language, voice type, and category.
         * @param language The language.
         * @param voice The voice type.
         * @param category The category.
         * @return The path to the directory.
         */
        fun SoundbankDirectory(language: Language, voice: VoiceType, category: Category) : Path{
            return Soundbank_directory.resolve(language.name).resolve(voice.name).resolve(category.name)
        }

        fun SoundbankDirectory(language: String, voice: String, category: String) : Path{
            return SoundbankDirectory(
                Language.valueOf(language),
                VoiceType.valueOf(voice),
                Category.valueOf(category)
            )
        }

        fun ExtractFilesFromClassPath(resourcePath: String, outputDir: Path)  {
            try {
                val resource = Somecodes::class.java.getResource(resourcePath)
                if (resource != null) {
                    val uri = resource.toURI()
                    val path = if (uri.scheme == "jar") {
                        val fileSystem = java.nio.file.FileSystems.newFileSystem(uri, emptyMap<String, Any>())
                        fileSystem.getPath(resourcePath)
                    } else {
                        Path.of(uri)
                    }

                    Files.walk(path).use { stream ->
                        stream.forEach { sourcePath ->
                            if (Files.isRegularFile(sourcePath)) {
                                val fn = sourcePath.fileName
                                val targetPath = outputDir.resolve(fn)
                                if (Files.isRegularFile(targetPath) && Files.size(targetPath) > 0) {
                                    Logger.info { "File $targetPath already exists, skipping extraction."  }
                                    return@forEach
                                }
                                Files.copy(sourcePath, targetPath)
                                Logger.info { "Extracted ${sourcePath.name} to $targetPath" }
                                return@forEach
                            }
                        }
                    }
                } else Logger.error { "Resource $resource not found" }
            } catch (e: Exception) {
                Logger.error { "Exception while extracting $resourcePath, Message = ${e.message}"}
            }
        }

        /**
         * Check if a string is a valid number.
         */
        fun IsNumber(value: String) : Boolean {
            return value.toIntOrNull() != null
        }

        /**
         * Check if a string is alphabetic (contains only letters).
         */
        fun IsAlphabethic(value: String) : Boolean {
            return value.all { it.isLetter() }
        }

        /**
         * Extract ANN ID from a message string.
         * The ANN ID is expected to be a number enclosed in square brackets (e.g., "[123]").
         * @param message The message string to extract the ANN ID from.
         * @return The extracted ANN ID as an integer, or -1 if not found or invalid.
         */
        fun Get_ANN_ID(message: String) : Int {
            val matchResult = ann_id_regex.find(message)
            return matchResult?.groups?.get(1)?.value?.toInt() ?: -1
        }

        /**
         * Convert an object to a JSON string.
         * @param data The object to convert.
         * @return A JSON string representation of the object, or "{}" if conversion fails.
         */
        fun toJsonString(data: Any) : String {
            return try {
                objectmapper.writeValueAsString(data)
            } catch (e: Exception){
                "{}"
            }
        }

        /**
         * Convert a JSON string to an object of the specified class.
         * @param json The JSON string to convert.
         * @param clazz The class of the object to convert to.
         * @return An object of the specified class, or null if conversion fails.
         */
        fun <T> fromJsonString(json: String, clazz: Class<T>) : T? {
            return try {
                objectmapper.readValue(json, clazz)
            } catch (e: Exception){
                null
            }
        }

        /**
         * Convert a JSON string to a JsonNode.
         * @param data The JSON string to convert.
         * @return A JsonNode representation of the JSON string, or empty JsonNode if conversion fails.
         */
        fun toJsonNode(data: String) : JsonNode {
            return try {
                objectmapper.readTree(data)
            } catch (e: Exception){
                objectmapper.createObjectNode()
            }
        }

        /**
         * List all audio files (.mp3 and .wav) in the specified directory and its subdirectories.
         * Only files larger than 1KB are included.
         * @param p The directory Path to search in
         * @return A list of absolute paths to the audio files found.
         */
        fun ListAudioFiles(p: Path) : List<String>{
            return try{
                // find all files that ends with .mp3 or .wav
                // and find them recursively
                if (Files.exists(p) && Files.isDirectory(p)){
                    Files.walk(p)
                        // cari file regular saja
                        .filter { Files.isRegularFile(it)}
                        // size lebih dari 1KB
                        .filter { Files.size(it) > 1024}
                        // extension .mp3 atau .wav
                        .filter { it.name.endsWith(".mp3",true) || it.name.endsWith(".wav",true) }
                        .map { it.toAbsolutePath().toString() }
                        .toList()
                } else throw Exception()

            } catch (_ : Exception){
                emptyList()
            }
        }

        /**
         * Converts a size in bytes to a human-readable format.
         * @param size Size in bytes.
         * @return A string representing the size in a human-readable format.
         */
        fun SizetoHuman(size: Long): String {
            return when {
                size < KB_threshold -> "${size}B"
                size < MB_threshold -> String.format("%.2f KB", size / KB_threshold)
                size < GB_threshold -> String.format("%.2f MB", size / MB_threshold)
                size < TB_threshold -> String.format("%.2f GB", size / GB_threshold)
                else -> String.format("%.2f TB", size / TB_threshold)
            }
        }

        /**
         * Get Disk usage using OSHI library.
         * @param path The path to check disk usage, defaults to the current working directory.
         * @return A string representing the disk usage of the file system in a human-readable format.
         */
        fun getDiskUsage(path: String = current_directory) : String {
            return try{
                val p = Path.of(path).toFile()
                if (p.exists() && p.isDirectory){
                    val total = p.totalSpace
                    val free = p.freeSpace
                    val used = total - free
                    String.format("Total: %s, Used: %s, Free: %s, Usage: %.2f%%",
                        SizetoHuman(total),
                        SizetoHuman(used),
                        SizetoHuman(free),
                        (used.toDouble() / total * 100)
                    )
                } else throw Exception()

            } catch (_ : Exception){
                "N/A"
            }
        }

        /**
         * Get CPU usage using OSHI library.
         * @param cb A callback function that receives the CPU usage as a string.
         */
        fun getCPUUsage(cb : Consumer<String>){
            CoroutineScope(Dispatchers.Default).launch {
                val prev = processor.systemCpuLoadTicks
                delay(1000)
                val current = processor.systemCpuLoadTicks

                fun delta(t: CentralProcessor.TickType) = current[t.index] - prev[t.index]

                val idle = delta(CentralProcessor.TickType.IDLE) + delta(CentralProcessor.TickType.IOWAIT)
                val busy =  delta(CentralProcessor.TickType.USER) + delta(CentralProcessor.TickType.SYSTEM) +
                            delta(CentralProcessor.TickType.NICE) + delta(CentralProcessor.TickType.IRQ) +
                            delta(CentralProcessor.TickType.SOFTIRQ)+ delta(CentralProcessor.TickType.STEAL)

                val total = idle + busy
                val usage = if (total > 0) {
                    (busy.toDouble() / total) * 100
                } else {
                    0.0
                }
                cb.accept(String.format("%.2f%%", usage))
            }

        }

        /**
         * Get RAM usage using OSHI library.
         * @return A string representing the total, used, and available memory in a human-readable format.
         */
        fun getMemoryUsage() : String{
            val totalMemory = memory.total
            val availableMemory = memory.available
            val usedMemory = totalMemory - availableMemory
            return String.format("Total: %s, Used: %s, Available: %s, Usage: %.2f%%",
                SizetoHuman(totalMemory),
                SizetoHuman(usedMemory),
                SizetoHuman(availableMemory)
            , (usedMemory.toDouble() / totalMemory * 100))
        }

        fun GetNetworkStatus(cb : Consumer<List<NetworkInformation>>) {
            val networks: List<NetworkIF> = si.hardware.networkIFs.toList()
            networks.forEach { net ->

                if (net.ifOperStatus==NetworkIF.IfOperStatus.UP){
                    if (net.iPv4addr.size>0 || net.iPv6addr.size>0){
                        var ni = NetworkInfoMap[net.name]
                        if (ni == null){
                            ni = NetworkInformation(net.name, net.displayName, net.macaddr)
                            NetworkInfoMap[net.name] = ni
                        }
                        ni.ipV4addr = net.iPv4addr.toMutableList()
                        ni.ipV6addr = net.iPv6addr.toMutableList()
                        ni.speed = net.speed
                        ni.packetsSent = net.packetsSent
                        ni.packetsRecv = net.packetsRecv
                        if (ni.updateStamp==0L){
                            ni.bytesSent = net.bytesSent
                            ni.bytesRecv = net.bytesRecv
                            ni.txSpeed = 0
                            ni.rxSpeed = 0
                            ni.updateStamp = System.currentTimeMillis()
                        } else {
                            // tx speed = (current bytesSent - previous bytesSent) / (current time - previous time) * 1000
                            val currentTime = System.currentTimeMillis()
                            ni.txSpeed = ((net.bytesSent - ni.bytesSent) * 1000 / (currentTime - ni.updateStamp))
                            ni.rxSpeed = ((net.bytesRecv - ni.bytesRecv) * 1000 / (currentTime - ni.updateStamp))
                            ni.bytesSent = net.bytesSent
                            ni.bytesRecv = net.bytesRecv
                            ni.updateStamp = currentTime
                        }
                    } else if (NetworkInfoMap.contains(net.name)) NetworkInfoMap.remove(net.name)
                } else if (NetworkInfoMap.contains(net.name)) NetworkInfoMap.remove(net.name)


            }
            cb.accept(NetworkInfoMap.values.toList())
        }

        /**
         * Check if a value is a valid non-blank string.
         * @param value The value to check.
         * @return True if the value is a non-blank string, false otherwise.
         */
        fun ValidString(value: Any) : Boolean {
            return value is String && value.isNotBlank()
        }

        /**
         * Check if all strings in a list are valid non-blank strings.
         * @param values The list of strings to check.
         * @return True if all strings in the list are valid non-blank strings, false otherwise.
         */
        fun ValidStrings(values: List<String>) : Boolean{
            if (values.isNotEmpty()){
                for (v in values){
                    if (!ValidString(v)){
                        return false
                    }
                }
                return true
            }
            return false
        }

        /**
         * Check if a string is a valid file path and the file exists.
         * @param value The string to check.
         * @return True if the string is a valid file path, false otherwise.
         */
        fun ValidFile(value : String) : Boolean {
            if (value.isNotBlank()){
                return Files.exists(Path.of(value))
            }
            return false
        }

        /**
         * Check if a string is a valid date in the format "dd/MM/yyyy".
         * @param value The string to check.
         * @return True if the string is a valid date, false otherwise.
         */
        fun ValidDate(value: String): Boolean{
            return try{
                if (ValidString(value)){
                    dateformat1.parse(value)
                    true
                } else throw Exception()

            } catch (_: Exception){
                false
            }
        }

        /**
         * Check if a string is a valid IPv4 address.
         * @param value The string to check.
         * @return True if the string is a valid IPv4 address, false otherwise.
         */
        fun ValidIPV4(value: String): Boolean{
            return try{
                if (ValidString(value)){
                    val parts = value.split(".")
                    if (parts.size != 4) return false
                    for (part in parts){
                        val num = part.toInt()
                        if (num !in 0..255) return false
                    }
                    true
                } else throw Exception()

            } catch (_: Exception){
                false
            }
        }

        /**
         * Check if a string is a valid date in the format "dd-MM-yyyy".
         * This format is used for log HTML files.
         * @param value The string to check.
         * @return True if the string is a valid date, false otherwise.
         */
        fun ValiDateForLogHtml(value: String): Boolean{
            return try{
                if (ValidString(value)){
                    dateformat2.parse(value)
                    true
                } else throw Exception()

            } catch (_: Exception){
                false
            }
        }

        /**
         * Check if a string is a valid time in the format "hh:mm:ss".
         * @param value The string to check.
         * @return True if the string is a valid time, false otherwise.
         */
        fun ValidTime(value: String): Boolean{
            return try{
                if (ValidString(value)){
                    timeformat1.parse(value)
                    true
                } else throw Exception()

            } catch (_: Exception){
                false
            }
        }

        /**
         * Check if a string is a valid schedule time in the format "HH:mm".
         * @param value The string to check.
         * @return True if the string is a valid schedule time, false otherwise.
         */
        fun ValidScheduleTime(value: String): Boolean{
            // format HH:mm
            try {
                if (ValidString(value)){
                    timeformat2.parse(value)
                    return true
                }
            } catch (_ : Exception){

            }
            return false
        }

        /**
         * Find a schedule day by its name.
         * @param value The name of the schedule day to find.
         * @return The name of the schedule day if found, null otherwise.
         */
        fun FindScheduleDay(value: String) : String? {
            val sd = ScheduleDay.entries.find { sd -> sd.name == value }
            return sd?.name
        }

        /**
         * Check if a string is a valid schedule day or a valid date.
         * A valid schedule day is either one of the ScheduleDay enum names or a date in the format "dd/MM/yyyy".
         * @param value The string to check.
         * @return True if the string is a valid schedule day or date, false otherwise.
         */
        fun ValidScheduleDay(value: String) : Boolean {
            if (ValidString(value)){
                // check if value is one of ScheduleDay enum name
                if (FindScheduleDay(value) != null){
                    return true
                }
                // check if value is in format dd/MM/yyyy
                return ValidDate(value)
            }
            return false
        }

        /**
         * Generate a WAV file name with the current date and time.
         * The file name format is: [prefix]_ddMMyyyy_HHmmss_[postfix].wav
         * @param prefix An optional prefix to add before the date and time.
         * @param postfix An optional postfix to add after the date and time.
         * @return A string representing the generated WAV file name.
         */
        fun Make_WAV_FileName(prefix: String, postfix: String) : String{
            val sb = StringBuilder()
            if (prefix.isNotEmpty()){sb.append(prefix).append("_")}
            sb.append(filenameformat.format(LocalDateTime.now()))
            if (postfix.isNotEmpty()){sb.append("_").append(postfix)}
            sb.append(".wav")
            return sb.toString()
        }
    }


}