package com.deniscerri.ytdl.util

import android.annotation.SuppressLint
import android.content.Context
import android.content.SharedPreferences
import android.content.res.Resources
import android.os.Handler
import android.os.Looper
import android.text.Html
import android.util.Log
import android.util.Patterns
import android.widget.Toast
import androidx.preference.PreferenceManager
import com.afollestad.materialdialogs.utils.MDUtil.getStringArray
import com.anggrayudi.storage.extension.count
import com.deniscerri.ytdl.R
import com.deniscerri.ytdl.database.models.ChapterItem
import com.deniscerri.ytdl.database.models.DownloadItem
import com.deniscerri.ytdl.database.models.Format
import com.deniscerri.ytdl.database.models.ResultItem
import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdl.util.Extensions.getIntByAny
import com.deniscerri.ytdl.util.Extensions.getStringByAny
import com.deniscerri.ytdl.util.Extensions.isYoutubeURL
import com.deniscerri.ytdl.util.Extensions.toStringDuration
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import com.yausername.youtubedl_android.YoutubeDL
import com.yausername.youtubedl_android.YoutubeDLRequest
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.withContext
import org.json.JSONArray
import org.json.JSONException
import org.json.JSONObject
import java.io.BufferedReader
import java.io.File
import java.io.InputStreamReader
import java.lang.reflect.Type
import java.net.HttpURLConnection
import java.net.URL
import java.util.*
import java.util.regex.Pattern


class InfoUtil(private val context: Context) {
    private lateinit var sharedPreferences: SharedPreferences
    private lateinit var handler: Handler

    init {
        try {
            handler = Handler(Looper.getMainLooper())
            sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
            countryCODE = sharedPreferences.getString("locale", "")!!
            if (countryCODE.isEmpty()) countryCODE = "US"
        } catch (e: Exception) {
            e.printStackTrace()
        }
    }


    fun search(query: String): ArrayList<ResultItem> {
        return runCatching {
            when(sharedPreferences.getString("search_engine", "ytsearch")){
                "ytsearch" -> searchFromPiped(query)
                "ytsearchmusic" -> searchFromPipedMusic(query)
                else -> throw Exception()
            }
        }.getOrElse {
            getFromYTDL(query)
        }
    }

    @Throws(JSONException::class)
    fun searchFromPiped(query: String): ArrayList<ResultItem> {
        val items = arrayListOf<ResultItem>()
        val data = genericRequest("$pipedURL/search?q=$query&filter=videos&region=${countryCODE}")
        val dataArray = data.getJSONArray("items")
        if (dataArray.length() == 0) return getFromYTDL(query)
        for (i in 0 until dataArray.length()) {
            val element = dataArray.getJSONObject(i)
            if (element.getInt("duration") == -1) continue
            element.put("uploader", element.getString("uploaderName"))
            val v = createVideoFromPipedJSON(element, "https://youtube.com" + element.getString("url"))
            if (v == null || v.thumb.isEmpty()) {
                continue
            }
            items.add(v)
        }
        return items
    }

    @Throws(JSONException::class)
    fun searchFromPipedMusic(query: String): ArrayList<ResultItem> {
        val items = arrayListOf<ResultItem>()
        val data = genericRequest("$pipedURL/search?q=$query=&filter=music_songs&region=${countryCODE}")
        val dataArray = data.getJSONArray("items")
        if (dataArray.length() == 0) return getFromYTDL(query)
        for (i in 0 until dataArray.length()) {
            val element = dataArray.getJSONObject(i)
            if (element.getInt("duration") == -1) continue
            element.put("uploader", element.getString("uploaderName"))
            val v = createVideoFromPipedJSON(element, "https://youtube.com" + element.getString("url"))
            if (v == null || v.thumb.isEmpty()) {
                continue
            }
            items.add(v)
        }
        return items
    }

    @Throws(JSONException::class)
    fun getPlaylist(id: String, nextPageToken: String, playlistName: String): PlaylistTuple {
        try{
            val items = arrayListOf<ResultItem>()
            // -------------- PIPED API FUNCTION -------------------
            var url = ""
            url = if (nextPageToken.isBlank()) "$pipedURL/playlists/$id"
            else """$pipedURL/nextpage/playlists/$id?nextpage=${nextPageToken.replace("&prettyPrint", "%26prettyPrint")}"""

            val res = genericRequest(url)
            if (!res.has("relatedStreams")) throw Exception()

            val dataArray = res.getJSONArray("relatedStreams")
            var nextpage = res.getString("nextpage")
            for (i in 0 until dataArray.length()){
                kotlin.runCatching {
                    val obj = dataArray.getJSONObject(i)
                    createVideoFromPipedJSON(obj, "https://youtube.com" + obj.getString("url"))?.apply {
                        playlistTitle = runCatching { res.getString("name") }.getOrElse { playlistName }
                        playlistURL = "https://www.youtube.com/playlist?list=$id"
                        items.add(this)
                    }
                }
            }
            if (nextpage == "null") nextpage = ""
            return PlaylistTuple(nextpage, items)
        }catch (e: Exception){
            Log.e("PLST", e.toString())
            return PlaylistTuple(
                "",
                getFromYTDL("https://www.youtube.com/playlist?list=$id")
            )
        }
    }

    @Throws(JSONException::class)
    fun getYoutubeVideo(url: String): List<ResultItem>? {
        val theURL = url.replace("\\?list.*".toRegex(), "")
        try{
            val id = getIDFromYoutubeURL(theURL)
            val res = genericRequest("$pipedURL/streams/$id")
            if (res.length() == 0) {
                return getFromYTDL(theURL)
            }

            createVideoFromPipedJSON(res, theURL)?.apply {
                return listOf(this)
            }
        }catch (e: Exception){
            val v = getFromYTDL(theURL)
            v.forEach { it.url = theURL }
            return v
        }

        return null
    }

