package com.connectsdk

import android.app.AlertDialog
import android.content.res.Resources.NotFoundException
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import com.facebook.react.bridge.Promise
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.ReactContextBaseJavaModule
import com.facebook.react.bridge.ReactMethod
import com.facebook.react.bridge.ReadableArray
import com.facebook.react.bridge.ReadableMap
import com.facebook.react.bridge.ReadableType
import com.facebook.react.bridge.WritableArray
import com.facebook.react.bridge.WritableMap
import com.facebook.react.bridge.WritableNativeArray
import com.facebook.react.bridge.WritableNativeMap
import com.facebook.react.bridge.PromiseImpl
import com.facebook.react.modules.core.DeviceEventManagerModule
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import com.google.gson.reflect.TypeToken
import com.google.gson.ToNumberPolicy
import com.google.gson.JsonSerializer
import com.google.gson.JsonDeserializer
import com.google.gson.JsonSerializationContext
import com.google.gson.JsonElement
import com.google.gson.JsonDeserializationContext
import com.google.gson.JsonPrimitive
import com.google.gson.JsonObject
import com.google.gson.JsonArray
import java.lang.RuntimeException
import java.lang.reflect.Type
import java.security.InvalidParameterException
import java.util.Base64
import java.util.concurrent.CompletableFuture
import kotlin.coroutines.cancellation.CancellationException
import kotlin.onFailure
import kotlin.collections.filter
import kotlinx.coroutines.future.await
import kotlinx.coroutines.launch
import kotlinx.coroutines.async
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.withContext
import org.jetbrains.annotations.Nullable
import org.json.JSONArray
import org.json.JSONException
import org.json.JSONObject
import tv.younify.sdk.connect.Connect
import tv.younify.sdk.connect.ConnectOptions
import tv.younify.sdk.connect.Errors
import tv.younify.sdk.connect.FetchContentCategoryResult
import tv.younify.sdk.connect.GenericException
import tv.younify.sdk.connect.LogLevel
import tv.younify.sdk.connect.LogListener
import tv.younify.sdk.connect.OperationCanceledException
import tv.younify.sdk.connect.RenewTokensCallback
import tv.younify.sdk.connect.SearchContentServiceResult
import tv.younify.sdk.connect.StreamingCategory
import tv.younify.sdk.connect.StreamingContent
import tv.younify.sdk.connect.StreamingService
import tv.younify.sdk.connect.TokenHandler
import tv.younify.sdk.connect.InvalidArgumentException
import tv.younify.sdk.connect.InvalidViewContextException
import tv.younify.sdk.connect.UnknownException
import tv.younify.sdk.connect.GenericExceptionUserInfoKeys

class ConnectSdkModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext), LogListener, TokenHandler
{
    companion object
    {
        const val EventLogLogName = "Log_onLog"
        const val EventLogLogParam_Level = "level"
        const val EventLogLogParam_Message = "message"

        const val EventTokenRenewTokensName = "Token_onRenewTokens"
        const val EventTokenRenewTokensParam_RenewTokenID = "renewalId"
        const val EventTokenRenewTokensParam_ExpiredAccessToken = "expiredAccessToken"
        const val EventTokenRenewTokensParam_RefreshToken = "refreshToken"

        const val EventTokenRenewedTokensName = "Token_onRenewedTokens"
        const val EventTokenRenewedTokensParam_NewAccessToken = "newAccessToken"
        const val EventTokenRenewedTokensParam_NewRefreshToken = "newRefreshToken"

        const val EventFetchContentServiceErrorName = "FetchContent_onServiceError"
        const val EventFetchContentServiceErrorParam_RequestID = "requestId"
        const val EventFetchContentServiceErrorParam_Service = "service"
        const val EventFetchContentServiceErrorParam_Error = "error"

        const val EventFetchContentCategoryServiceCompleteName = "FetchContent_onCategoryServiceComplete"
        const val EventFetchContentCategoryServiceCompleteParam_RequestID = "requestId"
        const val EventFetchContentCategoryServiceCompleteParam_Category = "category"
        const val EventFetchContentCategoryServiceCompleteParam_Service = "service"
        const val EventFetchContentCategoryServiceCompleteParam_Content = "content"
        const val EventFetchContentCategoryServiceCompleteParam_Error = "error"

        const val EventSearchContentServiceCompleteName = "SearchContent_onServiceComplete"
        const val EventSearchContentServiceCompleteParam_RequestID = "requestId"
        const val EventSearchContentServiceCompleteParam_Service = "service"
        const val EventSearchContentServiceCompleteParam_Content = "content"
        const val EventSearchContentServiceCompleteParam_Error = "error"

        const val StreamingServiceLinkResultKey_Service = "service"
        const val StreamingServiceLinkResultKey_Success = "success"

        const val TAG = "YounifyConnectSDK"
    }

