package com.cheng.blzb.utils

import android.os.Handler
import android.os.Looper
import android.util.Log
import com.example.base.utils.L
import okhttp3.*
import okhttp3.Headers.Companion.toHeaders
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import java.io.*
import java.net.URL
import java.util.concurrent.CancellationException
import java.util.concurrent.TimeUnit


class DownLoadUtils private constructor() {

    companion object {
        private const val TAG = "DownLoadUtils"
        private val downLoadHttpUtils: DownLoadUtils by lazy {
            DownLoadUtils()
        }

        @JvmStatic
        @Synchronized
        fun getInstance(): DownLoadUtils {
            return downLoadHttpUtils
        }
    }

    private var downloadSizeInfo = mutableMapOf<String, Long>()
    private var cancelledList = mutableListOf<String>()

    private var buffSize = 4096//建议设置为2048
    fun setBuffSize(size: Int): DownLoadUtils {
        this.buffSize = size
        return this
    }

    private var interceptor: Interceptor? = null
    fun setInterceptor(interceptor: Interceptor?): DownLoadUtils {
        this.interceptor = interceptor
        return this
    }

    private var readTimeOut = 30L
    fun setReadTimeOut(read: Long): DownLoadUtils {
        this.readTimeOut = read
        return this
    }

    private var writeTimeout = 30L
    fun setWriteTimeOut(write: Long): DownLoadUtils {
        this.writeTimeout = write
        return this
    }

    private var connectTimeout = 30L
    fun setConnectTimeOut(connect: Long): DownLoadUtils {
        this.connectTimeout = connect
        return this
    }

    var filePath = ""
    fun setFilePath(path: String): DownLoadUtils {
        this.filePath = path
        return this
    }

    private var fileName = ""
    fun setFileName(name: String): DownLoadUtils {
        this.fileName = name
        return this
    }

    private var deleteWhenException = true
    fun setDeleteWhenException(dele: Boolean): DownLoadUtils {
        this.deleteWhenException = dele
        return this
    }

    private val requestBuilder: Request.Builder = Request.Builder()
    private var urlBuilder: HttpUrl.Builder? = null

    private val okHttpClient = lazy {
        OkHttpClient.Builder()
            .readTimeout(readTimeOut, TimeUnit.SECONDS)
            .writeTimeout(writeTimeout, TimeUnit.SECONDS)
            .connectTimeout(connectTimeout, TimeUnit.SECONDS)
            .addInterceptor(interceptor ?: LoggingInterceptor())
            .build()
    }
    private var actionGetTotal: ((total: Long) -> Unit)? = { _ -> }
    private var actionProgress: ((position: Long) -> Unit)? = { _ -> }
    private var actionSuccess: (file: File) -> Unit = { _ -> }
    private var actionFail: ((msg: String) -> Unit)? = { _ -> }
    private var actionCancel: (() -> Unit)? = { }

    fun setActionCallBack(
        actionGetTotal: ((total: Long) -> Unit)? = null,
        actionProgress: ((position: Long) -> Unit)? = null,
        actionSuccess: (file: File) -> Unit,
        actionFail: (msg: String) -> Unit,
        actionCancel: (() -> Unit)? = null
    ): DownLoadUtils {
        this.actionGetTotal = actionGetTotal
        this.actionProgress = actionProgress
        this.actionSuccess = actionSuccess
        this.actionFail = actionFail
        this.actionCancel = actionCancel
        return this
    }

    private var downCallBack: DownCallBack? = null
    fun setDownCallBack(callBack: DownCallBack): DownLoadUtils {
        this.downCallBack = callBack
        return this
    }

    fun initUrl(url: String, params: Map<String, String>?): DownLoadUtils {
        urlBuilder = url.toHttpUrlOrNull()?.newBuilder()
        if (params.isNullOrEmpty()) {
            return this
        } else {
            for ((k, v) in params) {
                checkName(k)
                urlBuilder?.setQueryParameter(k, v)
            }
        }
        return this
    }

    fun addHeader(map: Map<String, String>): DownLoadUtils {
        requestBuilder.headers(map.toHeaders())
        return this
    }

    private fun checkName(name: String) {
        require(name.isNotEmpty()) { "name is empty" }
    }

    fun down() {
        if (urlBuilder == null) {
            throw IllegalStateException("url not init")
        } else {
            doDown()
        }
    }