    private fun createVideofromJSON(obj: JSONObject): ResultItem? {
        var video: ResultItem? = null
        try {
            val id = obj.getString("videoID")
            val title = obj.getString("title").toString()
            val author = obj.getString("channelTitle").toString()
            val duration = obj.getString("duration")
            val thumb = obj.getString("thumb")
            val url = "https://www.youtube.com/watch?v=$id"
            video = ResultItem(0,
                    url,
                    title,
                    author,
                    duration,
                    thumb,
                    "youtube",
                    "",
                    ArrayList(),
                "",
                ArrayList()
                )
        } catch (e: Exception) {
            Log.e(TAG, e.toString())
        }
        return video
    }
    private fun createVideoFromPipedJSON(obj: JSONObject, url: String, ignoreFormatPreference : Boolean = false): ResultItem? {
        var video: ResultItem? = null
        try {
            val id = getIDFromYoutubeURL(url)
            val title = Html.fromHtml(obj.getString("title").toString()).toString()
            val author = try {
                 Html.fromHtml(obj.getString("uploader").toString()).toString()
            }catch (e: Exception){
                Html.fromHtml(obj.getString("uploaderName").toString()).toString()
            }

            val duration = obj.getInt("duration").toStringDuration(Locale.US)
            val thumb = "https://i.ytimg.com/vi/$id/hqdefault.jpg"
            val formats : ArrayList<Format> = ArrayList()

            if(sharedPreferences.getString("formats_source", "yt-dlp") == "piped" || ignoreFormatPreference){
                if (obj.has("audioStreams")){
                    val formatsInJSON = obj.getJSONArray("audioStreams")
                    for (f in 0 until formatsInJSON.length()){
                        val format = formatsInJSON.getJSONObject(f)
                        if (format.getInt("bitrate") == 0) continue
                        val formatObj = Gson().fromJson(format.toString(), Format::class.java)
                        try{
                            formatObj.acodec = format.getString("codec")
                            formatObj.asr = format.getString("quality")
                            if (! format.getString("audioTrackName").equals("null", ignoreCase = true)){
                                formatObj.format_note = format.getString("audioTrackName") + " Audio, " + formatObj.format_note
                            }else{
                                formatObj.format_note = formatObj.format_note + " Audio"
                            }
                            if (!formatObj.tbr.isNullOrBlank()){
                                formatObj.tbr = (formatObj.tbr!!.toInt() / 1000).toString() + "k"
                            }

                        }catch (e: Exception) {
                            e.printStackTrace()
                        }
                        formats.add(formatObj)
                    }

                }

                if (obj.has("videoStreams")){
                    val formatsInJSON = obj.getJSONArray("videoStreams")
                    for (f in 0 until formatsInJSON.length()){
                        val format = formatsInJSON.getJSONObject(f)
                        if (format.getInt("bitrate") == 0) continue
                        val formatObj = Gson().fromJson(format.toString(), Format::class.java)
                        try{
                            formatObj.vcodec = format.getString("codec")
                            if (!formatObj.tbr.isNullOrBlank()){
                                formatObj.tbr = (formatObj.tbr!!.toInt() / 1000).toString() + "k"
                            }
                        }catch (e: Exception) {
                            e.printStackTrace()
                        }
                        formats.add(formatObj)
                    }

                }
                formats.groupBy { it.format_id }.forEach {
                    if (it.value.count() > 1) {
                        it.value.filter { f-> !f.format_note.contains("original", true) }.forEachIndexed { index, format -> format.format_id = format.format_id.split("-")[0] + "-${index}" }
                        val defaultLang = it.value.find { f -> f.format_note.contains("original", true) }
                        defaultLang?.format_id = (defaultLang?.format_id?.split("-")?.get(0) ?: "") + "-${it.value.size-1}"
                    }
                }
                formats.sortByDescending { it.filesize }
            }

            val chapters = ArrayList<ChapterItem>()
            if (obj.has("chapters") && obj.getJSONArray("chapters").length() > 0){
                val chaptersJArray = obj.getJSONArray("chapters")
                for (c in 0 until chaptersJArray.length()){
                    val chapter = chaptersJArray.getJSONObject(c)
                    val end = if (c == chaptersJArray.length() - 1) obj.getInt("duration") else chaptersJArray.getJSONObject(c+1).getInt("start")
                    val item = ChapterItem(chapter.getInt("start").toLong(), end.toLong(), chapter.getString("title"))
                    chapters.add(item)
                }
            }

            video = ResultItem(0,
                url,
                title,
                author,
                duration,
                thumb,
                "youtube",
                "",
                formats,
                if (obj.has("hls") && obj.getString("hls") != "null") obj.getString("hls") else "",
                chapters
            )
        } catch (e: Exception) {
            Log.e(TAG, e.toString())
        }
        return video
    }

    private fun genericRequest(url: String): JSONObject {
        Log.e(TAG, url)
        val reader: BufferedReader
        var line: String?
        val responseContent = StringBuilder()
        val conn: HttpURLConnection
        var json = JSONObject()
        try {
            val req = URL(url)
            conn = req.openConnection() as HttpURLConnection
            conn.requestMethod = "GET"
            conn.connectTimeout = 3000
            conn.readTimeout = 5000
            if (conn.responseCode < 300) {
                reader = BufferedReader(InputStreamReader(conn.inputStream))
                while (reader.readLine().also { line = it } != null) {
                    responseContent.append(line)
                }
                reader.close()
                json = JSONObject(responseContent.toString())
                if (json.has("error")) {
                    throw Exception()
                }
            }
            conn.disconnect()
        } catch (e: Exception) {
            Log.e(TAG, e.toString())
        }
        return json
    }

    private fun genericArrayRequest(url: String): JSONArray {
        Log.e(TAG, url)
        val reader: BufferedReader
        var line: String?
        val responseContent = StringBuilder()
        val conn: HttpURLConnection
        var json = JSONArray()
        try {
            val req = URL(url)
            conn = req.openConnection() as HttpURLConnection
            conn.requestMethod = "GET"
            conn.connectTimeout = 3000
            conn.readTimeout = 5000
            if (conn.responseCode < 300) {
                reader = BufferedReader(InputStreamReader(conn.inputStream))
                while (reader.readLine().also { line = it } != null) {
                    responseContent.append(line)
                }
                reader.close()
                json = JSONArray(responseContent.toString())
            }
            conn.disconnect()
        } catch (e: Exception) {
            Log.e(TAG, e.toString())
        }
        return json
    }

    private fun fixThumbnail(o: JSONObject): JSONObject {
        var imageURL = ""
        try {
            val thumbs = o.getJSONObject("thumbnails")
            imageURL = thumbs.getJSONObject("maxres").getString("url")
        } catch (e: Exception) {
            try {
                val thumbs = o.getJSONObject("thumbnails")
                imageURL = thumbs.getJSONObject("high").getString("url")
            } catch (u: Exception) {
                try {
                    val thumbs = o.getJSONObject("thumbnails")
                    imageURL = thumbs.getJSONObject("default").getString("url")
                } catch (ignored: Exception) {
                }
            }
        }
        try {
            o.put("thumb", imageURL)
        } catch (ignored: Exception) {
        }
        return o
    }

    @Throws(JSONException::class)
    fun getTrending(): ArrayList<ResultItem> {
        if (sharedPreferences.getString("api_key", "")!!.isNotBlank()){
            return getTrendingFromYoutubeAPI()
        }
        return getTrendingFromPiped()
    }

    @Throws(JSONException::class)
    fun getTrendingFromYoutubeAPI(): ArrayList<ResultItem> {
        val items = arrayListOf<ResultItem>()
        val key = sharedPreferences.getString("api_key", "")!!
        val url = "https://www.googleapis.com/youtube/v3/videos?part=snippet&chart=mostPopular&videoCategoryId=10&regionCode=$countryCODE&maxResults=25&key=$key"
        //short data
        val res = genericRequest(url)
        //extra data from the same videos
        val contentDetails =
            genericRequest("https://www.googleapis.com/youtube/v3/videos?part=contentDetails&chart=mostPopular&videoCategoryId=10&regionCode=$countryCODE&maxResults=25&key=$key")
        if (!contentDetails.has("items")) return ArrayList()
        val dataArray = res.getJSONArray("items")
        val extraDataArray = contentDetails.getJSONArray("items")
        for (i in 0 until dataArray.length()) {
            val element = dataArray.getJSONObject(i)
            val snippet = element.getJSONObject("snippet")
            var duration = extraDataArray.getJSONObject(i).getJSONObject("contentDetails")
                .getString("duration")
            duration = formatDuration(duration)
            snippet.put("videoID", element.getString("id"))
            snippet.put("duration", duration)
            fixThumbnail(snippet)
            val v = createVideofromJSON(snippet)
            if (v == null || v.thumb.isEmpty()) {
                continue
            }
            items.add(v)
        }
        return items
    }

    private fun getTrendingFromPiped(): ArrayList<ResultItem> {
        val items = arrayListOf<ResultItem>()
        val url = "$pipedURL/trending?region=$countryCODE"
        val res = genericArrayRequest(url)
        for (i in 0 until res.length()) {
            val element = res.getJSONObject(i)
            if (element.getInt("duration") < 0) continue
            element.put("uploader", element.getString("uploaderName"))
            val v = createVideoFromPipedJSON(element, "https://youtube.com" + element.getString("url"))
            if (v == null || v.thumb.isEmpty()) continue
            items.add(v)
        }
        return items
    }

