//
//  PreferencesViewController.swift
//  Steve ][
//
//  Created by Tamas Rudnai on 6/4/20.
//  Copyright © 2019, 2020 Tamas Rudnai. All rights reserved.
//
// This file is part of Steve ][ -- The Apple ][ Emulator.
//
// Steve ][ is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Steve ][ is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Steve ][.  If not, see <https://www.gnu.org/licenses/>.
//

import Cocoa

class DebuggerViewController: NSViewController {
    static var shared : DebuggerViewController? = nil

    @IBOutlet var CPU_Display: DisplayView!
    @IBOutlet var Stack_Display: DisplayView!
    @IBOutlet var Mem1_Addr_Nibbles: NSTextField!
    @IBOutlet var Mem1_Display: DisplayView!
    @IBOutlet var ZP_Display: DisplayView!
    @IBOutlet var IO_Display: DisplayView!
    @IBOutlet var Disass_Scroll: DisplayScrollView!
    @IBOutlet var Disass_Display: DisplayView!
    @IBOutlet weak var MemoryAddressField: NSTextField!
    @IBOutlet weak var MemoryTextField: NSTextField!
    @IBOutlet weak var DisassAddressField: NSTextField!
    @IBOutlet weak var DisassAddressPC: NSButton!
    @IBOutlet weak var DisassTextField: NSTextField!
    @IBOutlet weak var DisassHighlighter: NSTextField!
    @IBOutlet weak var DisassHightlighterContriant: NSLayoutConstraint!
    @IBOutlet weak var DisassCursor: NSTextField!
    @IBOutlet weak var DisassCursorContraint: NSLayoutConstraint!
    @IBOutlet weak var DisassBackground: NSTextField!


    let infoFont : NSFont = NSFont(name: "Print Char 21", size: 10.0)!
    let disassFont : NSFont = NSFont(name: "Print Char 21", size: 10.0)!
    let textColor : NSColor = NSColor.white
    let highlightColor : NSColor = NSColor.blue
    let infoTextParagraph : NSMutableParagraphStyle = NSMutableParagraphStyle()
    let disassTextParagraph : NSMutableParagraphStyle = NSMutableParagraphStyle()
    let infoTextAttribs : [NSAttributedString.Key : NSObject]
    let disassTextAttribs : [NSAttributedString.Key : NSObject]
    let infoHighlightAttribs : [NSAttributedString.Key : NSObject]
    let disassHighlightAttribs : [NSAttributedString.Key : NSObject]

    required init?(coder: NSCoder) {
//        textParagraph.lineSpacing = 10.0  /*this sets the space BETWEEN lines to 10points*/
//        textParagraph.maximumLineHeight = 12.0/*this sets the MAXIMUM height of the lines to 12points*/
        disassTextParagraph.lineHeightMultiple = 1.15
        disassTextAttribs = [
            NSAttributedString.Key.font: disassFont,
            NSAttributedString.Key.foregroundColor: textColor,
            NSAttributedString.Key.paragraphStyle: disassTextParagraph
        ]
        disassHighlightAttribs = [
            NSAttributedString.Key.font: disassFont,
            NSAttributedString.Key.foregroundColor: textColor,
            NSAttributedString.Key.backgroundColor: highlightColor,
            NSAttributedString.Key.paragraphStyle: disassTextParagraph
        ]
        infoTextParagraph.lineHeightMultiple = 1.15
        infoTextAttribs = [
            NSAttributedString.Key.font: disassFont,
            NSAttributedString.Key.foregroundColor: textColor,
            NSAttributedString.Key.paragraphStyle: infoTextParagraph
        ]
        infoHighlightAttribs = [
            NSAttributedString.Key.font: disassFont,
            NSAttributedString.Key.foregroundColor: textColor,
            NSAttributedString.Key.backgroundColor: highlightColor,
            NSAttributedString.Key.paragraphStyle: infoTextParagraph
        ]

        super.init(coder: coder)
        DebuggerViewController.shared = self
    }


    func disassDisplay(str : String) {
        let disassAttrString = NSAttributedString.init(string: String(str.dropLast()), attributes: disassTextAttribs)
        DisassTextField.attributedStringValue = disassAttrString
    }


