package net.mamoe.mirai.console

import com.googlecode.lanterna.SGR
import com.googlecode.lanterna.TerminalSize
import com.googlecode.lanterna.TextColor
import com.googlecode.lanterna.graphics.TextGraphics
import com.googlecode.lanterna.input.KeyStroke
import com.googlecode.lanterna.input.KeyType
import com.googlecode.lanterna.terminal.DefaultTerminalFactory
import com.googlecode.lanterna.terminal.Terminal
import com.googlecode.lanterna.terminal.TerminalResizeListener
import com.googlecode.lanterna.terminal.swing.SwingTerminal
import com.googlecode.lanterna.terminal.swing.SwingTerminalFontConfiguration
import com.googlecode.lanterna.terminal.swing.SwingTerminalFrame
import kotlinx.coroutines.*
import kotlinx.coroutines.io.close
import kotlinx.io.core.IoBuffer
import kotlinx.io.core.use
import net.mamoe.mirai.Bot
import net.mamoe.mirai.console.MiraiConsoleTerminalUI.LoggerDrawer.cleanPage
import net.mamoe.mirai.console.MiraiConsoleTerminalUI.LoggerDrawer.drawLog
import net.mamoe.mirai.console.MiraiConsoleTerminalUI.LoggerDrawer.redrawLogs
import net.mamoe.mirai.utils.LoginSolver
import net.mamoe.mirai.utils.createCharImg
import net.mamoe.mirai.utils.writeChannel
import java.awt.Font
import java.io.File
import java.io.OutputStream
import java.io.PrintStream
import java.nio.charset.Charset
import java.util.*
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.ConcurrentLinkedDeque
import javax.imageio.ImageIO
import kotlin.concurrent.thread
import kotlin.system.exitProcess

/**
 * 此文件不推荐任何人看
 * 可能导致
 *  1：心肌梗死
 *  2：呼吸困难
 *  3：想要重写但是发现改任何一个看似不合理的地方都会崩
 *
 * @author NaturalHG
 *
 */

fun String.actualLength(): Int {
    var x = 0
    this.forEach {
        if (it.isChineseChar()) {
            x += 2
        } else {
            x += 1
        }
    }
    return x
}

fun String.getSubStringIndexByActualLength(widthMax: Int): Int {
    var index = 0
    var currentLength = 0
    this.forEach {
        if (it.isChineseChar()) {
            currentLength += 2
        } else {
            currentLength += 1
        }
        if (currentLength > widthMax) {
            return@forEach
        }
        ++index
    }
    if (index < 2) {
        index = 2
    }
    return index
}

fun Char.isChineseChar(): Boolean {
    return this.toString().isChineseChar()
}

fun String.isChineseChar(): Boolean {
    return this.matches(Regex("[\u4e00-\u9fa5]"))
}


object MiraiConsoleTerminalUI : MiraiConsoleUI {
    val cacheLogSize = 50
    var mainTitle = "Mirai Console v0.01 Core v0.15"

    override fun pushVersion(consoleVersion: String, consoleBuild: String, coreVersion: String) {
        mainTitle = "Mirai Console(Terminal) $consoleVersion $consoleBuild Core $coreVersion"
    }

    override fun pushLog(identity: Long, message: String) {
        log[identity]!!.push(message)
        if (identity == screens[currentScreenId]) {
            drawLog(message)
        }
    }

    override fun prePushBot(identity: Long) {
        log[identity] = LimitLinkedQueue(cacheLogSize)
    }

    override fun pushBot(bot: Bot) {
        botAdminCount[bot.uin] = 0
        screens.add(bot.uin)
        drawFrame(this.getScreenName(currentScreenId))
        if (terminal is SwingTerminalFrame) {
            terminal.flush()
        }
    }

    var requesting = false
    var requestResult: String? = null
    override suspend fun requestInput(question: String): String {
        requesting = true
        while (requesting) {
            delay(100)//不然会卡死 迷惑吧
        }
        return requestResult!!
    }


    fun provideInput(input: String) {
        if (requesting) {
            requestResult = input
            requesting = false
        } else {
            MiraiConsole.CommandListener.commandChannel.offer(
                commandBuilder.toString()
            )
        }
    }


