package de.sebse.fuplanner2.network

import androidx.annotation.GuardedBy
import com.android.volley.*
import com.android.volley.toolbox.HttpHeaderParser
import de.sebse.fuplanner2.utils.console
import java.io.UnsupportedEncodingException
import java.net.URLEncoder
import java.nio.charset.Charset
import java.nio.charset.StandardCharsets


class CustomRequest: Request<NetData> {
    /** Lock to guard mListener as it is cleared on cancel() and read on delivery.  */
    private val mLock = Any()

    @GuardedBy("mLock")
    private var mListener: Response.Listener<NetData>? = null

    private var mCookies: Map<String, String>?
    private var mData: Map<String, String>?

    constructor(
        method: Int,
        url: String?,
        cookies: Map<String, String>?,
        data: Map<String, String>?,
        listener: Response.Listener<NetData>?, errorListener: Response.ErrorListener?
    ) : super(method, url, errorListener) {
        mListener = listener
        mCookies = cookies
        mData = data
    }

    constructor(
        method: Int,
        url: String?,
        cookies: Map<String, String>?,
        listener: Response.Listener<NetData>?, errorListener: Response.ErrorListener?
    ) : this(method, url, cookies, null, listener, errorListener)

    override fun cancel() {
        super.cancel()
        synchronized(mLock) { mListener = null }
    }

    override fun deliverResponse(response: NetData) {
        var listener: Response.Listener<NetData>?
        synchronized(mLock) { listener = mListener }
        listener?.onResponse(response)
    }

    override fun deliverError(error: VolleyError?) {
        val volleyError = if (error == null) {
            VolleyError(NetworkResponse(408, null, true, 0, null))
        } else if (error.networkResponse == null) {
            val statusCode: Int = if (error is TimeoutError) 408 else 500
            VolleyError(NetworkResponse(statusCode, null, true, error.networkTimeMs, null))
        } else {
            if (error.networkResponse.statusCode == 302) {
                deliverResponse(parseNetworkResponse(error.networkResponse)?.result ?: NetData("", NetworkResponse(null)))
                null
            } else {
                error
            }
        }
        volleyError?.let {
            console.error("RequestError", "${it.networkResponse.statusCode} on $method $url")
            super.deliverError(it)
        }
    }

    override fun parseNetworkResponse(response: NetworkResponse): Response<NetData>? {
        val parsed: String = response.headers?.let { parse(response.data, it) } ?: ""
        return Response.success(NetData(parsed, response), HttpHeaderParser.parseCacheHeaders(response))
    }

    override fun getHeaders(): MutableMap<String, String> {
        var params =
            super.getHeaders()
        mCookies?.let { cookieMap ->
            params = params?.let { HashMap(it) } ?: HashMap()
            val newStr = ArrayList<String>()
            for (key in cookieMap.keys) newStr.add("$key=${cookieMap[key]}")
            params["Cookie"] = newStr.joinToString("; ")
        }

        return params
    }

    override fun getBodyContentType(): String? {
        return "application/x-www-form-urlencoded"
    }

    override fun getBody(): ByteArray? {
        mData?.let { data ->
            val sb = StringBuilder()
            for (key in data.keys) {
                if (sb.isNotEmpty()) {
                    sb.append('&')
                }
                try {
                    sb.append(URLEncoder.encode(key, "UTF-8")).append('=')
                        .append(URLEncoder.encode(data[key], "UTF-8"))
                } catch (ignored: UnsupportedEncodingException) {
                }
            }
            val requestBody = sb.toString()
            return requestBody.toByteArray(StandardCharsets.UTF_8)
        }
        return null
    }

    private fun parse(body: ByteArray?, headers: Map<String, String>): String? {
        return if (body == null) null else try {
            String(body, Charset.forName(HttpHeaderParser.parseCharset(headers)))
        } catch (e: UnsupportedEncodingException) {
            String(body)
        }
    }
}