package net.jselby.kotgb

import com.badlogic.gdx.ApplicationAdapter
import com.badlogic.gdx.Files
import com.badlogic.gdx.Gdx
import com.badlogic.gdx.Input
import com.badlogic.gdx.files.FileHandle
import com.badlogic.gdx.graphics.GL20
import com.badlogic.gdx.graphics.Pixmap
import com.badlogic.gdx.graphics.Texture
import com.badlogic.gdx.graphics.g2d.SpriteBatch
import com.badlogic.gdx.scenes.scene2d.Actor
import com.badlogic.gdx.scenes.scene2d.Stage
import com.badlogic.gdx.scenes.scene2d.ui.Table
import com.badlogic.gdx.scenes.scene2d.utils.ChangeListener
import com.badlogic.gdx.utils.Array
import com.badlogic.gdx.utils.BufferUtils
import com.badlogic.gdx.utils.viewport.ScreenViewport
import com.kotcrab.vis.ui.VisUI
import com.kotcrab.vis.ui.util.dialog.Dialogs
import com.kotcrab.vis.ui.widget.Menu
import com.kotcrab.vis.ui.widget.MenuBar
import com.kotcrab.vis.ui.widget.MenuItem
import com.kotcrab.vis.ui.widget.file.FileChooser
import com.kotcrab.vis.ui.widget.file.FileChooserListener
import com.kotcrab.vis.ui.widget.file.FileTypeFilter
import mu.KotlinLogging
import net.jselby.kotgb.hw.GameboyButton
import net.jselby.kotgb.ui.*
import java.io.File
import java.io.PrintWriter
import java.io.StringWriter
import java.nio.ByteBuffer
import java.util.*

class KotGB(val args: MutableMap<String, Any>, val emuEnv: EmulationEnvironment) : ApplicationAdapter() {
    private val logger = KotlinLogging.logger {}

    private var batch: SpriteBatch? = null
    private var texture: Texture? = null
    private var buf: ByteBuffer? = null
    private var renderWidth: Int = 0
    private var renderHeight: Int = 0

    private val buttons = ArrayList<GameboyButton>()

    private var renderUI = true
    private var stage: Stage? = null
    private var menuBar: MenuBar? = null

    private lateinit var state : StateManager

    private var regViewer : RegisterViewer? = null
    private var ramViewer : RamViewer? = null
    private var frameTimer : FrametimeGraph? = null
    private var breakpointViewer: BreakpointsViewer? = null
    private var vramViewer : VramDumper? = null
    private var frameTimerEnabled = false


