package mars.tools

import mars.Globals
import mars.detectRadix
import mars.mips.hardware.*
import mars.toHex
import mars.util.Binary
import java.awt.*
import java.awt.BorderLayout.*
import java.awt.event.KeyEvent
import java.awt.event.KeyListener
import java.util.*
import javax.swing.*
import javax.swing.border.EmptyBorder
import kotlin.collections.ArrayList
import kotlin.collections.HashMap
import kotlin.collections.HashSet

/*
Copyright (c) 2010-2011,  Pete Sanderson and Kenneth Vollmar

Developed by Pete Sanderson (psanderson@otterbein.edu)
and Kenneth Vollmar (kenvollmar@missouristate.edu)

Permission is hereby granted, free of charge, to any person obtaining 
a copy of this software and associated documentation files (the 
"Software"), to deal in the Software without restriction, including 
without limitation the rights to use, copy, modify, merge, publish, 
distribute, sublicense, and/or sell copies of the Software, and to 
permit persons to whom the Software is furnished to do so, subject 
to the following conditions:

The above copyright notice and this permission notice shall be 
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR 
ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 
CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

(MIT license, http://www.opensource.org/licenses/mit-license.html)
 */
/**
 * Bitmapp display simulator.  It can be run either as a stand-alone Java application having access to the mars package,
 * or through MARS as an item in its Tools menu.  It makes maximum use of methods inherited from its abstract superclass
 * AbstractMarsToolAndApplication. Pete Sanderson, verison 1.0, 23 December 2010.
 */
class BitmapDisplay : AbstractMarsToolAndApplication
{
    // Major GUI components
    private lateinit var uiUnitWidthSelector: JComboBox<Int>
    private lateinit var uiUnitHeightSelector: JComboBox<Int>
    private lateinit var uiWidthSelector: JComboBox<Int>
    private lateinit var uiHeightSelector: JComboBox<Int>
    private lateinit var uiBaseAddressSelector: JComboBox<String>
    private lateinit var uiKeyboardCheckbox: JCheckBox
    private lateinit var canvas: JPanel
    private lateinit var results: JPanel

    // Some GUI settings
    private val emptyBorder = EmptyBorder(4, 4, 4, 4)
    private val backgroundColor = Color.WHITE

    // Values for display canvas.  Note their initialization uses the identifiers just above.
    private val unitWidth get() = uiUnitWidthSelector.getInt()
    private val unitHeight get() = uiUnitHeightSelector.getInt()
    private val displayWidth get() = uiWidthSelector.getInt()
    private val displayHeight get() = uiHeightSelector.getInt()
    private val displayDimension get() = Dimension(displayWidth, displayHeight)

    private val baseAddresses = intArrayOf(Memory.dataSegmentBaseAddress, Memory.globalPointer, Memory.dataBaseAddress,
        Memory.heapBaseAddress, Memory.memoryMapBaseAddress)

    private val baseAddress get() = baseAddresses[uiBaseAddressSelector.selectedIndex]

    // Keyboard++
    private var keyboardAddr: UInt = 0xFFFF0010u
    private var keyboardAttached = false
    private lateinit var uiKeyboard: JTextField

    private lateinit var grid: Grid

    val pooledKeyEvents = hashMapOf<UInt, ArrayList<KeyEvent>>(
        0x00u to ArrayList(), 0x10u to ArrayList(), 0x20u to ArrayList()
    )
    val downKeys = HashSet<Int>()

    /**
     * Simple constructor, likely used to run a stand-alone bitmap display tool.
     *
     * @param title String containing title for title bar
     * @param heading String containing text for heading shown in upper part of window.
     */
    constructor(title: String, heading: String) : super(title, heading)

    constructor() : super("Azalea's Bitmap Display++", HEADING)

    override fun getName() = HEADING