    override fun pushBotAdminStatus(identity: Long, admins: List<Long>) {
        botAdminCount[identity] = admins.size
    }

    override fun createLoginSolver(): LoginSolver {
        return object : LoginSolver() {
            override suspend fun onSolvePicCaptcha(bot: Bot, data: IoBuffer): String? {
                val tempFile: File = createTempFile(suffix = ".png").apply { deleteOnExit() }
                withContext(Dispatchers.IO) {
                    tempFile.createNewFile()
                    pushLog(0, "[Login Solver]需要图片验证码登录, 验证码为 4 字母")
                    try {
                        tempFile.writeChannel().apply {
                            writeFully(data)
                            close()
                        }
                        pushLog(0, "请查看文件 ${tempFile.absolutePath}")
                    } catch (e: Exception) {
                        error("[Login Solver]验证码无法保存[Error0001]")
                    }
                }

                var toLog = ""
                tempFile.inputStream().use {
                    val img = ImageIO.read(it)
                    if (img == null) {
                        toLog += "无法创建字符图片. 请查看文件\n"
                    } else {
                        toLog += img.createCharImg((terminal.terminalSize.columns / 1.5).toInt())
                    }
                }
                pushLog(0, "$toLog[Login Solver]请输验证码. ${tempFile.absolutePath}")
                return requestInput("[Login Solver]请输入 4 位字母验证码. 若要更换验证码, 请直接回车")!!
                    .takeUnless { it.isEmpty() || it.length != 4 }
                    .also {
                        pushLog(0, "[Login Solver]正在提交[$it]中...")
                    }
            }

            override suspend fun onSolveSliderCaptcha(bot: Bot, url: String): String? {
                pushLog(0, "[Login Solver]需要滑动验证码")
                pushLog(0, "[Login Solver]请在任意浏览器中打开以下链接并完成验证码. ")
                pushLog(0, "[Login Solver]完成后请输入任意字符 ")
                pushLog(0, url)
                return requestInput("[Login Solver]完成后请输入任意字符").also {
                    pushLog(0, "[Login Solver]正在提交中")
                }
            }

            override suspend fun onSolveUnsafeDeviceLoginVerify(bot: Bot, url: String): String? {
                pushLog(0, "[Login Solver]需要进行账户安全认证")
                pushLog(0, "[Login Solver]该账户有[设备锁]/[不常用登陆地点]/[不常用设备登陆]的问题")
                pushLog(0, "[Login Solver]完成以下账号认证即可成功登陆|理论本认证在mirai每个账户中最多出现1次")
                pushLog(0, "[Login Solver]请将该链接在QQ浏览器中打开并完成认证, 成功后输入任意字符")
                pushLog(0, "[Login Solver]这步操作将在后续的版本中优化")
                pushLog(0, url)
                return requestInput("[Login Solver]完成后请输入任意字符").also {
                    pushLog(0, "[Login Solver]正在提交中...")
                }
            }

        }
    }

    val log = ConcurrentHashMap<Long, LimitLinkedQueue<String>>().also {
        it[0L] = LimitLinkedQueue(cacheLogSize)
    }

    val botAdminCount = ConcurrentHashMap<Long, Int>()

    private val screens = mutableListOf(0L)
    private var currentScreenId = 0


    lateinit var terminal: Terminal
    lateinit var textGraphics: TextGraphics