    private fun getIDFromYoutubeURL(inputQuery: String) : String {
        var el: Array<String?> =
            inputQuery.split("/".toRegex()).dropLastWhile { it.isEmpty() }
                .toTypedArray()
        var query = el[el.size - 1]
        if (query!!.contains("watch?v=")) {
            query = query.substring(8)
        }
        el = query.split("&".toRegex()).dropLastWhile { it.isEmpty() }
            .toTypedArray()
        query = el[0]
        el = query!!.split("\\?".toRegex()).dropLastWhile { it.isEmpty() }
            .toTypedArray()
        query = el[0]
        return query!!
    }

    fun getFormats(url: String, source : String? = null) : List<Format> {
        val formatSource = source ?: sharedPreferences.getString("formats_source", "yt-dlp")
        if (url.isYoutubeURL() && formatSource == "piped"){
            try {
                val id = getIDFromYoutubeURL(url)
                val res = genericRequest("$pipedURL/streams/$id")
                if (res.length() == 0) {
                    return if (source != null) {
                        listOf()
                    }else {
                        getFormatsFromYTDL(url)
                    }
                }else {
                    val item = createVideoFromPipedJSON(res, "https://youtube.com/watch?v=$id", true)
                    return item!!.formats
                }

            }catch(e: Exception) {
                println(e)
                if (e is CancellationException) throw e
                return getFormatsFromYTDL(url)
            }
        }else{
            return getFormatsFromYTDL(url)
        }
    }

    private fun getFormatsFromYTDL(url: String) : List<Format> {
        val request = YoutubeDLRequest(url)
        request.addOption("--print", "%(formats)s")
        request.addOption("--print", "%(duration)s")
        request.addOption("--skip-download")
        request.addOption("-R", "1")
        request.addOption("--compat-options", "manifest-filesize-approx")

        if (sharedPreferences.getBoolean("force_ipv4", false)){
            request.addOption("-4")
        }

        if (sharedPreferences.getBoolean("use_cookies", false)){
            FileUtil.getCookieFile(context){
                request.addOption("--cookies", it)
            }

            val useHeader = sharedPreferences.getBoolean("use_header", false)
            val header = sharedPreferences.getString("useragent_header", "")
            if (useHeader && !header.isNullOrBlank()){
                request.addOption("--add-header","User-Agent:${header}")
            }
        }

        val proxy = sharedPreferences.getString("proxy", "")
        if (proxy!!.isNotBlank()) {
            request.addOption("--proxy", proxy)
        }
        request.addOption("-P", FileUtil.getCachePath(context) + "/tmp")



        val res = YoutubeDL.getInstance().execute(request)
        val results: Array<String?> = try {
            res.out.split(System.lineSeparator()).toTypedArray()
        } catch (e: Exception) {
            arrayOf(res.out)
        }
        val json = results[0]
        val jsonArray = kotlin.runCatching { JSONArray(json) }.getOrElse { JSONArray() }

        return parseYTDLFormats(jsonArray)
    }

    data class MultipleFormatProgress(
        val url: String,
        val formats: List<Format>,
        val unavailable : Boolean = false,
        val unavailableMessage: String = ""
    )

    suspend fun getFormatsMultiple(urls: List<String>, source: String? = null, progress: (progress: MultipleFormatProgress) -> Unit) : MutableList<MutableList<Format>> {
        val formatCollection = mutableListOf<MutableList<Format>>()

        val formatSource = source ?: sharedPreferences.getString("formats_source", "yt-dlp")
        val allYoutubeLinks = urls.all { it.isYoutubeURL() }
        if (allYoutubeLinks && formatSource == "piped") {
            urls.forEach { url ->
                val id = getIDFromYoutubeURL(url)
                val res = genericRequest("$pipedURL/streams/$id")
                createVideoFromPipedJSON(res, url).apply {
                    formatCollection.add(this!!.formats)
                    progress(
                        MultipleFormatProgress(url, this.formats)
                    )
                }
            }
        }
        else {
            val urlsFile = File(context.cacheDir, "urls.txt")
            urlsFile.delete()
            withContext(Dispatchers.IO) {
                urlsFile.createNewFile()
            }
            urls.forEach {
                urlsFile.appendText(it+"\n")
            }

            try {
                val request = YoutubeDLRequest(emptyList())
                request.addOption("--print", "formats")
                request.addOption("-a", urlsFile.absolutePath)
                request.addOption("--skip-download")
                request.addOption("-R", "1")
                request.addOption("--socket-timeout", "5")

                if (sharedPreferences.getBoolean("force_ipv4", false)){
                    request.addOption("-4")
                }

                if (sharedPreferences.getBoolean("use_cookies", false)){
                    FileUtil.getCookieFile(context){
                        request.addOption("--cookies", it)
                    }

                    val useHeader = sharedPreferences.getBoolean("use_header", false)
                    val header = sharedPreferences.getString("useragent_header", "")
                    if (useHeader && !header.isNullOrBlank()){
                        request.addOption("--add-header","User-Agent:${header}")
                    }
                }


                val proxy = sharedPreferences.getString("proxy", "")
                if (proxy!!.isNotBlank()){
                    request.addOption("--proxy", proxy)
                }
                request.addOption("-P", FileUtil.getCachePath(context) + "/tmp")

                val txt = parseYTDLRequestString(request)
                println(txt)

                var urlIdx = 0
                YoutubeDL.getInstance().execute(request){ progress, _, line ->
                    try{
                        if (line.isNotBlank()){
                            val url = urls[urlIdx]
                            println(line)
                            println(url)

                            if (line.contains("unavailable")) {
                                progress(MultipleFormatProgress(url, listOf(), true, line))
                            }else{
                                val formatsJSON = JSONArray(line)
                                val formats = parseYTDLFormats(formatsJSON)

                                formatCollection.add(formats)
                                progress(MultipleFormatProgress(url, formats))
                            }
                        }

                    }catch (e: Exception){
                        Log.e("GET MULTIPLE FORMATS", e.toString())
                    }
                    urlIdx++
                }
            } catch (e: Exception) {
                e.message?.split(System.lineSeparator())?.apply {
                    this.forEach { line ->
                        println(line)
                        if (line.contains("unavailable")) {
                            kotlin.runCatching {
                                val id = Regex("""\[.*?\] (\w+):""").find(line)!!.groupValues[1]
                                val url = urls.first { it.contains(id) }
                                progress(MultipleFormatProgress(url, listOf(), true, line))
                                delay(500)
                            }
                        }

                    }
                }

                handler.post {
                    Toast.makeText(context, e.message, Toast.LENGTH_LONG).show()
                }
                e.printStackTrace()
            }

            urlsFile.delete()
        }

        return formatCollection
    }