    /**
     * Override the inherited method, which registers us as an Observer over the static data segment (starting address
     * 0x10010000) only.  This version will register us as observer over the the memory range as selected by the base
     * address combo box and capacity of the visualization display (number of visualization elements times the number of
     * memory words each one represents). It does so by calling the inherited 2-parameter overload of this method. If
     * you use the inherited GUI buttons, this method is invoked when you click "Connect" button on MarsTool or the
     * "Assemble and Run" button on a Mars-based app.
     */
    override fun addAsObserver()
    {
        var highAddress: Int = baseAddress + grid.rows * grid.cols * Memory.WORD_LENGTH_BYTES
        // Special case: baseAddress<0 means we're in kernel memory (0x80000000 and up) and most likely
        // in memory map address space (0xffff0000 and up).  In this case, we need to make sure the high address
        // does not drop off the high end of 32 bit address space.  Highest allowable word address is 0xfffffffc,
        // which is interpreted in Java int as -4.
        if (baseAddress < 0 && highAddress > -4)
        {
            highAddress = -4
        }
        addAsObserver(baseAddress, highAddress)
        addAsObserver(keyboardAddr.toInt(), (keyboardAddr + 0x20u).toInt())
    }

    /**
     * Method that constructs the main display area.  It is organized vertically into two major components: the display
     * configuration which an be modified using combo boxes, and the visualization display which is updated as the
     * attached MIPS program executes.
     *
     * @return the GUI component containing these two areas
     */
    override fun buildMainDisplayArea(): JComponent
    {
        results = JPanel()
        results.add(buildOrganizationArea())
        results.add(buildVisualizationArea())
        return results
    }

    //////////////////////////////////////////////////////////////////////////////////////
    //  Rest of the protected methods.  These override do-nothing methods inherited from
    //  the abstract superclass.
    //////////////////////////////////////////////////////////////////////////////////////
    /**
     * Update display when connected MIPS program accesses (data) memory.
     *
     * @param memory the attached memory
     * @param ac information provided by memory in MemoryAccessNotice object
     */
    override fun processMIPSUpdate(memory: Observable, ac: AccessNotice)
    {
        if (ac !is MemoryAccessNotice || ac.accessType != AccessNotice.WRITE) return
        val addr = ac.address.toUInt()

        // For the keyboard
        if (addr in keyboardAddr..keyboardAddr + 0x30u)
        {
            // Offset 0x01 or 0x11 or 0x21 bytes are for telling the keyboard that the events are received
            var offset = addr - keyboardAddr

            // Clear the segment
            if (offset == 0x01u || offset == 0x11u)
            {
                if (Globals.memory.getByte(addr.toInt()) != 1) return

                println("Offset written: ${offset.toHex(2)}")

                // Clear bytes if 1 is written to the 0x01 offset
                offset -= 1u
                val range = offset..offset + 0x0Fu
                synchronized(Globals.memoryAndRegistersLock)
                {
                    range.map { keyboardAddr + it }.forEach { Globals.memory.setByte(it.toInt(), 0) }
                }
                pooledKeyEvents[offset]!!.clear()
                println("[Keyboard++] Keyboard segment range $range cleared")
            }
            return
        }

        // For the display
        updateColorForAddress(ac)
    }

    /**
     * Event on key press
     *
     * @param offset Event type / offset
     * @param e Event
     */
    fun keyEvent(offset: UInt, e: KeyEvent)
    {
        if (!keyboardAttached) return
        println("[Keyboard++] ${e.id} '${e.keyChar}' ${e.keyCode}")
        val queue = pooledKeyEvents[offset]!!

        // Check for more than 7 key events queued
        if (queue.size == 7) return

        // Add to queue
        queue.add(e)

        // Add to memory
        synchronized(Globals.memoryAndRegistersLock)
        {
            // Change 0x0: Number of events
            var addr = keyboardAddr + offset
            println("[Keyboard++] Address ${addr.toHex(8)} set to ${queue.size} | ${addr.toHex(8)} set to ${e.keyCode} (${e.keyChar.toHex()})")
            Globals.memory.setByte(addr.toInt(), queue.size)

            // Set the keycode
            addr += queue.size.toUInt() * 2u
            Globals.memory.setHalf(addr.toInt(), e.keyCode)
        }
    }