    var hasStart = false
    private lateinit var internalPrinter: PrintStream
    fun start() {
        if (hasStart) {
            return
        }

        internalPrinter = System.out


        hasStart = true
        val defaultTerminalFactory = DefaultTerminalFactory(internalPrinter, System.`in`, Charset.defaultCharset())

        val fontSize = 12
        defaultTerminalFactory
            .setInitialTerminalSize(
                TerminalSize(
                    101, 60
                )
            )
            .setTerminalEmulatorFontConfiguration(
                SwingTerminalFontConfiguration.newInstance(
                    Font("Monospaced", Font.PLAIN, fontSize)
                )
            )
        try {
            terminal = defaultTerminalFactory.createTerminal()
            terminal.enterPrivateMode()
            terminal.clearScreen()
            terminal.setCursorVisible(false)
        } catch (e: Exception) {
            try {
                terminal = SwingTerminalFrame("Mirai Console")
                terminal.enterPrivateMode()
                terminal.clearScreen()
                terminal.setCursorVisible(false)
            } catch (e: Exception) {
                error("can not create terminal")
            }
        }
        textGraphics = terminal.newTextGraphics()

        /*
        var lastRedrawTime = 0L
        var lastNewWidth = 0
        var lastNewHeight = 0

        terminal.addResizeListener(TerminalResizeListener { terminal1: Terminal, newSize: TerminalSize ->
            try {
                if (lastNewHeight == newSize.rows
                    &&
                    lastNewWidth == newSize.columns
                ) {
                    return@TerminalResizeListener
                }
                lastNewHeight = newSize.rows
                lastNewWidth = newSize.columns
                terminal.clearScreen()
                if(terminal !is SwingTerminalFrame) {
                    Thread.sleep(300)
                }
                update()
                redrawCommand()
                redrawLogs(log[screens[currentScreenId]]!!)
            }catch (ignored:Exception){

            }
        })

       */
        var lastJob: Job? = null
        terminal.addResizeListener(TerminalResizeListener { terminal1: Terminal, newSize: TerminalSize ->
            lastJob = GlobalScope.launch {
                try {
                    delay(300)
                    if (lastJob == coroutineContext[Job]) {
                        terminal.clearScreen()
                        //inited = false
                        update()
                        redrawCommand()
                        redrawLogs(log[screens[currentScreenId]]!!)
                    }
                } catch (e: Exception) {
                    pushLog(0, "[UI ERROR] ${e.message}")
                }
            }
        })

        if (terminal !is SwingTerminalFrame) {
            System.setOut(PrintStream(object : OutputStream() {
                var builder = java.lang.StringBuilder()
                override fun write(b: Int) {
                    with(b.toChar()) {
                        if (this == '\n') {
                            pushLog(0, builder.toString())
                            builder = java.lang.StringBuilder()
                        } else {
                            builder.append(this)
                        }
                    }
                }
            }))
        }

        System.setErr(System.out)

        try {
            update()
        } catch (e: Exception) {
            pushLog(0, "[UI ERROR] ${e.message}")
        }

        val charList = listOf(',', '.', '/', '@', '#', '$', '%', '^', '&', '*', '(', ')', '_', '=', '+', '!', ' ')
        thread {
            while (true) {
                try {
                    var keyStroke: KeyStroke = terminal.readInput()

                    when (keyStroke.keyType) {
                        KeyType.ArrowLeft -> {
                            currentScreenId =
                                getLeftScreenId()
                            clearRows(2)
                            cleanPage()
                            update()
                        }
                        KeyType.ArrowRight -> {
                            currentScreenId =
                                getRightScreenId()
                            clearRows(2)
                            cleanPage()
                            update()
                        }
                        KeyType.Enter -> {
                            provideInput(commandBuilder.toString())
                            emptyCommand()
                        }
                        KeyType.Escape -> {
                            exitProcess(0)
                        }
                        else -> {
                            if (keyStroke.character != null) {
                                if (keyStroke.character.toInt() == 8) {
                                    deleteCommandChar()
                                }
                                if (keyStroke.character.isLetterOrDigit() || charList.contains(keyStroke.character)) {
                                    addCommandChar(keyStroke.character)
                                }
                            }
                        }
                    }
                } catch (e: Exception) {
                    pushLog(0, "[UI ERROR] ${e.message}")
                }
            }
        }
    }

    private fun getLeftScreenId(): Int {
        var newId = currentScreenId - 1
        if (newId < 0) {
            newId = screens.size - 1
        }
        return newId
    }

    private fun getRightScreenId(): Int {
        var newId = 1 + currentScreenId
        if (newId >= screens.size) {
            newId = 0
        }
        return newId
    }

    private fun getScreenName(id: Int): String {
        return when (screens[id]) {
            0L -> {
                "Console Screen"
            }
            else -> {
                "Bot: ${screens[id]}"
            }
        }
    }


    fun clearRows(row: Int) {
        textGraphics.putString(
            0, row, " ".repeat(
                terminal.terminalSize.columns
            )
        )
    }