    @SuppressLint("RestrictedApi")
    fun getFromYTDL(query: String, singleItem: Boolean = false): ArrayList<ResultItem> {
        val items = arrayListOf<ResultItem>()
        val searchEngine = sharedPreferences.getString("search_engine", "ytsearch")

        val request : YoutubeDLRequest
        if (query.contains("http")){
            request = YoutubeDLRequest(query)
        }else{
            request = YoutubeDLRequest(emptyList())
            when (searchEngine){
                "ytsearchmusic" -> {
                    request.addOption("--default-search", "https://music.youtube.com/search?q=")
                    request.addOption("ytsearch25:\"${query}\"")
                }
                else -> {
                    request.addOption("${searchEngine}25:\"${query}\"")
                }
            }
        }
        val lang = sharedPreferences.getString("app_language", "en")
        if (searchEngine == "ytsearch" && context.getStringArray(R.array.subtitle_langs).contains(lang)){
            request.addOption("--extractor-args", "youtube:lang=$lang")
        }

        request.addOption("--flat-playlist")
        request.addOption(if (singleItem) "-J" else "-j")
        request.addOption("--skip-download")
        request.addOption("-R", "1")
        request.addOption("--compat-options", "manifest-filesize-approx")
        request.addOption("--socket-timeout", "5")

        if (sharedPreferences.getBoolean("force_ipv4", false)){
            request.addOption("-4")
        }

        if (sharedPreferences.getBoolean("use_cookies", false)){
            FileUtil.getCookieFile(context){
                request.addOption("--cookies", it)
            }

            val useHeader = sharedPreferences.getBoolean("use_header", false)
            val header = sharedPreferences.getString("useragent_header", "")
            if (useHeader && !header.isNullOrBlank()){
                request.addOption("--add-header","User-Agent:${header}")
            }
        }

        val proxy = sharedPreferences.getString("proxy", "")
        if (proxy!!.isNotBlank()){
            request.addOption("--proxy", proxy)
        }
        request.addOption("-P", FileUtil.getCachePath(context) + "/tmp")



        val youtubeDLResponse = YoutubeDL.getInstance().execute(request)
        val results: List<String?> = try {
            val lineSeparator = System.getProperty("line.separator")
            youtubeDLResponse.out.split(lineSeparator!!)
        } catch (e: Exception) {
            listOf(youtubeDLResponse.out)
        }.filter { it.isNotBlank() }.apply {
            if (this.isEmpty()) throw Exception("Command Used: \n yt-dlp ${parseYTDLRequestString(request)}")
        }
        for (result in results) {
            if (result.isNullOrBlank()) continue
            val jsonObject = JSONObject(result)
            val title = jsonObject.getStringByAny("alt_title", "title", "webpage_url_basename")
            if (title == "[Private video]" || title == "[Deleted video]") continue

            var author = jsonObject.getStringByAny("uploader", "channel", "playlist_uploader", "uploader_id")
            var duration = jsonObject.getIntByAny("duration").toString()
            if (duration != "-1"){
                duration = jsonObject.getInt("duration").toStringDuration(Locale.US)
            }

            var thumb: String? = ""
            if (jsonObject.has("thumbnail")) {
                thumb = jsonObject.getString("thumbnail")
            } else if (jsonObject.has("thumbnails")) {
                val thumbs = jsonObject.getJSONArray("thumbnails")
                if (thumbs.length() > 0){
                    thumb = thumbs.getJSONObject(thumbs.length() - 1).getString("url")
                }
            }

            if(author.isEmpty()){
                runCatching {
                    val firstEntry = jsonObject.getJSONArray("entries").getJSONObject(0)
                    author = firstEntry.getStringByAny("uploader", "channel", "playlist_uploader", "uploader_id")
                    val thumbs = firstEntry.getJSONArray("thumbnails")
                    if (thumbs.length() > 0){
                        thumb = thumbs.getJSONObject(thumbs.length() - 1).getString("url")
                    }
                }
            }

            var website = jsonObject.getStringByAny("extractor_key", "extractor","ie_key")
            if (website == "Generic" || website == "HTML5MediaEmbed") website = jsonObject.getStringByAny("webpage_url_domain")
            var playlistTitle = jsonObject.getStringByAny("playlist_title")
            var playlistURL: String? = ""
            var playlistIndex: Int? = null

            if(playlistTitle.removeSurrounding("\"") == query) playlistTitle = ""

            if (playlistTitle.isNotBlank()){
                playlistURL = query
                kotlin.runCatching { playlistIndex = jsonObject.getInt("playlist_index") }
            }

            val formatsInJSON = if (jsonObject.has("formats") && jsonObject.get("formats") is JSONArray) jsonObject.getJSONArray("formats") else null
            val formats : ArrayList<Format> = parseYTDLFormats(formatsInJSON)

            val chaptersInJSON = if (jsonObject.has("chapters") && jsonObject.get("chapters") is JSONArray) jsonObject.getJSONArray("chapters") else null
            val listType: Type = object : TypeToken<List<ChapterItem>>() {}.type
            var chapters : ArrayList<ChapterItem> = arrayListOf()

            if (chaptersInJSON != null){
                chapters = Gson().fromJson(chaptersInJSON.toString(), listType)
            }

            var urls = "";
            if(jsonObject.has("requested_formats")) {
                val requestedFormats = jsonObject.getJSONArray("requested_formats")
                val urlList = mutableListOf<String>()
                val length = requestedFormats.length()-1
                for (i in length downTo 0) {
                    urlList.add(requestedFormats.getJSONObject(i).getString("url"))
                }

                urls = urlList.joinToString("\n")
            }

            val url = if (jsonObject.has("url") && results.size > 1){
                jsonObject.getString("url")
            }else{
                if (Patterns.WEB_URL.matcher(query).matches() && playlistURL?.isEmpty() == true){
                    query
                }else{
                    jsonObject.getStringByAny("webpage_url", "original_url", "url", )
                    jsonObject.getString("webpage_url")
                }
            }

            val type = jsonObject.getStringByAny("_type")
            if (type == "playlist" && playlistTitle.isEmpty()) {
                playlistTitle = title
            }

            val res = ResultItem(0,
                url,
                title,
                author,
                duration,
                thumb!!,
                website,
                playlistTitle,
                formats,
                urls,
                chapters,
                playlistURL,
                playlistIndex
            )

            items.add(res)
        }
        return items
    }

    private fun parseYTDLFormats(formatsInJSON: JSONArray?) : ArrayList<Format> {
        val formats = arrayListOf<Format>()

        if (formatsInJSON != null) {
            for (f in formatsInJSON.length() - 1 downTo 0){
                val format = formatsInJSON.getJSONObject(f)
                kotlin.runCatching {
                    if (format.get("filesize").toString() == "None") {
                        format.remove("filesize")
                    }
                }

                kotlin.runCatching {
                    if (format.get("filesize_approx").toString() == "None") {
                        format.remove("filesize_approx")
                    }
                }

                kotlin.runCatching {
                    if (!format.has("filesize")) {
                        format.put("filesize", 0)
                    }
                }

                kotlin.runCatching {
                    if(format.get("format_note").toString() == "null"){
                        format.remove("format_note")
                    }
                }

                val formatProper = Gson().fromJson(format.toString(), Format::class.java)
                if (formatProper.format_note == null) continue

                if (format.has("format_note")){
                    if (!formatProper!!.format_note.contains("audio only", true)) {
                        formatProper.format_note = format.getString("format_note")
                    }else{
                        if (!formatProper.format_note.endsWith("audio", true)){
                            formatProper.format_note = format.getString("format_note").uppercase().removeSuffix("AUDIO") + " AUDIO"
                        }
                    }
                }
                if (formatProper.format_note == "storyboard") continue
                formatProper.container = format.getString("ext")
                if (formatProper.tbr == "None") formatProper.tbr = ""
                if (!formatProper.tbr.isNullOrBlank()){
                    formatProper.tbr = formatProper.tbr + "k"
                }

                if(formatProper.vcodec.isNullOrEmpty() || formatProper.vcodec == "null"){
                    if(formatProper.acodec.isNullOrEmpty() || formatProper.acodec == "null"){
                        formatProper.vcodec = format.getStringByAny("video_ext", "ext").ifEmpty { "unknown" }
                    }
                }

                formats.add(formatProper)
            }
        }
        return formats
    }