    override func viewDidLoad() {
        super.viewDidLoad()
        self.preferredContentSize = NSMakeSize(self.view.frame.size.width, self.view.frame.size.height)

        // For the fake text view scroller
        // 64K RAM/2 as an average bytes / instruction
        Disass_Display.string = ""
        // for proper address scrolling
        var r = Disass_Display.frame
        r.size.height = 65535
        Disass_Display.frame = r
        
        
        CPU_Display.font = infoFont
        Stack_Display.font = infoFont
        Mem1_Addr_Nibbles.font = infoFont
        Mem1_Display.font = infoFont
        ZP_Display.font = infoFont
        IO_Display.font = infoFont

    }

    
    override func viewDidAppear() {
        super.viewDidAppear()
        
        UpdateImmediately()

        if let debugger = DebuggerWindowController.shared {
            debugger.PauseButtonUpdate(needUpdateMainToolbar: false)
        }

        MemoryAddressField.formatter = HexDigitFormatter(maxLen: 4)
        DisassAddressField.formatter = HexDigitFormatter(maxLen: 4)
    }


    let maxAddr = Float(0xFFFF)

    /// Disassembly View Scroll changed
    func disassScroller(needScroll : Bool = false) {
        var scrollTo = Disass_Display.visibleRect.origin
        let scrollPos = Disass_Scroll.verticalScroller?.floatValue ?? 0
        let addr = scrollPos * maxAddr
        disass_addr_pc = addr < maxAddr ? UInt16(addr) : UInt16(maxAddr)

//        print("disassScroller ", "scrollTo", scrollTo, "scrollPos", scrollPos, " addr:", disass_addr_pc)

        if needScroll {
            scrollTo.y = Disass_Display.frame.height * CGFloat(scrollPos)
        }

        DisassAddressPC.state = .off
        DisplayDisassembly(scrollY: scrollTo.y)
    }


    func scrollEvent(location: NSPoint, scrollView: NSScrollView, deltaY: Float, action: () -> Void) {
        if location.x > scrollView.frame.minX
        && location.x < scrollView.frame.maxX
        && location.y > scrollView.frame.minY
        && location.y < scrollView.frame.maxY
        {
            if let documentView = scrollView.documentView {
                scrollView.verticalScroller?.floatValue += deltaY / Float(documentView.frame.height)
            }

            action()
        }
    }


    override func scrollWheel(with event: NSEvent) {
//        print("scrollWheel")
        super.scrollWheel(with: event)

        let location = event.locationInWindow

//        print("scrollWheel, location:", location,
//              Disass_Scroll.frame.minX,
//              Disass_Scroll.frame.maxX,
//              Disass_Scroll.frame.minY,
//              Disass_Scroll.frame.maxY
//              )

        let deltaY = Float(event.scrollingDeltaY)
//        print("scrollWheel:", event.scrollingDeltaY, deltaY)

        scrollEvent(location: location, scrollView: Disass_Scroll, deltaY: deltaY, action: {
            disassScroller(needScroll: true)
        })

    }


    /// Disassemby View had been Scrolled using the ScrollBar Y
    /// - Parameter sender: ScrollBar
    @IBAction func DisassScrolled(_ sender: NSScroller) {
//        print("DisassScrolled")
        disassScroller(needScroll: true)
    }


    func bin(n : UInt8, sp : String = "", mid : String = " ") -> String {
        var str = ""
        var n = n

        for i in 1...8 {
            if n & 0x80 == 0 {
                str += "0"
            }
            else {
                str += "1"
            }

            str += sp
            if i == 4 {
                str += mid
            }

            n <<= 1
        }

        return str
    }

    func DisplayRegisters() {

        let registers = String(format:"""
A: %02X    %3u      %@
X: %02X    %3u      %@
Y: %02X    %3u      %@

SP: %02X
PC: %04X

N V - B D I Z C
%d %d %d %d %d %d %d %d

STK REA WRI LNG BNK PG2 80S CXR
mne mne mne rom  2  off off off

KEY TXT MIX HGR 80C ALT
93  on  off off off off
""",

            m6502.A, m6502.A, bin(n: m6502.A),
            m6502.X, m6502.X, bin(n: m6502.X),
            m6502.Y, m6502.Y, bin(n: m6502.Y),
            m6502.SP,
            m6502.PC,
            m6502.N != 0, m6502.V != 0, m6502.res != 0, m6502.B != 0, m6502.D != 0, m6502.I != 0, m6502.Z != 0, m6502.C != 0
        )

        DispatchQueue.main.async {
            self.CPU_Display.string = registers
        }
    }