    private lateinit var gson: Gson
    private lateinit var mainScope: CoroutineScope
    private lateinit var workerScope: CoroutineScope
    private lateinit var base64Encoder: Base64.Encoder

    private var cancellationMap: MutableMap<String, MutableList<Job>> = mutableMapOf<String, MutableList<Job>>()

    private var renewTokenId: Int = 0
    private var pendingRenewal: MutableMap<Int, RenewTokensCallback> = mutableMapOf<Int, RenewTokensCallback>()

    override fun initialize()
    {
        gson = GsonBuilder()
            .setDateFormat("yyyy-MM-dd'T'HH:mm:ss")
            .registerTypeAdapter(LogLevel::class.java, LogLevelSerializerDeserializer.serializerDeserializer)
            .create()
        base64Encoder = Base64.getEncoder()
        mainScope = CoroutineScope(Job() + Dispatchers.Main)
        workerScope = CoroutineScope(Job() + Dispatchers.IO)
    }

    override fun getName(): String
    {
        return TAG
    }

    private fun sendEventName(eventName: String, body: Any?)
    {
        reactApplicationContext
            .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
            .emit(eventName, body)
    }

    override fun log(level: LogLevel, message: String)
    {
        sendEventName(EventLogLogName,
            WritableNativeMap().apply
            {
                putInt(EventLogLogParam_Level, level.value)
                putString(EventLogLogParam_Message, message)
            })
    }

    override fun renewTokens(expiredAccessToken: String?, refreshToken: String?, complete: RenewTokensCallback)
    {
        var renewTokenId: Int
        var pendingRenewal = this.pendingRenewal

        synchronized(pendingRenewal)
        {
            renewTokenId = this.renewTokenId.inc()
            pendingRenewal[renewTokenId] = complete
            this.renewTokenId = renewTokenId
        }

        sendEventName(EventTokenRenewTokensName,
            WritableNativeMap().apply
            {
                putInt(EventTokenRenewTokensParam_RenewTokenID, renewTokenId)
                putString(EventTokenRenewTokensParam_ExpiredAccessToken, expiredAccessToken)
                putString(EventTokenRenewTokensParam_RefreshToken, refreshToken)
            })
    }

    override fun renewedTokens(newAccessToken: String, newRefreshToken: String)
    {
        sendEventName(EventTokenRenewedTokensName,
            WritableNativeMap().apply
            {
                putString(EventTokenRenewedTokensParam_NewAccessToken, newAccessToken)
                putString(EventTokenRenewedTokensParam_NewRefreshToken, newRefreshToken)
            })
    }

    // Required for RN built in EventEmitter Calls, otherwise the app will give warnings
    // (Warning: `new NativeEventEmitter` was called with anon null argument without the required `removeListeners` method.)
    @ReactMethod
    @Suppress("UNUSED_PARAMETER")
    fun addListener(type: String?)
    {
    }

    @ReactMethod
    @Suppress("UNUSED_PARAMETER")
    fun removeListeners(type: Int?)
    {
    }

    @ReactMethod
    fun configure(options: ReadableMap, promise: Promise)
    {
        runCatchingRejectOnFailure(promise) {
            runInMainScopeCatchingResolveOrReject(
                promise,
                {
                    val connectOptions = readableMapToObject<ConnectOptions>(options, ConnectOptions::class.java) ?: throw InvalidArgumentException("options")
                    connectOptions.logListener = this
                    connectOptions.tokenHandler = this
                    Connect.configure(connectOptions)
                },
                { }
            )
        }
    }