    fun drawFrame(
        title: String
    ) {
        val width = terminal.terminalSize.columns
        val height = terminal.terminalSize.rows
        terminal.setBackgroundColor(TextColor.ANSI.DEFAULT)

        textGraphics.foregroundColor = TextColor.ANSI.WHITE
        textGraphics.backgroundColor = TextColor.ANSI.GREEN
        textGraphics.putString((width - mainTitle.actualLength()) / 2, 1, mainTitle, SGR.BOLD)
        textGraphics.foregroundColor = TextColor.ANSI.DEFAULT
        textGraphics.backgroundColor = TextColor.ANSI.DEFAULT
        textGraphics.putString(2, 3, "-".repeat(width - 4))
        textGraphics.putString(2, 5, "-".repeat(width - 4))
        textGraphics.putString(2, height - 4, "-".repeat(width - 4))
        textGraphics.putString(2, height - 3, "|>>>")
        textGraphics.putString(width - 3, height - 3, "|")
        textGraphics.putString(2, height - 2, "-".repeat(width - 4))

        textGraphics.foregroundColor = TextColor.ANSI.DEFAULT
        textGraphics.backgroundColor = TextColor.ANSI.DEFAULT
        val leftName =
            getScreenName(getLeftScreenId())
        // clearRows(2)
        textGraphics.putString((width - title.actualLength()) / 2 - "$leftName << ".length, 2, "$leftName << ")
        textGraphics.foregroundColor = TextColor.ANSI.WHITE
        textGraphics.backgroundColor = TextColor.ANSI.YELLOW
        textGraphics.putString((width - title.actualLength()) / 2, 2, title, SGR.BOLD)
        textGraphics.foregroundColor = TextColor.ANSI.DEFAULT
        textGraphics.backgroundColor = TextColor.ANSI.DEFAULT
        val rightName =
            getScreenName(getRightScreenId())
        textGraphics.putString((width + title.actualLength()) / 2 + 1, 2, ">> $rightName")
    }

    fun drawMainFrame(
        onlineBotCount: Number
    ) {
        drawFrame("Console Screen")
        val width = terminal.terminalSize.columns
        textGraphics.foregroundColor = TextColor.ANSI.DEFAULT
        textGraphics.backgroundColor = TextColor.ANSI.DEFAULT
        clearRows(4)
        textGraphics.putString(2, 4, "|Online Bots: $onlineBotCount")
        textGraphics.putString(
            width - 2 - "Powered By Mamoe Technologies|".actualLength(),
            4,
            "Powered By Mamoe Technologies|"
        )
    }

    fun drawBotFrame(
        qq: Long,
        adminCount: Number
    ) {
        drawFrame("Bot: $qq")
        val width = terminal.terminalSize.columns
        textGraphics.foregroundColor = TextColor.ANSI.DEFAULT
        textGraphics.backgroundColor = TextColor.ANSI.DEFAULT
        clearRows(4)
        textGraphics.putString(2, 4, "|Admins: $adminCount")
        textGraphics.putString(width - 2 - "Add admins via commands|".actualLength(), 4, "Add admins via commands|")
    }


    object LoggerDrawer {
        var currentHeight = 6

        fun drawLog(string: String, flush: Boolean = true) {
            val maxHeight = terminal.terminalSize.rows - 4
            val heightNeed = (string.actualLength() / (terminal.terminalSize.columns - 6)) + 1
            if (heightNeed - 1 > maxHeight) {
                pushLog(0, "[UI ERROR]: 您的屏幕太小, 有一条超长LOG无法显示")
                return//拒绝打印
            }
            if (currentHeight + heightNeed > maxHeight) {
                cleanPage()//翻页
            }
            if (string.contains("\n")) {
                string.split("\n").forEach {
                    drawLog(string, false)
                }
            } else {
                val width = terminal.terminalSize.columns - 6
                var x = string
                while (true) {
                    if (x == "") break
                    val toWrite = if (x.actualLength() > width) {
                        val index = x.getSubStringIndexByActualLength(width)
                        x.substring(0, index).also {
                            x = if (index < x.length) {
                                x.substring(index)
                            } else {
                                ""
                            }
                        }
                    } else {
                        x.also {
                            x = ""
                        }
                    }
                    try {
                        textGraphics.foregroundColor = TextColor.ANSI.GREEN
                        textGraphics.backgroundColor = TextColor.ANSI.DEFAULT
                        textGraphics.putString(
                            3,
                            currentHeight, toWrite, SGR.ITALIC
                        )
                    } catch (ignored: Exception) {
                        //
                    }
                    ++currentHeight
                }
            }
            if (flush && terminal is SwingTerminalFrame) {
                terminal.flush()
            }
        }