    func DisplayStack() {
        var stack = ""
        for i : UInt16 in (0x100...0x1FF).reversed() {
            stack += String(format:"%03X: %02X\n", i, getMEM(i))
        }

        DispatchQueue.main.async {
            self.Stack_Display.string = stack
        }
    }

    func hexLine16(addr : UInt16) -> String {
        var line = String(format: "%04X: ", addr)

        for i : UInt16 in 0...15 {
            line += String(format: "%02X ", getMEM(addr + i))
        }

        return line
    }


    let txtClear = [Character](repeating: " ", count: 16)
    func textLine16(addr : UInt16) -> String {
        var unicodeTextArray = NSArray(array: txtClear, copyItems: true) as! [Character]

        // render the rest of the text screen
        for i in 0 ... 15 {
            let byte = getMEM(addr + UInt16(i))
            let idx = Int(byte);
            let chr = ViewController.charConvTbl[idx]

            unicodeTextArray[i] = chr
        }

        return String(unicodeTextArray)
    }


    var mem_1_addr : UInt16 = 0x400

    func DisplayMemory() {
        var memory = ""
        for i : UInt32 in stride(from: UInt32(mem_1_addr), to: UInt32(mem_1_addr) + 0x21F, by: 16) {
            memory += hexLine16(addr: UInt16(i & 0xFFFF)) + " " + textLine16(addr: UInt16(i & 0xFFFF)) + "\n"
        }

        DispatchQueue.main.async {
            self.Mem1_Display.string = memory
        }
    }


    func ASCII_to_Apple2(line : String) -> String {
        var converted = ""

        for chr in line {
            // make C character NORMAL Apple ][ character
            let c = Int(chr.asciiValue!) & 0x3F | 0x80

            // breakpoint marker
            if c == 0xAA { // '*'
//                converted.append("\u{E895}") // big dot (8x8)
//                converted.append("\u{ED3C}") // big dot2 (8x8)
                converted.append("\u{E095}") // right arrow
//                converted.append("\u{E09B}") // diamond
//                converted.append("\u{E080}") // closed apple
//                converted.append("\u{E081}") // open apple
//                converted.append("\u{E185}") // checkmark
            }
            // normal character
            else {
                converted.append(ViewController.charConvTbl[c])
            }
        }

        return converted
    }


    func invertLine(line : String) -> String {
        var converted = ""

        for chr in line {
            let c = Int(chr.asciiValue!) & 0x3F
            converted.append(ViewController.charConvTbl[c])
        }

        return converted
    }


    let disass_addr_min : UInt16 = 0 // 320
    let disass_addr_max : UInt16 = 50 // 512
    var disass_addr : UInt16 = 0 /// Address disassembled in the window
    var disass_addr_pc : UInt16 = 0 /// Address to disassemble
    let disass_addr_pre : UInt16 = 20
    let disass_addr_min_pre : UInt16 = 32 // how many bytes we need to start to disassemble before our target address
    var line_number = 0
    var scroll_line_number = 0
    var highlighted_line_number = 0
    var line_number_cursor = 0
    let lines_to_disass = 50


    func get_scroll_line(view: NSTextView) -> Int {
        let scrollPos = view.visibleRect.origin.y
        let lineSpacing = CGFloat(1.5)
        let lineHeight = view.font!.pointSize * lineSpacing

        return Int(scrollPos / lineHeight)
    }


    func scroll_to_old(view: DisplayView, line: Int) {
        var scrollTo = view.visibleRect.origin
        let lineSpacing = CGFloat(1.5)
        let lineHeight = view.font!.pointSize * lineSpacing

        scrollTo.y = CGFloat(line) * lineHeight

        view.scroll(scrollTo)
    }