    /**
     * Initialize all JComboBox choice structures not already initialized at declaration. Overrides inherited method
     * that does nothing.
     */
    override fun initializePreGUI()
    {
        grid = Grid(0, 0)
    }

    /**
     * The only post-GUI initialization is to create the initial Grid object based on the default settings of the
     * various combo boxes. Overrides inherited method that does nothing.
     */
    override fun initializePostGUI()
    {
        grid = createNewGrid()
    }

    /**
     * Method to reset counters and display when the Reset button selected. Overrides inherited method that does
     * nothing.
     */
    override fun reset()
    {
        resetCounts()
        updateDisplay()
    }

    /**
     * Updates display immediately after each update (AccessNotice) is processed, after display configuration changes as
     * needed, and after each execution step when Mars is running in timed mode.  Overrides inherited method that does
     * nothing.
     */
    override fun updateDisplay() = canvas.repaint()

    /**
     * Overrides default method, to provide a Help button for this tool/app.
     */
    override fun getHelpComponent(): JComponent
    {
        val helpContent = """
             Use this program to simulate a basic bitmap display where
             each memory word in a specified address space corresponds to
             one display pixel in row-major order starting at the upper left
             corner of the display.  This tool may be run either from the
             MARS Tools menu or as a stand-alone application.
             
             You can easily learn to use this small program by playing with
             it!   Each rectangular unit on the display represents one memory
             word in a contiguous address space starting with the specified
             base address.  The value stored in that word will be interpreted
             as a 24-bit RGB color value with the red component in bits 16-23,
             the green component in bits 8-15, and the blue component in bits 0-7.
             Each time a memory word within the display address space is written
             by the MIPS program, its position in the display will be rendered
             in the color that its value represents.
             
             Version 1.0 is very basic and was constructed from the Memory
             Reference Visualization tool's code.  Feel free to improve it and
             send me your code for consideration in the next MARS release.
             
             Contact Pete Sanderson at psanderson@otterbein.edu with
             questions or comments.
             
             """.trimIndent()
        val help = JButton("Help")
        help.addActionListener { JOptionPane.showMessageDialog(theWindow, helpContent) }
        return help
    }