    fun getSearchSuggestions(query: String): ArrayList<String> {
        val url = "https://suggestqueries.google.com/complete/search?client=youtube&ds=yt&client=firefox&q=$query"
        val res = genericArrayRequest(url)
        Log.e("aa", res.toString())
        if (res.length() == 0) return ArrayList()
        val suggestionList = ArrayList<String>()
        try {
            for (i in 0 until res.getJSONArray(1).length()) {
                val item = res.getJSONArray(1).getString(i)
                suggestionList.add(item)
            }
        } catch (ignored: Exception) {
            ignored.printStackTrace()
        }
        return suggestionList
    }

    private fun formatDuration(dur: String): String {
        var badDur = dur
        if (dur == "P0D") {
            return "LIVE"
        }
        var hours = false
        var duration = ""
        badDur = badDur.substring(2)
        if (badDur.contains("H")) {
            hours = true
            duration += String.format(
                Locale.getDefault(),
                "%02d",
                badDur.substring(0, badDur.indexOf("H")).toInt()
            ) + ":"
            badDur = badDur.substring(badDur.indexOf("H") + 1)
        }
        if (badDur.contains("M")) {
            duration += String.format(
                Locale.getDefault(),
                "%02d",
                badDur.substring(0, badDur.indexOf("M")).toInt()
            ) + ":"
            badDur = badDur.substring(badDur.indexOf("M") + 1)
        } else if (hours) duration += "00:"
        if (badDur.contains("S")) {
            if (duration.isEmpty()) duration = "00:"
            duration += String.format(
                Locale.getDefault(),
                "%02d",
                badDur.substring(0, badDur.indexOf("S")).toInt()
            )
        } else {
            duration += "00"
        }
        if (duration == "00:00") {
            duration = ""
        }
        return duration
    }



    fun getStreamingUrlAndChapters(url: String) : MutableList<String?> {
        try {
            val p = Pattern.compile("(^(https?)://(www.)?(music.)?youtu(.be)?)|(^(https?)://(www.)?piped.video)")
            val m = p.matcher(url)

            if (m.find()){
                return getStreamingUrlAndChaptersFromPIPED(url)
            }else{
                throw Exception()
            }
        }catch (e: Exception) {
            try {
                val request = YoutubeDLRequest(url)
                request.addOption("--get-url")
                request.addOption("--print", "%(chapters)s")
                request.addOption("--skip-download")
                request.addOption("-R", "1")
                request.addOption("--socket-timeout", "5")

                if (sharedPreferences.getBoolean("force_ipv4", false)){
                    request.addOption("-4")
                }

                if (sharedPreferences.getBoolean("use_cookies", false)){
                    FileUtil.getCookieFile(context){
                        request.addOption("--cookies", it)
                    }

                    val useHeader = sharedPreferences.getBoolean("use_header", false)
                    val header = sharedPreferences.getString("useragent_header", "")
                    if (useHeader && !header.isNullOrBlank()){
                        request.addOption("--add-header","User-Agent:${header}")
                    }
                }



                val proxy = sharedPreferences.getString("proxy", "")
                if (proxy!!.isNotBlank()){
                    request.addOption("--proxy", proxy)
                }
                request.addOption("-P", FileUtil.getCachePath(context) + "/tmp")



                val youtubeDLResponse = YoutubeDL.getInstance().execute(request)
                val results: Array<String?> = try {
                    val lineSeparator = System.getProperty("line.separator")
                    youtubeDLResponse.out.split(lineSeparator!!).toTypedArray()
                } catch (e: Exception) {
                    arrayOf(youtubeDLResponse.out)
                }
                return results.filter { it!!.isNotEmpty() }.toMutableList()
            } catch (e: Exception) {
                return mutableListOf()
            }
        }
    }

    private fun getStreamingUrlAndChaptersFromPIPED(url: String) : MutableList<String?> {
        val id = getIDFromYoutubeURL(url)
        val res = genericRequest("$pipedURL/streams/$id")
        if (res.length() == 0) {
            throw Exception()
        }else{
            val item = createVideoFromPipedJSON(res, url)
            if (item!!.urls.isBlank()) throw Exception()
            val list = mutableListOf<String?>(Gson().toJson(item.chapters))
            list.addAll(item.urls.split(","))
            return list
        }
    }