    func getLine(inView view: NSTextView, forY: CGFloat) -> Int {
//        var scrollTo = view.visibleRect.origin
        let lineSpacing = CGFloat(1.5)
        let fontPointSize = CGFloat(10) // view.font!.pointSize
        let lineHeight = fontPointSize * lineSpacing

        let line = round(forY) / lineHeight

        return Int(line) + 1
    }


    func getLineRange_old(inView view: NSTextView, forLine: Int) -> NSRange? {
        let layoutManager = view.layoutManager!
        var numberOfLines = 0
        let numberOfGlyphs = layoutManager.numberOfGlyphs
        var lineRange = NSRange()
        var indexOfGlyph = 0

        while indexOfGlyph < numberOfGlyphs {
            layoutManager.lineFragmentRect(forGlyphAt: indexOfGlyph, effectiveRange: &lineRange, withoutAdditionalLayout: false)

            // check if we've found our line number
            if numberOfLines == forLine {
                return lineRange
            }

            indexOfGlyph = NSMaxRange(lineRange)
            numberOfLines += 1
        }

        // could not find line number
        return nil
    }


    func getLineRange(_ lineRange : [LineRange_t], forLine: Int) -> NSRange? {
//        print("disassLineRange.count:", disassLineRange.count)
        if forLine >= 0 && forLine < lineRange.count {
            let disassRange = lineRange[forLine]
            return NSRange(location: disassRange.loc, length: disassRange.len)
        }

        // could not find line number
        return nil
    }


    let lineFromTopToMiddle = 0

    func scroll_to(view: NSTextView, line: Int) {
//        print("scroll_to", line)
        let lineSpacing = 1.5
        let fontPointSize = 10.0 // Disass_Display.font!.pointSize
        let lineHeight = fontPointSize * lineSpacing
        let line = line > 0 ? line - 1 : 0

        if getLineRange(disassLineRange, forLine: line + lineFromTopToMiddle) != nil {

            view.scroll( NSPoint(x: 0, y: Double(line) * lineHeight ) )
        }
    }


    func scroll_to_disass(addr: UInt16) {
        Disass_Scroll.verticalScroller?.floatValue = Float(addr) / Float(Disass_Display.frame.height)
    }


    let lineAttrAtSelected = [
        NSAttributedString.Key.backgroundColor: NSColor.lightGray,
        NSAttributedString.Key.foregroundColor: NSColor.black,
    ]

    let lineAttrAtPC = [
        NSAttributedString.Key.backgroundColor: NSColor.blue,
        NSAttributedString.Key.foregroundColor: NSColor.cyan,
    ]


    func remove_highlight(view : NSTextView, lineRange : NSRange) {
        DispatchQueue.main.async {
            if let layoutManager = view.layoutManager {
                layoutManager.removeTemporaryAttribute(NSAttributedString.Key.backgroundColor, forCharacterRange: lineRange)
                layoutManager.removeTemporaryAttribute(NSAttributedString.Key.foregroundColor, forCharacterRange: lineRange)
            }
        }
    }

    func remove_highlight(view : NSTextView, line : Int) {
        if line > 0 {
            if let lineRange = getLineRange(disassLineRange, forLine: line) {
                remove_highlight(view: view, lineRange: lineRange)
            }
        }
    }


    let lineHeight = CGFloat(14.96) // magic number... No idea why... 10pt font size + 1.5 lineSpacing

    func remove_highlight(view: NSTextField) {
        view.isHidden = true
    }


    func remove_highlight_attr(view: NSTextView) {
        if highlighted_line_number > 0 {
            if let lineRange = getLineRange(disassLineRange, forLine: highlighted_line_number) {
                if let layoutManager = view.layoutManager {
                    DispatchQueue.main.asyncAfter(deadline: .now() + 0.03, execute: {
                        layoutManager.removeTemporaryAttribute(NSAttributedString.Key.backgroundColor, forCharacterRange: lineRange)
                        layoutManager.removeTemporaryAttribute(NSAttributedString.Key.foregroundColor, forCharacterRange: lineRange)
                    })
                }
            }
        }
        highlighted_line_number = 0
    }