    override fun create() {
        logger.info("Running via LibGDX Frontend on ${Gdx.app.type}")

        renderWidth = Gdx.graphics.width
        renderHeight = Gdx.graphics.height

        state = StateManager({ des, e -> showError(des, e) }, emuEnv.cpuFactory)

        batch = SpriteBatch()
        VisUI.load(VisUI.SkinScale.X1)

        stage = Stage(ScreenViewport())
        val root = Table()
        root.setFillParent(true)
        stage!!.addActor(root)

        Gdx.input.inputProcessor = stage

        menuBar = MenuBar()
        root.add(menuBar!!.table).expandX().fillX().row()
        root.add().expand().fill()

        val fileMenu = Menu("File")

        FileChooser.setDefaultPrefsName("net.jselby.kotgb.filechooser")
        fileMenu.addItem(MenuItem("Load...", object : ChangeListener() {
            override fun changed(event: ChangeListener.ChangeEvent, actor: Actor) {
                /*val chooser = FileChooser(FileChooser.Mode.OPEN)
                val filter = FileTypeFilter(true)
                filter.addRule("Gameboy ROMs (*.gb)", ".gb")
                chooser.setFileTypeFilter(filter)
                chooser.isMultiSelectionEnabled = false
                chooser.setDirectory(File(""))
                chooser.setListener(object : FileChooserListener {
                    override fun selected(files: Array<FileHandle>) {
                        if (files.size != 1) {
                            logger.error {"Invalid ROM file array."}
                            return
                        }

                        loadRom(files.get(0))
                    }

                    override fun canceled() {
                    }
                })
                stage!!.addActor(chooser.fadeIn())*/
                emuEnv.openFile {
                    loadRom(it)
                }
            }
        }))
        fileMenu.addSeparator()
        fileMenu.addItem(MenuItem("Close", object : ChangeListener() {
            override fun changed(event: ChangeListener.ChangeEvent, actor: Actor) {
                Gdx.app.exit()
            }
        }))

        val emulationMenu = Menu("Emulation")
        emulationMenu.addItem(MenuItem("Resume", object : ChangeListener() {
            override fun changed(event: ChangeListener.ChangeEvent, actor: Actor) {
                state.resume()
            }
        }))
        emulationMenu.addItem(MenuItem("Pause", object : ChangeListener() {
            override fun changed(event: ChangeListener.ChangeEvent, actor: Actor) {
                state.pause()
            }
        }))
        emulationMenu.addItem(MenuItem("Stop", object : ChangeListener() {
            override fun changed(event: ChangeListener.ChangeEvent, actor: Actor) {
                state.stop()
            }
        }))
        emulationMenu.addItem(MenuItem("Step (F7)", object : ChangeListener() {
            override fun changed(event: ChangeListener.ChangeEvent, actor: Actor) {
                state.step()
            }
        }))

        val debugMenu = Menu("Debug")
        debugMenu.addItem(MenuItem("Registers", object : ChangeListener() {
            override fun changed(event: ChangeListener.ChangeEvent, actor: Actor) {
                actor as MenuItem

                if (actor.text.contains("-")) {
                    regViewer!!.fadeOut()
                    actor.text = actor.text.toString().replace("- ", "")
                } else {
                    actor.text = "- ${actor.text}"
                    stage!!.addActor(regViewer)
                }
            }
        }))
        debugMenu.addItem(MenuItem("RAM Viewer", object : ChangeListener() {
            override fun changed(event: ChangeListener.ChangeEvent, actor: Actor) {
                actor as MenuItem

                if (actor.text.contains("-")) {
                    ramViewer!!.fadeOut()
                    actor.text = actor.text.toString().replace("- ", "")
                } else {
                    actor.text = "- ${actor.text}"
                    stage!!.addActor(ramViewer)
                }
            }
        }))
        debugMenu.addItem(MenuItem("Breakpoints", object : ChangeListener() {
            override fun changed(event: ChangeListener.ChangeEvent, actor: Actor) {
                actor as MenuItem

                if (actor.text.contains("-")) {
                    breakpointViewer!!.fadeOut()
                    actor.text = actor.text.toString().replace("- ", "")
                } else {
                    actor.text = "- ${actor.text}"
                    stage!!.addActor(breakpointViewer)
                }
            }
        }))
        debugMenu.addItem(MenuItem("Frametimes", object : ChangeListener() {
            override fun changed(event: ChangeListener.ChangeEvent, actor: Actor) {
                actor as MenuItem

                if (actor.text.contains("-")) {
                    actor.text = actor.text.toString().replace("- ", "")
                    frameTimerEnabled = false
                } else {
                    actor.text = "- ${actor.text}"
                    frameTimerEnabled = true
                }
            }
        }))
        debugMenu.addItem(MenuItem("VRAM", object : ChangeListener() {
            override fun changed(event: ChangeListener.ChangeEvent, actor: Actor) {
                actor as MenuItem

                if (actor.text.contains("-")) {
                    actor.text = actor.text.toString().replace("- ", "")
                    vramViewer!!.fadeOut()
                } else {
                    actor.text = "- ${actor.text}"
                    stage!!.addActor(vramViewer)
                }
            }
        }))

        val viewMenu = Menu("View")
        viewMenu.addItem(MenuItem("Hide UI", object : ChangeListener() {
            override fun changed(event: ChangeListener.ChangeEvent, actor: Actor) {
                renderUI = false
            }
        }))
        viewMenu.addItem(MenuItem("Fullscreen", object : ChangeListener() {
            override fun changed(event: ChangeListener.ChangeEvent, actor: Actor) {
                actor as MenuItem

                if (actor.text.contains("-")) {
                    actor.text = actor.text.toString().replace("- ", "")
                    Gdx.graphics.setWindowedMode(800, 600)
                } else {
                    actor.text = "- ${actor.text}"
                    Gdx.graphics.setFullscreenMode(Gdx.graphics.displayMode)
                }
            }
        }))

        menuBar!!.addMenu(fileMenu)
        menuBar!!.addMenu(emulationMenu)
        menuBar!!.addMenu(debugMenu)
        menuBar!!.addMenu(viewMenu)

        frameTimer = FrametimeGraph()
        regViewer = RegisterViewer()
        ramViewer = RamViewer()
        breakpointViewer = BreakpointsViewer()
        vramViewer = VramDumper()

        if (args.containsKey("debug") && args["debug"] != null && args["debug"] as Boolean) {
            stage!!.addActor(regViewer)
            stage!!.addActor(ramViewer)
            stage!!.addActor(breakpointViewer)
        }

        if (args.containsKey("breakpoints")) {
            for (arg in args["breakpoints"]!! as ArrayList<*>) {
                logger.debug {"Adding breakpoint: $arg"}
                breakpointViewer!!.addBreakpoint(arg as String)
            }
        }

        buf = BufferUtils.newByteBuffer(160 * 144 * 4)
        texture = Texture(160, 144, Pixmap.Format.RGBA8888)

        if (args.containsKey("file") && args["file"] != null) {
            loadRom(Gdx.files.getFileHandle((args["file"]!! as ArrayList<*>)[0] as String, Files.FileType.Local))
        }
    }