    @SuppressLint("RestrictedApi")
    fun buildYoutubeDLRequest(downloadItem: DownloadItem) : YoutubeDLRequest {
        val request = if (downloadItem.playlistURL.isNullOrBlank() || downloadItem.playlistTitle.isBlank()){
            YoutubeDLRequest(downloadItem.url)
        }else{
            YoutubeDLRequest(downloadItem.playlistURL!!).apply {
                if(downloadItem.playlistIndex == null){
                    val matchPortion = downloadItem.url.split("/").last().split("=").last().split("&").first()
                    addOption("--match-filter", "id~='${matchPortion}'")
                }else{
                    addOption("-I", "${downloadItem.playlistIndex!!}:${downloadItem.playlistIndex}")
                }
            }
        }

        val type = downloadItem.type

        val downDir : File
        val canWrite = File(FileUtil.formatPath(downloadItem.downloadPath)).canWrite() || sharedPreferences.getBoolean("access_all_files", false)
        if (!sharedPreferences.getBoolean("cache_downloads", true) && canWrite){
            downDir = File(FileUtil.formatPath(downloadItem.downloadPath))
            request.addOption("--no-quiet")
            request.addOption("--no-simulate")
            request.addOption("--print", "after_move:'%(filepath,_filename)s'")
        }else{
            val cacheDir = FileUtil.getCachePath(context)
            downDir = File(cacheDir, downloadItem.id.toString())
            downDir.delete()
            downDir.mkdirs()
        }

        val aria2 = sharedPreferences.getBoolean("aria2", false)
        if (aria2) {
            request.addOption("--downloader", "libaria2c.so")
            request.addOption("--external-downloader-args", "aria2c:\"--summary-interval=1\"")
        }

        val concurrentFragments = sharedPreferences.getInt("concurrent_fragments", 1)
        if (concurrentFragments > 1) request.addOption("-N", concurrentFragments)

        val retries = sharedPreferences.getString("retries", "")!!
        val fragmentRetries = sharedPreferences.getString("fragment_retries", "")!!

        if(retries.isNotEmpty()) request.addOption("--retries", retries)
        if(fragmentRetries.isNotEmpty()) request.addOption("--fragment-retries", fragmentRetries)

        val limitRate = sharedPreferences.getString("limit_rate", "")!!
        if (limitRate.isNotBlank()) request.addOption("-r", limitRate)

        val bufferSize = sharedPreferences.getString("buffer_size", "")!!
        if (bufferSize.isNotBlank()){
            request.addOption("--buffer-size", bufferSize)
            request.addOption("--no-resize-buffer")
        }

        val sponsorblockURL = sharedPreferences.getString("sponsorblock_url", "")!!
        if (sponsorblockURL.isNotBlank()) request.addOption("--sponsorblock-api", sponsorblockURL)

        if (sharedPreferences.getBoolean("restrict_filenames", true)) request.addOption("--restrict-filenames")
        if (sharedPreferences.getBoolean("force_ipv4", false)){
            request.addOption("-4")
        }
        if (sharedPreferences.getBoolean("use_cookies", false)){
            FileUtil.getCookieFile(context){
                request.addOption("--cookies", it)
            }
        }

        val proxy = sharedPreferences.getString("proxy", "")
        if (proxy!!.isNotBlank()){
            request.addOption("--proxy", proxy)
        }

        val keepCache = sharedPreferences.getBoolean("keep_cache", false)
        if(keepCache){
            request.addOption("--keep-fragments")
        }

        val embedMetadata = sharedPreferences.getBoolean("embed_metadata", true)
        val thumbnailFormat = sharedPreferences.getString("thumbnail_format", "jpg")
        var filenameTemplate = downloadItem.customFileNameTemplate

        if(downloadItem.type != DownloadViewModel.Type.command){
            if (sharedPreferences.getBoolean("no_part", false)){
                request.addOption("--no-part")
            }

            request.addOption("--trim-filenames",  254 - downDir.absolutePath.length)

            if (downloadItem.SaveThumb) {
                request.addOption("--write-thumbnail")
                request.addOption("--convert-thumbnails", thumbnailFormat!!)
            }
            if (!sharedPreferences.getBoolean("mtime", false)){
                request.addOption("--no-mtime")
            }

            val sponsorBlockFilters : ArrayList<String> = when(downloadItem.type) {
                DownloadViewModel.Type.audio -> {
                    downloadItem.audioPreferences.sponsorBlockFilters
                }
                //video
                else -> {
                    downloadItem.videoPreferences.sponsorBlockFilters
                }
            }


            if (sharedPreferences.getBoolean("use_sponsorblock", true)){
                if (sponsorBlockFilters.isNotEmpty()) {
                    val filters = java.lang.String.join(",", sponsorBlockFilters.filter { it.isNotBlank() })
                    if (filters.isNotBlank()) {
                        request.addOption("--sponsorblock-remove", filters)
                        if (sharedPreferences.getBoolean("force_keyframes", false)){
                            request.addOption("--force-keyframes-at-cuts")
                        }
                    }
                }
            }

            if(downloadItem.title.isNotBlank()){
                request.addCommands(listOf("--replace-in-metadata", "video:title", ".+", downloadItem.title.take(180)))
            }


            if (downloadItem.author.isNotBlank()){
                request.addCommands(listOf("--replace-in-metadata", "video:uploader", ".+", downloadItem.author.take(30)))
            }

            request.addOption("--parse-metadata", "uploader:^(?P<uploader>.*?)(?:(?= - Topic)|$)")

            if (embedMetadata){
                request.addOption("--parse-metadata", "%(uploader,channel,creator,artist|null)s:%(uploader)s")
            }

            if (downloadItem.downloadSections.isNotBlank()){
                downloadItem.downloadSections.split(";").forEach {
                    if (it.isBlank()) return@forEach
                    request.addOption("--download-sections", "*${it.split(" ")[0]}")

                    if (sharedPreferences.getBoolean("force_keyframes", false) && !request.hasOption("--force-keyframes-at-cuts")){
                        request.addOption("--force-keyframes-at-cuts")
                    }
                }
                filenameTemplate = if (filenameTemplate.isBlank()){
                    "%(section_title&{} |)s%(title).170B"
                }else{
                    "%(section_title&{} |)s $filenameTemplate"
                }
                if (downloadItem.downloadSections.split(";").size > 1){
                    filenameTemplate = "%(autonumber)d. $filenameTemplate [%(section_start>%H∶%M∶%S)s]"
                }
            }

            if (sharedPreferences.getBoolean("use_audio_quality", false)){
                request.addOption("--audio-quality", sharedPreferences.getInt("audio_quality", 0))
            }

            if (sharedPreferences.getBoolean("write_description", false)){
                request.addOption("--write-description")
            }
        }

        if (sharedPreferences.getString("prevent_duplicate_downloads", "")!! == "download_archive"){
            request.addOption("--download-archive", FileUtil.getDownloadArchivePath(context))
        }

        val preferredAudioCodec = sharedPreferences.getString("audio_codec", "")!!
        val aCodecPrefIndex = context.getStringArray(R.array.audio_codec_values).indexOf(preferredAudioCodec)
        val aCodecPref = runCatching { context.getStringArray(R.array.audio_codec_values_ytdlp)[aCodecPrefIndex] }.getOrElse { "" }

        when(type){
            DownloadViewModel.Type.audio -> {
                val supportedContainers = context.resources.getStringArray(R.array.audio_containers)
                var abrSort = ""

                var audioQualityId : String = downloadItem.format.format_id
                if (audioQualityId.isBlank() || listOf("0", context.getString(R.string.best_quality), "ba", "best", "").contains(audioQualityId)){
                    audioQualityId = "ba/b"
                }else if (listOf(context.getString(R.string.worst_quality), "wa", "worst").contains(audioQualityId)){
                    audioQualityId = "wa/w"
                }else if(audioQualityId.contains("kbps_ytdlnisgeneric")){

                    abrSort = audioQualityId.split("kbps")[0]
                    audioQualityId = ""
                }else{
                    audioQualityId += "/ba/b"
                }


                val ext = downloadItem.container
                if (audioQualityId.isNotBlank()) {
                    if(audioQualityId.contains("-")){
                        audioQualityId = if(!downloadItem.format.lang.isNullOrBlank() && downloadItem.format.lang != "None"){
                            "ba[format_id~='^(${audioQualityId.split("-")[0]})'][language^=${downloadItem.format.lang}]/ba/b"
                        }else{
                            "$audioQualityId/${audioQualityId.split("-")[0]}"
                        }
                    }
                    request.addOption("-f", audioQualityId)
                }else{
                    //enters here if generic or quick downloaded with ba format
                    val preferredLanguage = sharedPreferences.getString("audio_language","")!!
                    if (preferredLanguage.isNotBlank()){
                        request.addOption("-f", "ba[language^=$preferredLanguage]/ba/b")
                    }
                }
                request.addOption("-x")

                val formatSorting = StringBuilder("hasaud")

                if (aCodecPref.isNotBlank()){
                    formatSorting.append(",acodec:$aCodecPref")
                }

                if (abrSort.isNotBlank()){
                    formatSorting.append(",abr~${abrSort}")
                }

                if(ext.isNotBlank()){
                    if(!ext.matches("(webm)|(Default)|(${context.getString(R.string.defaultValue)})".toRegex()) && supportedContainers.contains(ext)){
                        request.addOption("--audio-format", ext)
                        formatSorting.append(",aext:$ext")
                    }
                }

                request.addOption("-P", downDir.absolutePath)
                request.addOption("-S", formatSorting.toString())

                if (downloadItem.audioPreferences.splitByChapters && downloadItem.downloadSections.isBlank()){
                    request.addOption("--split-chapters")
                    request.addOption("-o", "chapter:%(section_title)s.%(ext)s")
                }else{

                    if (embedMetadata){
                        request.addOption("--embed-metadata")
                        request.addOption("--parse-metadata", "%(artist,uploader)s:^(?P<meta_album_artist>[^,]*)")
                        request.addOption("--parse-metadata", "%(album_artist,meta_album_artist|)s:%(album_artist)s")

                        request.addOption("--parse-metadata", "description:(?:Released on: )(?P<dscrptn_year>\\d{4})")
                        request.addOption("--parse-metadata", "%(dscrptn_year,release_year,release_date>%Y,upload_date>%Y)s:%(meta_date)s")

                        if (downloadItem.playlistTitle.isNotEmpty()) {
                            request.addOption("--parse-metadata", "%(album,title)s:%(meta_album)s")
                            request.addOption("--parse-metadata", "%(track_number,playlist_index)d:%(track_number)s")
                        } else {
                            request.addOption("--parse-metadata", "%(album,title)s:%(meta_album)s")
                        }
                    }

                    val cropThumb = downloadItem.audioPreferences.cropThumb ?: sharedPreferences.getBoolean("crop_thumbnail", true)
                    if (downloadItem.audioPreferences.embedThumb){
                        request.addOption("--embed-thumbnail")
                        if (!request.hasOption("--convert-thumbnails")) request.addOption("--convert-thumbnails", thumbnailFormat!!)

                        val thumbnailConfig = StringBuilder("")
                        val cropConfig = """-vf crop=\"'if(gt(ih,iw),iw,ih)':'if(gt(iw,ih),ih,iw)'\"""""
                        if (thumbnailFormat == "jpg")  thumbnailConfig.append("""--ppa "ThumbnailsConvertor:-qmin 1 -q:v 1"""")
                        if (cropThumb){
                            if (thumbnailFormat == "jpg") {
                                thumbnailConfig.deleteCharAt(thumbnailConfig.length - 1)
                                thumbnailConfig.append(""" $cropConfig""")
                            }
                            else thumbnailConfig.append("""--ppa "ThumbnailsConvertor:$cropConfig""")
                        }

                        if (thumbnailConfig.isNotBlank()){
                            runCatching {
                                val config = File(context.cacheDir.absolutePath + "/config" + downloadItem.id + "##ffmpegCrop.txt")
                                config.writeText(thumbnailConfig.toString())
                                request.addOption("--config", config.absolutePath)
                            }
                        }

                    }

                    if (filenameTemplate.isNotBlank()){
                        request.addOption("-o", "${filenameTemplate.removeSuffix(".%(ext)s")}.%(ext)s")
                    }
                }

            }
            DownloadViewModel.Type.video -> {
                val supportedContainers = context.resources.getStringArray(R.array.video_containers)

                if (downloadItem.videoPreferences.addChapters) {
                    if (sharedPreferences.getBoolean("use_sponsorblock", true)){
                        request.addOption("--sponsorblock-mark", "all")
                    }
                    request.addOption("--embed-chapters")
                }


                var cont = ""
                val outputContainer = downloadItem.container
                if(
                    outputContainer.isNotEmpty() &&
                    outputContainer != "Default" &&
                    outputContainer != context.getString(R.string.defaultValue) &&
                    supportedContainers.contains(outputContainer)
                ){
                    cont = outputContainer
                    request.addOption("--merge-output-format", outputContainer.lowercase())
                    if (outputContainer != "webm") {
                        val embedThumb = sharedPreferences.getBoolean("embed_thumbnail", false)
                        if (embedThumb) {
                            request.addOption("--embed-thumbnail")
                            if (!request.hasOption("--convert-thumbnails")) request.addOption("--convert-thumbnails", thumbnailFormat!!)
                        }
                    }
                }
                var acont = sharedPreferences.getString("audio_format", "")!!
                if (acont == "Default") acont = ""

                //format logic
                var videoF = downloadItem.format.format_id
                var altAudioF = ""
                var audioF = downloadItem.videoPreferences.audioFormatIDs.map { f ->
                    val format = downloadItem.allFormats.find { it.format_id == f }
                    format?.run {
                        if (this.format_id.contains("-")) {
                            if (!this.lang.isNullOrBlank() && this.lang != "None") {
                                "ba[format_id~='^(${this.format_id.split("-")[0]})'][language^=${this.lang}]"
                            } else {
                                altAudioF = this.format_id.split("-")[0]
                                this.format_id
                            }
                        } else this.format_id
                    } ?: f
                }.joinToString("+").ifBlank { "ba" }
                val preferredAudioLanguage = sharedPreferences.getString("audio_language", "")!!
                if (downloadItem.videoPreferences.removeAudio) audioF = ""

                var abrSort = ""
                if(audioF.contains("kbps_ytdlnisgeneric")){
                    abrSort = audioF.split("kbps")[0]
                    audioF = ""
                }

                val f = StringBuilder()

                val preferredCodec = sharedPreferences.getString("video_codec", "")
                val preferredQuality = sharedPreferences.getString("video_quality", "best")
                val vCodecPrefIndex = context.getStringArray(R.array.video_codec_values).indexOf(preferredCodec)
                val vCodecPref = context.getStringArray(R.array.video_codec_values_ytdlp)[vCodecPrefIndex]

                val defaultFormats = context.resources.getStringArray(R.array.video_formats_values)
                val usingGenericFormat = defaultFormats.contains(videoF) || downloadItem.allFormats.isEmpty() || downloadItem.allFormats == getGenericVideoFormats(context.resources)
                var hasGenericResulutionFormat = ""
                if (!usingGenericFormat){
                    // with audio removed
                    if (audioF.isBlank()){
                        f.append("$videoF/bv/b")
                    }else{
                        //with audio
                        f.append("$videoF+$audioF/")
                        if (altAudioF.isNotBlank()){
                            f.append("$videoF+$altAudioF/")
                        }
                        f.append("$videoF+ba/$videoF/b")

                        if (audioF.count("+") > 0){
                            request.addOption("--audio-multistreams")
                        }
                    }
                }else{
                    if (videoF == context.resources.getString(R.string.best_quality) || videoF == "best") {
                        videoF = "bv"
                    }else if (videoF == context.resources.getString(R.string.worst_quality) || videoF == "worst") {
                        videoF = "wv"
                        if (audioF == "ba") audioF = "wa"
                    }else if (defaultFormats.contains(videoF)) {
                        hasGenericResulutionFormat = videoF.split("_")[0].dropLast(1)
                        videoF = "bv"
                    }

                    val preferredFormatIDs = sharedPreferences.getString("format_id", "").toString()
                        .split(",")
                        .filter { it.isNotEmpty() }
                        .ifEmpty { listOf(videoF) }.toMutableList()
                    if (!preferredFormatIDs.contains(videoF)){
                        preferredFormatIDs.add(0, videoF)
                    }
                    // ^ [videoF, preferredID1, preferredID2...]

                    val preferredAudioFormatIDs = sharedPreferences.getString("format_id_audio", "")
                        .toString()
                        .split(",")
                        .filter { it.isNotBlank() }
                        .ifEmpty {
                            val list = mutableListOf<String>()
                            if (preferredAudioLanguage.isNotEmpty() && !downloadItem.videoPreferences.removeAudio) list.add("ba[language^=$preferredAudioLanguage]")
                            list.add(audioF)
                            list
                        }.apply {

                        }.toMutableList()
                    if (!preferredAudioFormatIDs.contains(audioF) && audioF != "ba"){
                        preferredAudioFormatIDs.add(0, audioF)
                    }
                    // ^ [audioF, preferredAID1, preferredAID2, ....]
                    if (preferredAudioFormatIDs.any{it.contains("+")}){
                        request.addOption("--audio-multistreams")
                    }


                    preferredFormatIDs.forEach { v ->
                        preferredAudioFormatIDs.forEach { a ->
                            val aa = if (a.isNotBlank()) "+$a" else ""
                            f.append("$v$aa/")
                        }
                        //build format with just videoformatid and audio if remove audio is not checked
                        if (!downloadItem.videoPreferences.removeAudio){
                            //build format with audio with preferred language
                            if (preferredAudioLanguage.isNotBlank()){
                                val al = if (v == "wv"){
                                    "$v+wa[language^=$preferredAudioLanguage]/"
                                }else{
                                    "$v+ba[language^=$preferredAudioLanguage]/"
                                }
                                if (!f.contains(al)) f.append(al)
                            }
                            //build format with best audio
                            if (!f.contains("$v+ba/") && !f.contains("wa")) f.append("$v+ba/")
                            //build format with standalone video
                            if (v == "wv"){
                                f.append("w/")
                            }else{
                                if (v != "bv") f.append("$v/")
                            }
                        }
                    }

                    if(!f.endsWith("b/")){
                        //last fallback
                        f.append("b")
                    }

                }

                val genericFormats = getGenericVideoFormats(context.resources).map { it.format_id }

                StringBuilder().apply {
                    if (hasGenericResulutionFormat.isNotBlank()) {
                        append(",res:${hasGenericResulutionFormat}")
                    }else if (genericFormats.contains(videoF) && preferredQuality!!.contains("p_")){
                        append(",res:${preferredQuality.split("_")[0]}")
                    }
                    if (sharedPreferences.getBoolean("prefer_smaller_formats", false)) append(",+size")
                    if (vCodecPref.isNotBlank()) append(",vcodec:$vCodecPref")
                    if (aCodecPref.isNotBlank()) append(",acodec:$aCodecPref")
                    if (cont.isNotBlank()) append(",vext:$cont")
                    if (acont.isNotBlank()) append(",aext:$acont")
                    if (abrSort.isNotBlank()) append(",abr~${abrSort}")
                    if (this.isNotBlank()){
                        request.addOption("-S", "+hasaud$this")
                    }
                }


                request.addOption("-f", f.toString().replace("/$".toRegex(), ""))

                if (downloadItem.videoPreferences.writeSubs){
                    request.addOption("--write-subs")
                }

                if (downloadItem.videoPreferences.embedSubs) {
                    request.addOption("--embed-subs")
                }

                if(downloadItem.videoPreferences.writeAutoSubs){
                    request.addOption("--write-auto-subs")
                }

                if (downloadItem.videoPreferences.embedSubs || downloadItem.videoPreferences.writeSubs || downloadItem.videoPreferences.writeAutoSubs){
                    val subFormat = sharedPreferences.getString("sub_format", "srt")
                    if(subFormat!!.isNotBlank()){
                        request.addOption("--sub-format", "${subFormat}/best")
                        request.addOption("--convert-subtitles", subFormat)
                    }
                    request.addOption("--sub-langs", downloadItem.videoPreferences.subsLanguages.ifEmpty { "en.*,.*-orig" })
                }



                if (downloadItem.videoPreferences.removeAudio){
                    request.addOption("--use-postprocessor", "FFmpegCopyStream")
                    request.addOption("--ppa", "CopyStream:-c copy -an")

                }

                request.addOption("-P", downDir.absolutePath)

                if (downloadItem.videoPreferences.splitByChapters  && downloadItem.downloadSections.isBlank()){
                    request.addOption("--split-chapters")
                    request.addOption("-o", "chapter:%(section_number)d - %(section_title)s.%(ext)s")
                }else{
                    if (filenameTemplate.isNotBlank()){
                        request.addOption("-o", "${filenameTemplate.removeSuffix(".%(ext)s")}.%(ext)s")
                    }
                }

            }
            DownloadViewModel.Type.command -> {
                request.addOption("-P", downDir.absolutePath)
                request.addOption(
                    "--config-locations",
                    File(context.cacheDir.absolutePath + "/config[${downloadItem.id}].txt").apply {
                        writeText(downloadItem.format.format_note)
                    }.absolutePath
                )

            }

            else -> {}
        }

        if (downloadItem.extraCommands.isNotBlank() && downloadItem.type != DownloadViewModel.Type.command){
            val cache = File(FileUtil.getCachePath(context))
            cache.mkdirs()
            val conf = File(cache.absolutePath + "/${System.currentTimeMillis()}${UUID.randomUUID()}.txt")
            conf.createNewFile()
            conf.writeText(downloadItem.extraCommands)
            request.addOption(
                "--config-locations",
                conf.absolutePath
            )
        }


        return request
    }

    fun getPipedInstances() : List<String> {
        kotlin.runCatching {
            val res = genericArrayRequest("https://piped-instances.kavin.rocks/")
            val list = mutableListOf<String>()
            for (i in 0 until res.length()) {
                val element = res.getJSONObject(i)
                list.add(element.getString("api_url"))
            }
            return list
        }
        return listOf()
    }

    fun parseYTDLRequestString(request : YoutubeDLRequest) : String {
        val arr = request.buildCommand().toMutableList()
        for (i in arr.indices) {
            if (!arr[i].startsWith("-")) {
                arr[i] = "\"${arr[i]}\""
            }
        }

        var final = java.lang.String.join(" ", arr).replace("\"\"", "\" \"")
        val ppas = "--config(-locations)? \"(.*?)\"".toRegex().findAll(final)
        ppas.forEach {res ->
            val path = "\"(.*?)\"".toRegex().find(res.value)?.value?.replace("\"", "")
            val newVal = runCatching {
                File(path ?: "").readText()
            }.onFailure {
                res.value
            }.getOrDefault("")

            final = final.replace(res.value, newVal)

        }

        return final
    }

    fun getGenericAudioFormats(resources: Resources) : MutableList<Format>{
        val audioFormatIDPreference = sharedPreferences.getString("format_id_audio", "").toString().split(",").filter { it.isNotEmpty() }
        val audioFormats = resources.getStringArray(R.array.audio_formats)
        val audioFormatsValues = resources.getStringArray(R.array.audio_formats_values)
        val formats = mutableListOf<Format>()
        val containerPreference = sharedPreferences.getString("audio_format", "")
        val acodecPreference = sharedPreferences.getString("audio_codec", "")!!.run {
            if (this.isEmpty()){
                resources.getString(R.string.defaultValue)
            }else{
                val audioCodecs = resources.getStringArray(R.array.audio_codec)
                val audioCodecsValues = resources.getStringArray(R.array.audio_codec_values)
                audioCodecs[audioCodecsValues.indexOf(this)]
            }
        }
        audioFormats.forEachIndexed { idx, it -> formats.add(Format(audioFormatsValues[idx], containerPreference!!,"",acodecPreference!!, "",0, it)) }
        audioFormatIDPreference.forEach { formats.add(Format(it, containerPreference!!,"",resources.getString(R.string.preferred_format_id), "",1, it)) }
        return formats
    }

    fun getGenericVideoFormats(resources: Resources) : MutableList<Format>{
        val formatIDPreference = sharedPreferences.getString("format_id", "").toString().split(",").filter { it.isNotEmpty() }
        val videoFormatsValues = resources.getStringArray(R.array.video_formats_values)
        val videoFormats = resources.getStringArray(R.array.video_formats)
        val formats = mutableListOf<Format>()
        val containerPreference = sharedPreferences.getString("video_format", "")
        val audioCodecPreference = sharedPreferences.getString("audio_codec", "")!!.run {
            if (this.isNotEmpty()){
                val audioCodecs = resources.getStringArray(R.array.audio_codec)
                val audioCodecsValues = resources.getStringArray(R.array.audio_codec_values)
                audioCodecs[audioCodecsValues.indexOf(this)]
            }else this
        }
        val videoCodecPreference = sharedPreferences.getString("video_codec", "")!!.run {
            if (this.isEmpty()){
                resources.getString(R.string.defaultValue)
            }else{
                val videoCodecs = resources.getStringArray(R.array.video_codec)
                val videoCodecsValues = resources.getStringArray(R.array.video_codec_values)
                videoCodecs[videoCodecsValues.indexOf(this)]
            }
        }
        videoFormatsValues.forEachIndexed { index, it ->  formats.add(Format(it, containerPreference!!,videoCodecPreference,audioCodecPreference, "",0, videoFormats[index])) }
        formatIDPreference.forEach { formats.add(Format(it, containerPreference!!,resources.getString(R.string.preferred_format_id),"", "",1, it)) }
        return formats
    }

    private val pipedURL = sharedPreferences.getString("piped_instance", defaultPipedURL)?.ifEmpty { defaultPipedURL }?.removeSuffix("/")

    class PlaylistTuple internal constructor(
        var nextPageToken: String,
        var videos: ArrayList<ResultItem>,
    )

    companion object {
        private const val TAG = "API MANAGER"
        private const val defaultPipedURL = "https://pipedapi.kavin.rocks/"
        private var countryCODE: String = ""
    }
}