    private fun doDown() {
        val startTime = System.currentTimeMillis()
        Log.i(TAG, "startTime=$startTime")
        val url = urlBuilder?.build()
        if (url == null) {
            doException("url is null")
            return
        }
        if (isDowning(filePath + fileName)) {
            return
        }
        cancelledList.remove(fileName)

        val currentLen = downloadSizeInfo[fileName] ?: 0L
        if (isCanContinueDownload(url.toUrl(), currentLen)) {
            requestBuilder.removeHeader("RANGE")
            requestBuilder.addHeader("RANGE", "bytes=${currentLen}-")
        }
        val request = requestBuilder.url(url).tag(filePath + fileName).build()

        var `is`: InputStream? = null
        var raf: RandomAccessFile? = null
        var file: File? = null
        try {
            val response = okHttpClient.value.newCall(request).execute()
            val total = response.body?.contentLength() ?: 0
            doGetTotal(currentLen + total)

            val buf = ByteArray(buffSize)
            var len: Int

            file = if (fileName.isEmpty()) {
                File(filePath)
            } else {
                val fileDir = File(filePath)
                if (!fileDir.exists() || !fileDir.isDirectory) {
                    fileDir.mkdirs()
                }
                File(filePath, fileName)
            }

            `is` = response.body?.byteStream() ?: FileInputStream("")
            raf = RandomAccessFile(file, "rw")
            raf.seek(currentLen)

            var sum: Long = currentLen
            while (`is`.read(buf).also { len = it } != -1) {
                if (isCancelled()) throw CancellationException()
                raf.write(buf, 0, len)
                sum += len.toLong()
                downloadSizeInfo[fileName] = sum
                Log.e(TAG, "download progress : $sum")
                doProgress(sum)
            }
            Log.e(TAG, "download success")

            if (!file.exists()) {
                throw FileNotFoundException("file create err,not exists")
            } else {
                doSuccess(file)
            }
            Log.e(TAG, "totalTime=" + (System.currentTimeMillis() - startTime))
        } catch (e: Exception) {
            if (deleteWhenException && file?.exists() == true) file.delete()
            Log.e(TAG, "download failed : " + e.message)
            doException(e.message.toString())
        } finally {
            try {
                `is`?.close()
            } catch (e: IOException) {
                L.d(e)
            }
            try {
                raf?.close()
            } catch (e: IOException) {
                L.d(e)
            }
        }
    }

    private fun isCanContinueDownload(url: URL, start: Long): Boolean { //是否支持断点下载
        val requestBuilder = Request.Builder()
        requestBuilder.addHeader("RANGE", "bytes=$start-")
        requestBuilder.addHeader("Connection", "close")
        val request: Request = requestBuilder.url(url).head().build()
        val response: Response = okHttpClient.value.newCall(request).execute()
        return if (response.isSuccessful) {
            if (response.code == 206) { //支持
                response.close()
                true
            } else { //不支持
                response.close()
                false
            }
        } else {
            response.close()
            false
        }
    }

    private fun isDowning(tag: String): Boolean {
        for (call in okHttpClient.value.dispatcher.runningCalls()) {
            if (call.request().tag() == tag) {
                return true
            }
        }
        return false
    }

    private fun isCancelled(): Boolean {
        return cancelledList.contains(fileName)
    }

    fun cancel() {
        cancel(filePath + fileName)
    }

    fun cancel(tag: String) {
        cancelledList.add(fileName)
        if (okHttpClient.value.dispatcher.runningCalls().isNotEmpty()) {
            for (call in okHttpClient.value.dispatcher.runningCalls()) {
                if (call.request().tag() == tag) {
                    call.cancel()
                }
            }
        }
        doCancel()
    }

    private fun doException(err: String) {
        runOnUiThread {
            if (downCallBack == null) {
                actionFail?.invoke(err)
            } else {
                downCallBack?.fail(err)
            }
            mainThread = null
        }
    }

    private fun doSuccess(file: File?) {
        runOnUiThread {
            if (file == null) {
                doException("file not exit")
            } else {
                if (downCallBack == null) {
                    actionSuccess.invoke(file)
                } else {
                    downCallBack?.success(file)
                }
            }
            mainThread = null
        }
    }

    private fun doGetTotal(total: Long) {
        runOnUiThread {
            if (downCallBack == null) {
                actionGetTotal?.invoke(total)
            } else {
                downCallBack?.total(total)
            }
        }
        mainThread = null
    }

    private fun doProgress(progress: Long) {
        runOnUiThread {
            if (downCallBack == null) {
                actionProgress?.invoke(progress)
            } else {
                downCallBack?.progress(progress)
            }
        }
    }

    private fun doCancel() {
        runOnUiThread {
            if (downCallBack == null) {
                actionCancel?.invoke()
            } else {
                downCallBack?.cancel()
            }
        }
        mainThread = null
    }

    private var mainThread: Handler? = null
    private fun runOnUiThread(action: () -> Unit) {
        if (Looper.myLooper() != Looper.getMainLooper()) { // If we finish marking off of the main thread, we need to
            // actually do it on the main thread to ensure correct ordering.
            if (mainThread == null) {
                mainThread = Handler(Looper.getMainLooper())
            }
            mainThread?.post {
                action.invoke()
            }
            return
        }
        action.invoke()
    }


    class LoggingInterceptor : Interceptor {
        @Throws(IOException::class)
        override fun intercept(chain: Interceptor.Chain): Response {
            val request: Request = chain.request()
            val startTime = System.nanoTime()
            Log.d(
                TAG, String.format(
                    "Sending request %s on %s%n%s",
                    request.url, chain.connection(), request.headers
                )
            )
            val response: Response = chain.proceed(request)
            val endTime = System.nanoTime()
            Log.d(
                TAG, String.format(
                    "Received response for %s in %.1fms%n%s",
                    response.request.url, (endTime - startTime) / 1e6, response.headers
                )
            )
            return response
        }

        companion object {
            private const val TAG = "LoggingInterceptor"
        }
    }

    interface DownCallBack {
        fun success(file: File)
        fun fail(str: String)
        fun progress(position: Long)
        fun total(total: Long)
        fun cancel()
    }
}