    //////////////////////////////////////////////////////////////////////////////////////
    //  Private methods defined to support the above.
    //////////////////////////////////////////////////////////////////////////////////////
    // UI components and layout for left half of GUI, where settings are specified.
    private fun buildOrganizationArea(): JComponent
    {
        uiUnitWidthSelector = JComboBox(arrayOf(1, 2, 4, 8, 16, 32)).apply {
            isEditable = false
            background = backgroundColor
            selectedIndex = 1
            toolTipText = "Width in pixels of rectangle representing memory word"
            addActionListener {
                grid = createNewGrid()
                updateDisplay()
            }
        }

        uiUnitHeightSelector = JComboBox(arrayOf(1, 2, 4, 8, 16, 32)).apply {
            isEditable = false
            background = backgroundColor
            selectedIndex = 1
            toolTipText = "Height in pixels of rectangle representing memory word"
            addActionListener {
                grid = createNewGrid()
                updateDisplay()
            }
        }

        uiWidthSelector = JComboBox(arrayOf(64, 128, 256, 512, 1024)).apply {
            isEditable = false
            background = backgroundColor
            selectedIndex = 3
            toolTipText = "Total width in pixels of display area"
            addActionListener {
                canvas.preferredSize = displayDimension
                canvas.size = displayDimension
                grid = createNewGrid()
                updateDisplay()
            }
        }

        uiHeightSelector = JComboBox(arrayOf(64, 128, 256, 512, 1024)).apply {
            isEditable = false
            background = backgroundColor
            selectedIndex = 2
            toolTipText = "Total height in pixels of display area"
            addActionListener {
                canvas.preferredSize = displayDimension
                canvas.size = displayDimension
                grid = createNewGrid()
                updateDisplay()
            }
        }

        val descriptions = arrayOf("global data", "\$gp", "static data", "heap", "memory map")

        val displayBaseAddressChoices = baseAddresses
            .mapIndexed { i, it -> "${Binary.intToHexString(it)} (${descriptions[i]})" }.toTypedArray()

        uiBaseAddressSelector = JComboBox(displayBaseAddressChoices).apply {
            isEditable = false
            background = backgroundColor
            selectedIndex = 1
            toolTipText = "Base address for display area (upper left corner)"
            addActionListener {
                // If display base address is changed while connected to MIPS (this can only occur
                // when being used as a MarsTool), we have to delete ourselves as an observer and re-register.
                if (connectButton != null && connectButton.isConnected)
                {
                    deleteAsObserver()
                    addAsObserver()
                }
                grid = createNewGrid()
                updateDisplay()
            }
        }

        val uiKeyboardAddress = JTextField(keyboardAddr.toHex(8)).apply {
            addActionListener {
                text.toUIntOrNull(text.detectRadix())?.let {
                    keyboardAddr = it
                    uiKeyboardCheckbox.isSelected = false
                }
            }
        }
        val uiKeyboardCheckbox = JCheckBox("Attach Keyboard++", keyboardAttached).apply {
            addActionListener {
                keyboardAttached = isSelected
                if (isSelected)
                {
                    deleteAsObserver()
                    addAsObserver()
                }
                pooledKeyEvents.values.forEach { it.clear() }
            }
        }

        println("Bitmap Display++ Initialized")

        // Register key listener
        KeyboardFocusManager.getCurrentKeyboardFocusManager().addKeyEventDispatcher {
            if (!(keyboardAttached && uiKeyboard.hasFocus())) return@addKeyEventDispatcher false
            when (it.id)
            {
                KeyEvent.KEY_PRESSED ->
                {
                    // Keep track of down keys to avoid the system's key repeat
                    if (!downKeys.contains(it.keyCode))
                    {
                        keyEvent(0x00u, it)
                        downKeys.add(it.keyCode)
                    }
                }
                KeyEvent.KEY_RELEASED ->
                {
                    keyEvent(0x10u, it)
                    downKeys.remove(it.keyCode)
                }
            }
            true
        }
        uiKeyboard = JTextField(2).apply {
            class KL : KeyListener
            {
                override fun keyTyped(e: KeyEvent) {
                    uiKeyboard.text = ""
                }
                override fun keyPressed(e: KeyEvent) {
                    uiKeyboard.text = ""
                    // keyEvent(0x0u, e)
                }
                override fun keyReleased(e: KeyEvent) {
                    uiKeyboard.text = ""
                    // keyEvent(0x10u, e)
                }
            }

            addKeyListener(KL())
        }

        // Lay 'em out in the grid...
        return JPanel(GridLayout(8, 1)).apply {
            add(getPanelWithBorderLayout().apply {
                add(JLabel("Unit Width in Pixels "), WEST)
                add(uiUnitWidthSelector, EAST)
            })
            add(getPanelWithBorderLayout().apply {
                add(JLabel("Unit Height in Pixels "), WEST)
                add(uiUnitHeightSelector, EAST)
            })
            add(getPanelWithBorderLayout().apply {
                add(JLabel("Display Width in Pixels "), WEST)
                add(uiWidthSelector, EAST)
            })
            add(getPanelWithBorderLayout().apply {
                add(JLabel("Display Height in Pixels "), WEST)
                add(uiHeightSelector, EAST)
            })
            add(getPanelWithBorderLayout().apply {
                add(JLabel("Base address for display "), WEST)
                add(uiBaseAddressSelector, EAST)
            })
            add(getPanelWithBorderLayout().apply {
                add(uiKeyboardCheckbox, WEST)
                add(uiKeyboardAddress, EAST)
            })
            add(getPanelWithBorderLayout().apply {
                add(JLabel("To use keyboard, put your cursor here >"), WEST)
                add(uiKeyboard)
            })
        }
    }

    // UI components and layout for right half of GUI, the visualization display area.
    private fun buildVisualizationArea(): JComponent
    {
        canvas = GraphicsPanel()
        canvas.preferredSize = displayDimension
        canvas.toolTipText = "Bitmap display area"
        return canvas
    }