        fun cleanPage() {
            for (index in 6 until terminal.terminalSize.rows - 4) {
                clearRows(index)
            }
            currentHeight = 6
        }


        fun redrawLogs(toDraw: Queue<String>) {
            //this.cleanPage()
            currentHeight = 6
            var logsToDraw = 0
            var vara = 0
            val toPrint = mutableListOf<String>()
            toDraw.forEach {
                val heightNeed = (it.actualLength() / (terminal.terminalSize.columns - 6)) + 1
                vara += heightNeed
                if (currentHeight + vara < terminal.terminalSize.rows - 4) {
                    logsToDraw++
                    toPrint.add(it)
                } else {
                    return@forEach
                }
            }
            toPrint.reversed().forEach {
                drawLog(it, false)
            }
            if (terminal is SwingTerminalFrame) {
                terminal.flush()
            }
        }
    }


    var commandBuilder = StringBuilder()
    fun redrawCommand() {
        val height = terminal.terminalSize.rows
        val width = terminal.terminalSize.columns
        clearRows(height - 3)
        textGraphics.foregroundColor = TextColor.ANSI.DEFAULT
        textGraphics.putString(2, height - 3, "|>>>")
        textGraphics.putString(width - 3, height - 3, "|")
        textGraphics.foregroundColor = TextColor.ANSI.WHITE
        textGraphics.backgroundColor = TextColor.ANSI.BLACK
        textGraphics.putString(7, height - 3, commandBuilder.toString())
        if (terminal is SwingTerminalFrame) {
            terminal.flush()
        }
        textGraphics.backgroundColor = TextColor.ANSI.DEFAULT
    }

    private fun addCommandChar(
        c: Char
    ) {
        if (!requesting && commandBuilder.isEmpty() && c != '/') {
            addCommandChar('/')
        }
        textGraphics.foregroundColor = TextColor.ANSI.WHITE
        textGraphics.backgroundColor = TextColor.ANSI.BLACK
        val height = terminal.terminalSize.rows
        commandBuilder.append(c)
        if (terminal is SwingTerminalFrame) {
            redrawCommand()
        } else {
            textGraphics.putString(6 + commandBuilder.length, height - 3, c.toString())
        }
        textGraphics.backgroundColor = TextColor.ANSI.DEFAULT
    }

    private fun deleteCommandChar() {
        if (!commandBuilder.isEmpty()) {
            commandBuilder = StringBuilder(commandBuilder.toString().substring(0, commandBuilder.length - 1))
        }
        val height = terminal.terminalSize.rows
        if (terminal is SwingTerminalFrame) {
            redrawCommand()
        } else {
            textGraphics.putString(7 + commandBuilder.length, height - 3, " ")
        }
    }


    var lastEmpty: Job? = null
    private fun emptyCommand() {
        commandBuilder = StringBuilder()
        if (terminal is SwingTerminal) {
            redrawCommand()
            terminal.flush()
        } else {
            lastEmpty = GlobalScope.launch {
                try {
                    delay(100)
                    if (lastEmpty == coroutineContext[Job]) {
                        terminal.clearScreen()
                        //inited = false
                        update()
                        redrawCommand()
                        redrawLogs(log[screens[currentScreenId]]!!)
                    }
                } catch (e: Exception) {
                    pushLog(0, "[UI ERROR] ${e.message}")
                }
            }
        }
    }

    fun update() {
        when (screens[currentScreenId]) {
            0L -> {
                drawMainFrame(screens.size - 1)
            }
            else -> {
                drawBotFrame(
                    screens[currentScreenId],
                    0
                )
            }
        }
        redrawLogs(log[screens[currentScreenId]]!!)
    }
}


class LimitLinkedQueue<T>(
    val limit: Int = 50
) : ConcurrentLinkedDeque<T>() {
    override fun push(e: T) {
        if (size >= limit) {
            this.pollLast()
        }
        return super.push(e)
    }
}