    @ReactMethod
    fun setUserTokens(accessToken: String?, refreshToken: String?, promise: Promise)
    {
        runCatchingRejectOnFailure(promise) {
            runInMainScopeCatchingResolveOrReject(
                promise,
                { Connect.setUserTokens(accessToken, refreshToken) },
                { }
            )
        }
    }

    @ReactMethod
    fun completeTokenRenewal(renewalId: Int, accessToken: String?, refreshToken: String?, promise: Promise)
    {
        runCatchingRejectOnFailure(promise) {
            runInMainScopeCatchingResolveOrReject(
                promise,
                {
                    var renewCallback: RenewTokensCallback?
                    var pendingRenewal = this.pendingRenewal

                    synchronized(pendingRenewal)
                    {
                        renewCallback = pendingRenewal[renewalId]
                        if (renewCallback != null)
                            pendingRenewal.remove(renewalId)
                    }

                    if (renewCallback == null)
                        throw InvalidArgumentException("renewalId")

                    renewCallback?.invoke(accessToken, refreshToken)
                },
                { }
            )
        }
    }

    @ReactMethod
    fun requestUserConsent(cancellationTicket: String?, promise: Promise)
    {
        runCatchingRejectOnFailure(promise) {
            runCancellableInMainScopeCatchingResolveOrReject(
                cancellationTicket,
                promise,
                {
                    val appCompatActivity = reactApplicationContext?.currentActivity as? AppCompatActivity ?: throw InvalidViewContextException()
                    Connect.requestUserConsent(appCompatActivity, appCompatActivity.activityResultRegistry)
                },
                { it }
            )
        }
    }

    @ReactMethod
    fun linkService(service: ReadableMap, cancellationTicket: String?, promise: Promise)
    {
        runCatchingRejectOnFailure(promise) {
            val streamingService = readableMapToObject<StreamingService>(service, StreamingService::class.java) ?: throw InvalidArgumentException("service")
            runCancellableInMainScopeCatchingResolveOrReject(
                cancellationTicket,
                promise,
                {
                    val appCompatActivity = reactApplicationContext?.currentActivity as? AppCompatActivity ?: throw InvalidViewContextException()
                    Connect.linkService(streamingService, appCompatActivity, appCompatActivity.activityResultRegistry)
                },
                {
                    WritableNativeMap().apply {
                        putBoolean(StreamingServiceLinkResultKey_Success, it ?: false)
                        putMap(StreamingServiceLinkResultKey_Service, objectToWritableMap(streamingService))
                    }
                }
            )
        }
    }

    @ReactMethod
    fun manageLinkedService(service: ReadableMap, cancellationTicket: String?, promise: Promise)
    {
        runCatchingRejectOnFailure(promise) {
            val streamingService = readableMapToObject<StreamingService>(service, StreamingService::class.java) ?: throw InvalidArgumentException("service")
            runCancellableInMainScopeCatchingResolveOrReject(
                cancellationTicket,
                promise,
                {
                    var appCompatActivity = reactApplicationContext?.currentActivity as? AppCompatActivity ?: throw InvalidViewContextException()
                    Connect.manageLinkedService(streamingService, appCompatActivity, appCompatActivity.activityResultRegistry)
                },
                { objectToWritableMap(streamingService) }
            )
        }
    }

    @ReactMethod
    fun unlinkService(service: ReadableMap, cancellationTicket: String?, promise: Promise)
    {
        runCatchingRejectOnFailure(promise) {
            val streamingService = readableMapToObject<StreamingService>(service, StreamingService::class.java) ?: throw InvalidArgumentException("service")
            runCancellableInMainScopeCatchingResolveOrReject(
                cancellationTicket,
                promise,
                { Connect.unlinkService(streamingService) },
                { objectToWritableMap(streamingService) }
            )
        }
    }