    func highlight(view: NSTextField, constraint: NSLayoutConstraint, line: Int) {
        if line > 0 {
            let line = line > 0 ? line - 1 : 0

            if getLineRange(disassLineRange, forLine: line) != nil {
                constraint.constant = CGFloat(line) * lineHeight + 1
                view.isHidden = false
                // to make sure not to remove higlight
                return
            }
        }

        // remove old highlighted line
        remove_highlight(view: view)
    }


    func isMouseInView(view: NSView) -> Bool {
        if let window = view.window {
            return view.isMousePoint(window.mouseLocationOutsideOfEventStream, in: view.frame)
        }
        return false
    }


    let textViewMouseYOffset = CGFloat(-4.0)
    func convertMouseCoordinates(scrollView : NSView, display : NSTextView, mouseLocation : NSPoint) -> NSPoint {
        var location = mouseLocation
        let parent_frame = scrollView.superview?.frame

//        let minX = parent_frame!.minX + scrollView.frame.minX
        let minY = parent_frame!.minY + scrollView.frame.minY
//        let maxX = minX + scrollView.frame.width
        let maxY = minY + scrollView.frame.height

//        location.x = maxX - location.x
        location.y = maxY - location.y + display.visibleRect.origin.y + textViewMouseYOffset

        return location
    }


    /// Highlight the entire line at the mouse location
    /// - Parameters:
    ///   - scrollView: ScrollView of the scrollable TextView
    ///   - display: The TextView itself
    ///   - mouseLocation: Mouse location locally inside the ScrollView
    func highlightCursor(scrollView : NSScrollView, mouseLocation : NSPoint) {
        let display = scrollView.documentView as! NSTextView

        // covering rectangle of the entire document
        let minX = scrollView.frame.minX
        let minY = scrollView.frame.minY
        let maxX = scrollView.frame.maxX
        let maxY = scrollView.frame.maxY - 4

        if mouseLocation.x > minX && mouseLocation.x < maxX
        && mouseLocation.y > minY && mouseLocation.y < maxY
        {
            let line = getLine(inView: display, forY: mouseLocation.y)
            highlight(view: DisassCursor, constraint: DisassCursorContraint, line: line)
            line_number_cursor = line
        }
    }

    let normalBackground = NSColor(red: 38/255, green: 38/255, blue: 38/255, alpha: 1)
    let selectedBackground = NSColor(red: 38/255, green: 38/255, blue: 64/255, alpha: 1.0)

    func unselectViews() {
        DisassBackground.backgroundColor = normalBackground
    }

    func selectView(view: NSTextView) {
        unselectViews()
        view.backgroundColor = normalBackground
    }

    let leftSideSize = CGFloat(30)

    // select disassembly line
    override func mouseDown(with event: NSEvent) {
        unselectViews()

        let location = convertMouseCoordinates(scrollView: Disass_Scroll, display: Disass_Display, mouseLocation: event.locationInWindow)

        if location.x < leftSideSize {
            let line = getLine(inView: Disass_Display, forY: location.y)
            let addr = getAddr(forLine: line)

            if m6502_dbg_bp_exists(breakpoints, addr) {
                m6502_dbg_bp_del(breakpoints, addr)
            }
            else {
                m6502_dbg_bp_add(breakpoints, addr)
            }

            // force regenerate disassembly
            disass_addr = 0xFFFF
            DisplayDisassembly(scrollY: Disass_Display.visibleRect.origin.y)
        }
        else {
            highlightCursor(scrollView: Disass_Scroll, mouseLocation: location)
        }
    }


    // context menu
    override func rightMouseDown(with event: NSEvent) {
        let location = event.locationInWindow
        highlightCursor(scrollView: Disass_Scroll, mouseLocation: location)
    }


    var addr_line = [UInt16 : Int]()

    func getLine(forAddr: UInt16) -> Int {
        return addr_line[forAddr] ?? -1
    }

    func getAddr(forLine: Int) -> UInt16 {
        return addr_line.first(where: { $1 == forLine })?.key ?? 0
    }


    struct LineRange_t {
        var loc : Int
        var len : Int
    }

    var disassLineRange = [LineRange_t]()


    func TrunDisassAddressPC(_ on: NSControl.StateValue = .on) {
        if let disassAddressPC = DisassAddressPC {
            disassAddressPC.state = on
        }
    }


    var isCurrentLine = false

    var disass = ""
    var loc = 0

