package com.gh.common.xapk

import android.os.Build
import android.os.Environment
import com.gh.gamecenter.common.utils.debounceActionWithInterval
import com.gh.gamecenter.common.utils.getExtension
import com.gh.gamecenter.common.utils.throwException
import com.gh.gamecenter.common.utils.throwExceptionInDebug
import com.gh.gamecenter.BuildConfig
import com.gh.gamecenter.core.utils.MD5Utils
import com.halo.assistant.HaloApp
import com.lightgame.download.DownloadEntity
import com.lightgame.download.FileUtils
import com.lightgame.utils.Utils
import net.lingala.zip4j.progress.ProgressMonitor
import java.io.File
import java.util.zip.ZipFile


class XapkUnzipThread(
    private var mDownloadEntity: DownloadEntity,
    private var mUnzipListener: IXapkUnzipListener
) : Thread() {

    private val mDefaultBufferSize = 1024 * 1024

    var canceled = false

    override fun run() {
        super.run()
        try {
            val path = mDownloadEntity.path
            var unzipSize = 0L
            try {
                unzipSize = getUnzipSize(path)
            } catch (e: Exception) {
                planB()
                return
            }

            val msg = FileUtils.isCanDownload(HaloApp.getInstance().application, unzipSize)
            if (!msg.isNullOrEmpty()) {
                // 空间不足应该不用刷新页面,保持不变就好
                Utils.toast(HaloApp.getInstance().application, "设备存储空间不足，请清理后重试！")
                mUnzipListener.onCancel(mDownloadEntity)
                return
            }
            var unzipProgress = 0L
            val absolutePath = Environment.getExternalStorageDirectory().absolutePath
            val xapkFile = File(path)

            val unzipClosure: (zip: ZipFile) -> Unit = { zip ->
                for (zipEntry in zip.entries().asSequence()) {
                    val outputFile = if (zipEntry.name.getExtension() == XapkInstaller.XAPK_DATA_EXTENSION_NAME) {
                        File(absolutePath + File.separator + zipEntry.name)
                    } else if (zipEntry.name.getExtension() == XapkInstaller.PACKAGE_EXTENSION_NAME) {
                        // apk文件名称 = xapk文件名 + MD5(本身的文件名称) + ".apk"
                        // 如 abc_com.gh.gamecenter.apk 这样的文件名，在使用浏览器安装时系统浏览器可能会抹掉文件类型，导致无法唤起下载完自动安装，这里去掉多个 "." 号，保证浏览器安装正常使用
                        val fileName = xapkFile.nameWithoutExtension + "_" + MD5Utils.getContentMD5(zipEntry.name) + "." + XapkInstaller.PACKAGE_EXTENSION_NAME
                        File(FileUtils.getDownloadPath(HaloApp.getInstance().application, fileName))
                    } else continue // 暂时只需要解压xpk/obb文件

                    if (zipEntry.isDirectory) {
                        if (!outputFile.exists()) {
                            throwException("unzip create file path failure", !outputFile.mkdirs())
                        }
                        continue
                    }

                    if (!outputFile.parentFile.exists()) {
                        throwException("unzip create file path failure", !outputFile.parentFile.mkdirs())
                    }

                    // 如果存在同名且大小一致,可以认为该文件已经解压缩(在不解压缩的情况下如果如果获取压缩文件的MD5????)
                    if (!outputFile.exists()) {
                        throwException("unzip create file failure", !outputFile.createNewFile())
                    } else if (outputFile.length() != zipEntry.size) {
                        throwException("unzip delete existing file failure", !outputFile.delete())
                        throwException("unzip create file failure", !outputFile.createNewFile())
                    } else {
                        unzipProgress += zipEntry.size
                        mUnzipListener.onProgress(mDownloadEntity, outputFile.path, unzipSize, unzipProgress)
                        mUnzipListener.onNext(mDownloadEntity, outputFile.path)
                        continue
                    }

                    // unzip
                    zip.getInputStream(zipEntry).use { input ->
                        outputFile.outputStream().use { output ->
                            val buffer = ByteArray(mDefaultBufferSize)
                            var bytes = input.read(buffer)
                            while (bytes >= 0) {
                                output.write(buffer, 0, bytes)
                                unzipProgress += bytes
                                bytes = input.read(buffer)
                                if (canceled) {
                                    mUnzipListener.onCancel(mDownloadEntity)
                                    return@use
                                } else {
                                    // 防止多次短时间内多次触发onProgress方法导致阻塞主线程（低端机会出现十分明显的卡顿）
                                    debounceActionWithInterval(-1, 500) {
                                        mUnzipListener.onProgress(
                                            mDownloadEntity,
                                            outputFile.path,
                                            unzipSize,
                                            unzipProgress
                                        )
                                    }
                                }
                            }
                        }
                    }
                    mUnzipListener.onNext(mDownloadEntity, outputFile.path)
                }
            }

            // Kotlin 1.4.X 在安卓 4.4 以下使用 use 默认关闭 ZipFile 的流时会触发
            // java.lang.IncompatibleClassChangeError: interface not implemented 的 Error (Throwable)
            // 但实测是不影响解压的，所以这里换用 let 不关闭流，确保不闪退，并且不影响解压结果
            // 帮用户解压了，但游戏能不能安装就看天吧 (毕竟支持4.X的游戏也不多了)
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
                ZipFile(File(path)).use { unzipClosure.invoke(it) }
            } else {
                ZipFile(File(path)).let { unzipClosure.invoke(it) }
            }
            mUnzipListener.onSuccess(mDownloadEntity)
        } catch (e: Exception) {
            if (BuildConfig.DEBUG) throw e
            mUnzipListener.onFailure(mDownloadEntity, e)
        }
    }

    /**
     *  部分未知情况(有可能是项目配置问题，有可能时Zip包破损)原生的ZipFile无法打开压缩包,则启动以下备用方案
     *  具体复现机型:小米 9(Android 9)
     *  具体复现的压缩包:太空竞速2 具体可以咨询陈思雨
     *
     *  实现方式请参考:https://github.com/srikanth-lingala/zip4j
     *  注意使用该库的ZipInputStream依然无法解压,提示头文件丢失
     *
     * 后续如果有需要可以直接使用该方法进行解压
     */
    private fun planB() {
        val zipPath = mDownloadEntity.path

        val absolutePath = Environment.getExternalStorageDirectory().absolutePath

        val zipFile = net.lingala.zip4j.ZipFile(zipPath)
        val progressMonitor = zipFile.progressMonitor
        zipFile.isRunInThread = true

        val fileHeaders = zipFile.fileHeaders
        var unzipSize = 0L
        for (fileHeader in fileHeaders) {
            unzipSize += fileHeader.uncompressedSize;
        }

        var unzipProgress = 0L
        var completedSize = 0L

        for (fileHeader in fileHeaders) {
            if (canceled) {
                mUnzipListener.onCancel(mDownloadEntity)
                return
            }

            // 暂时只需要解压xpk/obb文件
            val extension = fileHeader.fileName.getExtension()
            if (extension != XapkInstaller.XAPK_DATA_EXTENSION_NAME && extension != XapkInstaller.PACKAGE_EXTENSION_NAME) continue


            var unzipPath = ""
            if (extension == XapkInstaller.XAPK_DATA_EXTENSION_NAME) {
                unzipPath = absolutePath + File.separator + fileHeader.fileName

                if (hasUnzipFile(unzipPath, fileHeader.uncompressedSize)) {
                    mUnzipListener.onNext(mDownloadEntity, unzipPath)
                    continue
                }

                zipFile.extractFile(fileHeader.fileName, absolutePath)
            }

            if (extension == XapkInstaller.PACKAGE_EXTENSION_NAME) {
                val downloadDir = FileUtils.getDownloadDir(HaloApp.getInstance().application)
                val unzipFileName = File(zipPath).nameWithoutExtension + "_" + fileHeader.fileName
                unzipPath = downloadDir + File.separator + unzipFileName

                if (hasUnzipFile(unzipPath, fileHeader.uncompressedSize)) {
                    mUnzipListener.onNext(mDownloadEntity, unzipPath)
                    continue
                }

                zipFile.extractFile(fileHeader.fileName, downloadDir, unzipFileName)
            }

            throwExceptionInDebug("check unzipPath", unzipPath.isEmpty())

            // 回调太频繁了,变态吗? 4K回调一次,还不能改,FUCK
            var filterCounter = 0
            val filterInterval = 1024 * 10
            while (progressMonitor.state != ProgressMonitor.State.READY) {
                filterCounter++
                if (filterCounter % filterInterval == 0) {
                    unzipProgress = completedSize + progressMonitor.workCompleted
                    mUnzipListener.onProgress(mDownloadEntity, unzipPath, unzipSize, unzipProgress)

                    if (canceled) {
                        progressMonitor.isCancelAllTasks = true
                        mUnzipListener.onCancel(mDownloadEntity)
                        return
                    }
                }
            }
            completedSize += fileHeader.uncompressedSize
            mUnzipListener.onNext(mDownloadEntity, unzipPath)
        }

        mUnzipListener.onSuccess(mDownloadEntity)
    }

    private fun hasUnzipFile(unzipPath: String, uncompressedSize: Long): Boolean {
        val unzipFile = File(unzipPath)
        if (unzipFile.exists() || unzipFile.length() == uncompressedSize) return true
        return false
    }

    private fun getUnzipSize(path: String): Long {
        var totalSize = 0L

        val calculateSizeClosure: (zip: ZipFile) -> Unit = { zip ->
            for (entry in zip.entries()) {
                totalSize += entry.size
            }
        }

        // Kotlin 1.4.X 在安卓 4.4 以下使用 use 默认ZipFile 的流时会触发
        // java.lang.IncompatibleClassChangeError: interface not implemented 的 Error (Throwable)
        // 实测是不影响解压，所以这里换用 let 不关闭流，确保不闪退
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
            ZipFile(File(path)).use { calculateSizeClosure.invoke(it) }
        } else {
            ZipFile(File(path)).let { calculateSizeClosure.invoke(it) }
        }
        return totalSize
    }
}