    @ReactMethod
    fun fetchServices(cancellationTicket: String?, promise: Promise)
    {
        runCatchingRejectOnFailure(promise) {
            runCancellableInMainScopeCatchingResolveOrReject(
                cancellationTicket,
                promise,
                { Connect.fetchServices() },
                { arrayToWritableArray(it ?: listOf<StreamingService>()) }
            )
        }
    }

    @ReactMethod
    fun fetchLinkedServices(cancellationTicket: String?, promise: Promise)
    {
        runCatchingRejectOnFailure(promise) {
            runCancellableInMainScopeCatchingResolveOrReject(
                cancellationTicket,
                promise,
                { Connect.fetchLinkedServices() },
                { arrayToWritableArray(it ?: listOf<StreamingService>()) }
            )
        }
    }

    @ReactMethod
    fun fetchBrokenServices(cancellationTicket: String?, promise: Promise)
    {
        runCatchingRejectOnFailure(promise) {
            runCancellableInMainScopeCatchingResolveOrReject(
                cancellationTicket,
                promise,
                { Connect.fetchBrokenServices() },
                { arrayToWritableArray(it ?: listOf<StreamingService>()) }
            )
        }
    }

    @ReactMethod
    fun fetchCategories(cancellationTicket: String?, promise: Promise)
    {
        runCatchingRejectOnFailure(promise) {
            runCancellableInMainScopeCatchingResolveOrReject(
                cancellationTicket,
                promise,
                { Connect.fetchCategories() },
                { arrayToWritableArray(it ?: listOf<StreamingCategory>()) }
            )
        }
    }

    @ReactMethod
    fun fetchContent(requestId: String, categories: ReadableArray, services: ReadableArray, cancellationTicket: String?, serviceErrors: Boolean, categoryServiceCompletions: Boolean, allCompletions: Boolean, promise: Promise)
    {
        runCatchingRejectOnFailure(promise) {
            val streamingCategories = readableArrayToList<List<StreamingCategory>>(categories, object : TypeToken<List<StreamingCategory>>(){}.type) ?: throw InvalidArgumentException("categories")
            val streamingServices = readableArrayToList<List<StreamingService>>(services, object : TypeToken<List<StreamingService>>(){}.type) ?: throw InvalidArgumentException("services")

            runCancellableInMainScopeCatchingResolveOrReject(
                cancellationTicket,
                promise,
                {
                    Connect.fetchContent(
                        streamingCategories,
                        streamingServices,
                        { service: StreamingService, cause: Throwable? ->

                            if (serviceErrors)
                                sendEventName(EventFetchContentServiceErrorName,
                                    WritableNativeMap().apply
                                    {
                                        putString(EventFetchContentServiceErrorParam_RequestID, requestId)
                                        putMap(EventFetchContentServiceErrorParam_Error, throwableToWritableMap(cause))
                                        putMap(EventFetchContentServiceErrorParam_Service, objectToWritableMap(service))
                                    })
                        },
                        { category: StreamingCategory, service: StreamingService, content: List<StreamingContent>, cause: Throwable? ->

                            if (categoryServiceCompletions)
                                sendEventName(EventFetchContentCategoryServiceCompleteName,
                                    WritableNativeMap().apply() {
                                        putString(EventFetchContentCategoryServiceCompleteParam_RequestID, requestId)
                                        putMap(EventFetchContentCategoryServiceCompleteParam_Category, objectToWritableMap(category))
                                        putMap(EventFetchContentCategoryServiceCompleteParam_Service, objectToWritableMap(service))
                                        putMap(EventFetchContentCategoryServiceCompleteParam_Error, throwableToWritableMap(cause))
                                        putArray(EventFetchContentCategoryServiceCompleteParam_Content, arrayToWritableArray(content))
                                    })
                        },
                        null
                    )
                },
                { arrayToWritableArray(if (allCompletions && (it != null)) it else listOf<FetchContentCategoryResult>()) }
            )
        }
    }

