package com.doverfuelingsolutions.issp.utils.log

import android.os.Build
import android.util.Log
import com.doverfuelingsolutions.issp.utils.thread.ThreadUtil
import com.google.gson.Gson
import java.io.File
import java.io.PrintWriter
import java.io.StringWriter
import java.text.SimpleDateFormat
import java.util.*

object DFSLogger {

    private val LINE_BR = System.getProperty("line.separator")?.toString() ?: "\n"
    private val formatter = SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.CHINA)
    private const val consoleMaxLen = 3584 // 1024 * 3
    private val gson = Gson()

    fun log(type: DFSLogType, vararg contents: Any?) {
        val string = when (contents.size) {
            0 -> ""
            1 -> stringify(contents[0])
            else -> {
                contents.mapIndexed { index, obj ->
                    "[$index] = ${stringify(obj)}"
                }.joinToString(LINE_BR)
            }
        }
        if (DFSLog.console) {
            if (string.length > consoleMaxLen) {
                var content = string
                while (content.length > consoleMaxLen) {
                    console(type, content.substring(0 until consoleMaxLen))
                    content = content.substring(consoleMaxLen)
                }
                console(type, content)
            } else {
                console(type, string)
            }
        }
        if (DFSLog.file) file(type, string)
    }

    private fun file(type: DFSLogType, content: String) {
        ThreadUtil.log {
            val today = Date()
            val format = formatter.format(today)
            val date = format.substring(0..9)
            val time = format.substring(11)
            val folder = File(DFSLog.folder)
            if (!folder.exists() || !folder.isDirectory) folder.mkdirs()
            val mFile = File("${DFSLog.folder}/${date}")
            if (!mFile.isFile) {
                mFile.createNewFile()
                mFile.writeText(
                    "************* Log Info ****************$LINE_BR" +
                            "Date: $date$LINE_BR" +
                            "Device Manufacturer: ${Build.MANUFACTURER}$LINE_BR" +
                            "Android Version: ${Build.VERSION.RELEASE}$LINE_BR" +
                            "Android SDK: ${Build.VERSION.SDK_INT}$LINE_BR" +
                            "************* Log Info ****************$LINE_BR$LINE_BR"
                )
                // delete old file
                val dateOld = formatter.format(Date(today.time - 86400000L * DFSLog.saveDays)).substring(0..9)
                val oldFile = File("${DFSLog.folder}/${dateOld}")
                oldFile.deleteOnExit()
            }
            mFile.appendText("$time [${type.value}] $content$LINE_BR")
        }
    }

    private fun console(type: DFSLogType, content: String) {
        when (type) {
            DFSLogType.Info -> Log.i(DFSLog.TAG, content)
            DFSLogType.Warn -> Log.w(DFSLog.TAG, content)
            DFSLogType.Error -> Log.e(DFSLog.TAG, content)
            DFSLogType.Verbose -> Log.v(DFSLog.TAG, content)
            else -> Log.d(DFSLog.TAG, content)
        }
    }

    private fun stringify(content: Any?): String = when (content) {
        is String -> content
        is Iterable<*> -> content.joinToString { stringify(it) }
        is Throwable -> getFullStackTrace(content)
        else -> gson.toJson(content)
    }

    private fun getFullStackTrace(throwable: Throwable): String {
        var insideThrowable: Throwable? = throwable
        val throwableList: MutableList<Throwable> = ArrayList()
        while (insideThrowable != null && !throwableList.contains(insideThrowable)) {
            throwableList.add(insideThrowable)
            insideThrowable = insideThrowable.cause
        }
        val size = throwableList.size
        val frames: MutableList<String> = ArrayList()
        var nextTrace: MutableList<String> = getStackFrameList(
            throwableList[size - 1]
        )
        var i = size
        while (--i >= 0) {
            val trace = nextTrace
            if (i != 0) {
                nextTrace = getStackFrameList(throwableList[i - 1])
                removeCommonFrames(trace, nextTrace)
            }
            if (i == size - 1) {
                frames.add(throwableList[i].toString())
            } else {
                frames.add(" Caused by: " + throwableList[i].toString())
            }
            frames.addAll(trace)
        }
        val sb = StringBuilder()
        for (element in frames) {
            sb.append(element).append(LINE_BR)
        }
        return sb.toString()
    }

    private fun getStackFrameList(throwable: Throwable): MutableList<String> {
        val sw = StringWriter()
        val pw = PrintWriter(sw, true)
        throwable.printStackTrace(pw)
        val stackTrace = sw.toString()
        val frames = StringTokenizer(stackTrace, LINE_BR)
        val list: MutableList<String> = ArrayList()
        var traceStarted = false
        while (frames.hasMoreTokens()) {
            val token = frames.nextToken()
            // Determine if the line starts with <whitespace>at
            val at = token.indexOf("at")
            if (at != -1 && token.substring(0, at).trim { it <= ' ' }.isEmpty()) {
                traceStarted = true
                list.add(token)
            } else if (traceStarted) {
                break
            }
        }
        return list
    }

    private fun removeCommonFrames(causeFrames: MutableList<String>, wrapperFrames: List<String>) {
        var causeFrameIndex = causeFrames.size - 1
        var wrapperFrameIndex = wrapperFrames.size - 1
        while (causeFrameIndex >= 0 && wrapperFrameIndex >= 0) {
            // Remove the frame from the cause trace if it is the same
            // as in the wrapper trace
            val causeFrame = causeFrames[causeFrameIndex]
            val wrapperFrame = wrapperFrames[wrapperFrameIndex]
            if (causeFrame == wrapperFrame) {
                causeFrames.removeAt(causeFrameIndex)
            }
            causeFrameIndex--
            wrapperFrameIndex--
        }
    }
}