    // reset all counters in the Grid.
    private fun resetCounts() = grid.reset()

    private fun <T> JComboBox<T>.getInt() = selectedItem!!.toString().toInt()

    // Use this for consistent results.
    private fun getPanelWithBorderLayout(): JPanel = JPanel(BorderLayout(2, 2)).apply { border = emptyBorder }

    // Method to determine grid dimensions based on current control settings.
    // Each grid element corresponds to one visualization unit.
    private fun createNewGrid(): Grid
    {
        val rows = displayHeight / unitHeight
        val columns = displayWidth / unitWidth
        return Grid(rows, columns)
    }

    // Given memory address, update color for the corresponding grid element.
    private fun updateColorForAddress(notice: MemoryAccessNotice)
    {
        val address = notice.address
        val value = notice.value
        val offset = (address - baseAddress) / Memory.WORD_LENGTH_BYTES
        try
        {
            grid.setElement(offset / grid.cols, offset % grid.cols, value)
        }
        catch (e: IndexOutOfBoundsException)
        {
            // If address is out of range for display, do nothing.
        }
    }

    //////////////////////////////////////////////////////////////////////////////////////
    //  Specialized inner classes for modeling and animation.
    //////////////////////////////////////////////////////////////////////////////////////
    /////////////////////////////////////////////////////////////////////////////
    //  Class that represents the panel for visualizing and animating memory reference
    //  patterns.
    private inner class GraphicsPanel : JPanel()
    {
        // override default paint method to assure display updated correctly every time
        // the panel is repainted.
        override fun paint(g: Graphics) = paintGrid(g, grid)

        // Paint the color codes.
        private fun paintGrid(g: Graphics, grid: Grid)
        {
            var upperLeftX = 0
            var upperLeftY = 0
            for (i in 0 until grid.rows)
            {
                for (j in 0 until grid.cols)
                {
                    g.color = grid.getElementFast(i, j)
                    g.fillRect(upperLeftX, upperLeftY, unitWidth, unitHeight)
                    upperLeftX += unitWidth // faster than multiplying
                }
                // get ready for next row...
                upperLeftX = 0
                upperLeftY += unitHeight // faster than multiplying
            }
        }
    }

    ////////////////////////////////////////////////////////////////////////
    // Represents grid of colors
    private class Grid(rows: Int, columns: Int)
    {
        var grid: Array<Array<Color?>>
        var rows: Int
        var cols: Int

        init
        {
            grid = Array(rows) { arrayOfNulls(columns) }
            this.rows = rows
            this.cols = columns
            reset()
        }

        // Returns value in given grid element; null if row or column is out of range.
        private fun getElement(row: Int, column: Int): Color?
        {
            return if (row in 0..rows && column >= 0 && column <= cols) grid[row][column] else null
        }

        // Returns value in given grid element without doing any row/column index checking.
        // Is faster than getElement but will throw array index out of bounds exception if
        // parameter values are outside the bounds of the grid.
        fun getElementFast(row: Int, column: Int): Color?
        {
            return grid[row][column]
        }

        // Set the grid element.
        fun setElement(row: Int, column: Int, color: Int)
        {
            grid[row][column] = Color(color)
        }

        // Set the grid element.
        private fun setElement(row: Int, column: Int, color: Color)
        {
            grid[row][column] = color
        }

        // Just set all grid elements to black.
        fun reset()
        {
            for (i in 0 until rows)
            {
                for (j in 0 until cols)
                {
                    grid[i][j] = Color.BLACK
                }
            }
        }
    }

    companion object
    {
        private const val VERSION = "Version 1.0"
        private const val HEADING = "Bitmap Display++"

        /**
         * Main provided for pure stand-alone use.  Recommended stand-alone use is to write a driver program that
         * instantiates a Bitmap object then invokes its go() method. "stand-alone" means it is not invoked from the MARS
         * Tools menu.  "Pure" means there is no driver program to invoke the application.
         */
        @JvmStatic
        fun main(args: Array<String>)
        {
            BitmapDisplay("Bitmap Display stand-alone, " + VERSION, HEADING).go()
        }
    }
}