    @ReactMethod
    fun fetchContentChildren(content: ReadableMap, cancellationTicket: String?, promise: Promise)
    {
        runCatchingRejectOnFailure(promise) {
            val streamingContent = readableMapToObject<StreamingContent>(content, StreamingContent::class.java) ?: throw InvalidArgumentException("content")
            runCancellableInMainScopeCatchingResolveOrReject(
                cancellationTicket,
                promise,
                { Connect.fetchContentChildren(streamingContent) },
                { arrayToWritableArray(it ?: listOf<StreamingContent>()) }
            )
        }
    }

    @ReactMethod
    fun fetchContentDetails(content: ReadableMap, cancellationTicket: String?, promise: Promise)
    {
        runCatchingRejectOnFailure(promise) {
            val streamingContent = readableMapToObject<StreamingContent>(content, StreamingContent::class.java) ?: throw InvalidArgumentException("content")
            runCancellableInMainScopeCatchingResolveOrReject(
                cancellationTicket,
                promise,
                { Connect.fetchContentDetails(streamingContent) },
                { objectToWritableMap(it) }
            )
        }
    }

    @ReactMethod
    fun searchContent(requestId: String, services: ReadableArray, searchPhrase: String, cancellationTicket: String?, serviceCompletions: Boolean, allCompletions: Boolean, promise: Promise)
    {
        runCatchingRejectOnFailure(promise) {
            val streamingServices = readableArrayToList<List<StreamingService>>(services, object : TypeToken<List<StreamingService>>(){}.type) ?: throw InvalidArgumentException("services")

            runCancellableInMainScopeCatchingResolveOrReject(
                cancellationTicket,
                promise,
                {
                    Connect.searchContent(
                        streamingServices,
                        searchPhrase,
                        { service: StreamingService, results: List<StreamingContent>, cause: Throwable? ->

                            if (serviceCompletions)
                                sendEventName(EventSearchContentServiceCompleteName,
                                    WritableNativeMap().apply() {
                                        putString(EventSearchContentServiceCompleteParam_RequestID, requestId)
                                        putMap(EventSearchContentServiceCompleteParam_Service, objectToWritableMap(service))
                                        putMap(EventSearchContentServiceCompleteParam_Error, throwableToWritableMap(cause))
                                        putArray(EventSearchContentServiceCompleteParam_Content, arrayToWritableArray(results))
                                    })
                        },
                        null
                    )
                },
                { arrayToWritableArray(if (allCompletions && (it != null)) it else listOf<SearchContentServiceResult>()) }
            )
        }
    }

    @ReactMethod
    fun fetchImage(url: String, cancellationTicket: String?, promise: Promise)
    {
        runCatchingRejectOnFailure(promise) {
            runCancellableInMainScopeCatchingResolveOrReject(
                cancellationTicket,
                promise,
                { Connect.fetchImage(url) },
                { if (it != null) withContext(workerScope.coroutineContext) { base64Encoder.encodeToString(it) } else null }
            )
        }
    }

    @ReactMethod
    fun cancel(ticketId: String, promise: Promise)
    {
        runCatchingRejectOnFailure(promise) {
            runInMainScopeCatchingResolveOrReject(
                promise,
                {
                    synchronized(cancellationMap)
                    {
                        val futures = cancellationMap[ticketId]
                        if (futures != null)
                            for (future in futures)
                                future.cancel()
                    }
                },
                { it }
            )
        }
    }

    private fun resolve(promise: Promise, data: Any?)
    {
        workerScope.launch { promise.resolve(if (data is Unit) null else data) }
    }

    private fun reject(promise: Promise, data: Any?, cause: Throwable, rejecter: (() -> Unit) -> Unit = { workerScope.launch { it() } })
    {
        var rdata = if (data is Unit) null else data

        var error = cause
        if ((error is CancellationException) && !(error is OperationCanceledException))
            error = OperationCanceledException(null)

        var userInfo = WritableNativeMap()
        var errorCode = Errors.Unknown.toString()

        runCatching {

            val ge = error as? GenericException
            if (ge != null)
            {
                var geUserInfo = ge.userInfo
                if (geUserInfo != null)
                {
                    errorCode = ge.code.toString()
                    geUserInfo.forEach { entry ->
                        putObjectInWritableMap(userInfo, entry.key, entry.value)
                    }
                }
            }

            if (rdata is ReadableMap)
                userInfo.putMap(GenericExceptionUserInfoKeys.PARTIAL_RESULT, rdata)
            else if (rdata is ReadableArray)
                userInfo.putArray(GenericExceptionUserInfoKeys.PARTIAL_RESULT, rdata)
            else
                putObjectInWritableMap(userInfo, GenericExceptionUserInfoKeys.PARTIAL_RESULT, rdata)
        }

        rejecter({ promise.reject(errorCode, error.message, error, userInfo) })
    }