    private fun showError(description: String, e: Exception) {
        val sw = StringWriter()
        val pw = PrintWriter(sw)
        e.printStackTrace(pw)
        val exceptionText = sw.toString().replace("\t", "    ")

        val dialog = Dialogs.showErrorDialog(stage!!, description, exceptionText)
        dialog.isDetailsVisible = true
    }

    private fun loadRom(file: FileHandle) {
        if (state.isRunning) {
            state.stop()
        }

        try {
            state.launch(file)
        } catch (e : Exception) {
            e.printStackTrace()
            showError("Failed to load ROM.", e)
            return
        }

        breakpointViewer?.update(state.gameboy!!)

        state.resume()
    }

    override fun render() {
        stage!!.act(Math.min(Gdx.graphics.deltaTime, 1 / 30f))

        Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT)

        var title = "KotGB"

        if (state.isGameLoaded) {
            state.withGPU {
                BufferUtils.copy(it.pixelArray, 0,
                        buf!!, it.pixelArray.size)

                if (frameTimerEnabled) {
                    frameTimer?.update(state)
                }
                if (regViewer?.isVisible!!) {
                    regViewer?.update(state.gameboy!!)
                }
                if (ramViewer?.isVisible!!) {
                    ramViewer?.update(state.gameboy!!)
                }
                if (breakpointViewer?.isVisible!!) {
                    breakpointViewer?.update(state.gameboy!!)
                }
                /*if (vramViewer?.isVisible!!) {
                    vramViewer?.update(state.gameboy!!)
                }*/
            }

            synchronized(state.vsyncSignal, {
                state.vsyncSignal.notifyAll()
            })

            // Upload texture
            texture!!.bind()
            Gdx.gl.glTexImage2D(GL20.GL_TEXTURE_2D, 0, GL20.GL_RGBA, 160, 144,
                    0, GL20.GL_RGBA, GL20.GL_UNSIGNED_BYTE, buf)

            // Render texture
            batch!!.transformMatrix = stage!!.viewport.camera.view
            batch!!.projectionMatrix = stage!!.viewport.camera.projection
            batch!!.begin()
            batch!!.draw(texture, 0f, 0f, renderWidth.toFloat(),
                    renderHeight - if (renderUI) menuBar!!.table.height else 0f)

            if (frameTimerEnabled) {
                frameTimer?.render(batch!!)
            }
            batch!!.end()

            // Update input
            buttons.clear()

            val toCollect = intArrayOf(Input.Keys.UP, Input.Keys.DOWN, Input.Keys.LEFT, Input.Keys.RIGHT,
                    Input.Keys.X, Input.Keys.Z, Input.Keys.A, Input.Keys.S)

            toCollect
                    .filter { Gdx.input.isKeyPressed(it) }
                    .forEach { // Translate
                        when (it) {
                            Input.Keys.UP -> buttons.add(GameboyButton.UP)
                            Input.Keys.DOWN -> buttons.add(GameboyButton.DOWN)
                            Input.Keys.LEFT -> buttons.add(GameboyButton.LEFT)
                            Input.Keys.RIGHT -> buttons.add(GameboyButton.RIGHT)
                            Input.Keys.X -> buttons.add(GameboyButton.A)
                            Input.Keys.Z -> buttons.add(GameboyButton.B)
                            Input.Keys.A -> buttons.add(GameboyButton.SELECT)
                            Input.Keys.S -> buttons.add(GameboyButton.START)
                        }
                    }

            state.buttonsPressed = buttons.toTypedArray()

            state.doFastForward = Gdx.input.isKeyPressed(Input.Keys.TAB)

            title += (" - Emulating ${state.gameName}" + if (!state.isRunning) " - " + if (state.isDebugging) "Debugging" else "Paused" else "" )

            val gameboy = state.gameboy
            if (gameboy != null) {
                title += " - Using " + gameboy.cpu.executor::class.java.simpleName.replace("Executor", "").trim()

                val state = emuEnv.cpuState.invoke(gameboy.cpu)

                if (state != null) {
                    title += " ($state)"
                }
            }
        }

        // Handle hotkeys
        if (Gdx.input.isKeyJustPressed(Input.Keys.ESCAPE)) {
            renderUI = true
        }
        if (Gdx.input.isKeyJustPressed(Input.Keys.F7)) {
            state.step()
        }

        title += " - Frontend FPS: ${Gdx.graphics.framesPerSecond}"

        Gdx.graphics.setTitle(title)

        if (renderUI) {
            stage!!.draw()
        }
    }

    override fun resize(width: Int, height: Int) {
        if (width == 0 && height == 0) return  //see https://github.com/libgdx/libgdx/issues/3673#issuecomment-177606278
        stage!!.viewport.update(width, height, true)
        renderWidth = width
        renderHeight = height
    }

    override fun dispose() {
        VisUI.dispose()
        batch!!.dispose()
        texture!!.dispose()
    }
}