    func AddDisassLine() {
        let line = ASCII_to_Apple2( line: String(cString: disassemblyLine()!) )
        let len = disassLineLength + 1
        let lineRange = LineRange_t(loc: loc, len: len)
        disassLineRange.append(lineRange)
        loc += len

        disass += line + "\n"
    }


    func DisplayDisassembly( scrollY : CGFloat = -1 ) {
        disass = ""
        loc = 0
        isCurrentLine = false
        highlighted_line_number = -1 // getLine(forAddr: m6502.PC)

        if cpuState == cpuState_running {
            remove_highlight(view: DisassHighlighter)
        }

        line_number = 0

        // TODO: Also check if memory area updated!

        let addrpc = DisassAddressPC == nil || DisassAddressPC.state == .on
        if addrpc {
            disass_addr_pc = m6502.PC
        }

        let need_scroll = scrollY > 0 || disass_addr_pc < disass_addr || UInt(disass_addr_pc) > UInt(disass_addr) + UInt(disass_addr_max)

        scroll_line_number = getLine(forAddr: disass_addr_pc)

        disassLineRange.removeAll()
        ViewController.shared?.UpdateSemaphore.wait()

        let m6502_saved = m6502

        if !addrpc {
            m6502.PC = disass_addr_pc
        }

        addr_line.removeAll()

        // de we need to scroll or prell at the same location?
        if need_scroll {
            disass_addr = m6502.PC
        }
        else {
            m6502.PC = disass_addr
        }

        if m6502.PC >= disass_addr_min_pre {
            m6502.PC -= disass_addr_min_pre
        }

        // try to sync disassembly code
        let addr_min = disass_addr >= disass_addr_min ? disass_addr - disass_addr_min : disass_addr

        while m6502.PC < addr_min {
            m6502_Disass_1_Instr()
        }

        // hopefully instruction address is in sync
        disass_addr = m6502.PC

        // Scroll by address is needed only when address is NOT calculated from scroll position...
        if scrollY < 0 {
            scroll_to_disass(addr: disass_addr)
        }

        // normal disassembly
        for _ in 1...lines_to_disass {
            // check if this is the current line before disassembling it (that will change PC...)
            line_number += 1
            addr_line.updateValue(line_number, forKey: m6502.PC)

            isCurrentLine = m6502.PC == m6502_saved.PC
            if isCurrentLine {
//                line = invertLine(line: line)
                highlighted_line_number = line_number
            }

            if m6502.PC == disass_addr {
                scroll_line_number = line_number
            }

            m6502_Disass_1_Instr()
            AddDisassLine()
        }

        m6502 = m6502_saved
        ViewController.shared?.UpdateSemaphore.signal()

        DispatchQueue.main.async {
            self.disassDisplay(str: self.disass)
            self.highlight(view: self.DisassHighlighter, constraint: self.DisassHightlighterContriant, line: self.highlighted_line_number)
        }
    }


    func UpdateImmediately() {
        DispatchQueue.main.async {
            self.DisplayRegisters()
            self.DisplayStack()
            self.DisplayMemory()
            self.DisplayDisassembly()
        }
    }


    let UpdateSemaphore = DispatchSemaphore(value: 1)
    func Update() {
        if self.UpdateSemaphore.wait(timeout: .now()) == .success {
            if Disass_Display != nil {
                self.UpdateImmediately()
            }
            self.UpdateSemaphore.signal()
        }
    }


    @IBAction func MemoryAddressEntered(_ sender: NSTextFieldCell) {
        NSLog("MemoryAddressEntered %@", sender.stringValue)
        mem_1_addr = UInt16(sender.stringValue.hexValue())
        DisplayMemory()
    }

    @IBAction func DisassAddressEntered(_ sender: NSTextFieldCell) {
        NSLog("DisassAddressEntered %@", sender.stringValue)
//        sender.stringValue = "4321" // MemoryAddressField.stringValue
        DisassAddressPC.state = .off
        disass_addr_pc = UInt16(sender.stringValue.hexValue())
//        Disass_Display.scroll(NSPoint(x: 0, y: Int(disass_addr_pc)))

//        scroll_to_disass(addr: disass_addr_pc)

        DisplayDisassembly()
    }

}