    private inline fun <reified T, Z> runInMainScopeCatchingResolveOrReject(promise: Promise, crossinline block: suspend () -> T?, crossinline resBuilder: suspend (T?) -> Z): Job
    {
        return mainScope.launch {
            runCatching {
                block()
            }
            .onSuccess {
                runCatching { resBuilder(it) }
                    .onSuccess { resolve(promise, it) }
                    .onFailure { e ->
                        runCatching { resBuilder((e as? GenericException?)?.userInfo?.get(GenericExceptionUserInfoKeys.PARTIAL_RESULT) as? T?) }
                            .onSuccess { reject(promise, it, e) }
                            .onFailure { reject(promise, Unit, e) }
                    }
            }
            .onFailure { e ->
                runCatching { resBuilder((e as? GenericException?)?.userInfo?.get(GenericExceptionUserInfoKeys.PARTIAL_RESULT) as? T?) }
                    .onSuccess { reject(promise, it, e) }
                    .onFailure { reject(promise, Unit, e) }
            }
        }
    }

    private fun runCatchingRejectOnFailure(promise: Promise, block: () -> Unit)
    {
        runCatching {
            block()
        }
        .onFailure { e -> reject(promise, (e as? GenericException?)?.userInfo?.get(GenericExceptionUserInfoKeys.PARTIAL_RESULT), e) }
    }

    private inline fun <reified T, Z> runCancellableInMainScopeCatchingResolveOrReject(cancellationTicket: String?, promise: Promise, crossinline block: suspend () -> T?, crossinline resBuilder: suspend (T?) -> Z): Job
    {
        val job = runInMainScopeCatchingResolveOrReject(promise, block, resBuilder)

        if (!cancellationTicket.isNullOrEmpty())
        {
            synchronized(cancellationMap)
            {
                if (job.isActive)
                {
                    var cancellationList = cancellationMap[cancellationTicket]
                    if (cancellationList == null)
                    {
                        cancellationList = mutableListOf<Job>()
                        cancellationMap[cancellationTicket] = cancellationList
                    }

                    cancellationList.add(job)
                }
            }
        }

        job.invokeOnCompletion {
            if (!cancellationTicket.isNullOrEmpty())
                synchronized(cancellationMap)
                {
                    val cancellationList = cancellationMap[cancellationTicket]
                    if (cancellationList != null)
                    {
                        cancellationList.remove(job)
                        if (cancellationList.isEmpty())
                            cancellationMap.remove(cancellationTicket)
                    }
                }
        }

        return job
    }

    private fun <T : Any?> readableMapToObject(readableMap: ReadableMap?, classOfT: Class<T>): T?
    {
        if (readableMap == null)
            return null

        val hashMap: HashMap<String, Any> = readableMap.toHashMap()
        val jsonTree = gson.toJsonTree(hashMap)
        return gson.fromJson<T>(jsonTree, classOfT)
    }

    private fun <T : List<Any?>> readableArrayToList(readableArray: ReadableArray?, typeOfT: Type): T?
    {
        if (readableArray == null)
            return null

        val arrayList = readableArray.toArrayList()
        val jsonTree = gson.toJsonTree(arrayList)
        return gson.fromJson<T>(jsonTree, typeOfT)
    }

    private fun throwableToWritableMap(throwable: Throwable?): WritableMap?
    {
        if (throwable == null)
            return null

        var map: WritableMap? = null
        var promise = PromiseImpl(null, { args ->
            map = args[0] as? WritableMap?
        })
        reject(promise, null, throwable, { it() })

        return map
    }

    private fun objectToWritableMap(obj: Any?): WritableMap?
    {
        if (obj == null)
            return null

        val jsonTree = gson.toJsonTree(obj)
        val jsonObj = jsonTree.asJsonObject
        return jsonObjectToWritableMap(jsonObj)
    }

    private fun arrayToWritableArray(arr: List<Any?>?): WritableArray?
    {
        if (arr == null)
            return null

        val jsonTree = gson.toJsonTree(arr)
        val jsonArr = jsonTree.asJsonArray
        return jsonArrayToWritableArray(jsonArr)
    }

    private fun jsonObjectToWritableMap(jsonObj: JsonObject): WritableMap
    {
        val map: WritableMap = WritableNativeMap()
        val entrySet = jsonObj.entrySet()
        for (entry in entrySet)
        {
            val key = entry.key
            val value = entry.value
            putElementInWritableMap(map, key, value)
        }

        return map
    }

    private fun putObjectInWritableMap(map: WritableMap, key: String, obj: Any?)
    {
        val jsonTree = gson.toJsonTree(obj)
        putElementInWritableMap(map, key, jsonTree)
    }

    private fun putElementInWritableMap(map: WritableMap, key: String, value: JsonElement)
    {
        if (value.isJsonNull)
            map.putNull(key)
        else if (value.isJsonPrimitive)
        {
            val prim = value.asJsonPrimitive
            if (prim.isString)
                map.putString(key, prim.asString)
            else if (prim.isBoolean)
                map.putBoolean(key, prim.asBoolean)
            else if (prim.isNumber)
                map.putDouble(key, prim.asDouble)
        }
        else if (value.isJsonObject)
        {
            val valueMap = jsonObjectToWritableMap(value.asJsonObject)
            map.putMap(key, valueMap)
        }
        else if (value.isJsonArray)
        {
            val valueArr = jsonArrayToWritableArray(value.asJsonArray)
            map.putArray(key, valueArr)
        }
    }

    private fun jsonArrayToWritableArray(jsonArr: JsonArray): WritableArray
    {
        val arr = WritableNativeArray()
        for (value in jsonArr)
            pushElementOnWritableArray(arr, value)
        return arr
    }

    private fun pushElementOnWritableArray(arr: WritableArray, obj: Any?)
    {
        val jsonTree = gson.toJsonTree(obj)
        pushElementOnWritableArray(arr, jsonTree)
    }

    private fun pushElementOnWritableArray(arr: WritableArray, value: JsonElement)
    {
        if (value.isJsonNull)
            arr.pushNull()
        else if (value.isJsonPrimitive)
        {
            val prim = value.asJsonPrimitive
            if (prim.isString)
                arr.pushString(prim.asString)
            else if (prim.isBoolean)
                arr.pushBoolean(prim.asBoolean)
            else if (prim.isNumber)
                arr.pushDouble(prim.asDouble)
        }
        else if (value.isJsonObject)
        {
            val valueMap = jsonObjectToWritableMap(value.asJsonObject)
            arr.pushMap(valueMap)
        }
        else if (value.isJsonArray)
        {
            val valueArr = jsonArrayToWritableArray(value.asJsonArray)
            arr.pushArray(valueArr)
        }
    }

    internal class LogLevelSerializerDeserializer : JsonSerializer<LogLevel>, JsonDeserializer<LogLevel>
    {
        companion object
        {
            internal val serializerDeserializer = LogLevelSerializerDeserializer()
        }

        override fun serialize(value: LogLevel, typeOfT: Type, context: JsonSerializationContext): JsonElement
        {
            return JsonPrimitive(value.value)
        }

        override fun deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext): LogLevel
        {
            runCatching {
                val primitive = json.asJsonPrimitive
                if (primitive.isString)
                {
                    val sval = primitive.asString
                    return LogLevel.valueOf(sval)
                }
                else if (primitive.isNumber)
                {
                    val ival = primitive.asInt
                    return LogLevel.values().first() { it.value == ival }
                }
            }

            return LogLevel.Warning
        }
    }
}
