; Pat80 Memory Monitor
; @author Daniele Verducci
; @language: Z80 ASM
;
;
; This file is part of Pat80 Memory Monitor.
;
; Pat80 Memory Monitor 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.
;
; Pat80 Memory Monitor 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 Pat80 Memory Monitor.  If not, see <http://www.gnu.org/licenses/>.
;
;
; Monitor commands (CMD $arg):
;   H (HELP) Shows available commands
;   D (DUMP) $pos Dumps bytes of memory starting at $pos
;   S (SET) $pos $val Replaces byte at $pos with $val
;   L (LOAD) $pos $val
;   R (RUN) $pos Starts executing code from $pos
;   A (ADB) Enters in Assembly Depoy Bridge mode: loads all the incoming bytes in application memory and starts executing.
; The commands are entered with a single letter and the program completes the command

include 'libs/strings.asm'

; CONSTANTS
MON_WELCOME: DB 10,"PAT80 MEMORY MONITOR 0.2",10,0
MON_COMMAND_HELP: DB "HELP",0  ; null terminated strings
MON_COMMAND_DUMP: DB "DUMP",0
MON_COMMAND_SET: DB "SET",0
MON_COMMAND_ZERO: DB "ZERO",0
MON_COMMAND_LOAD: DB "LOAD",0
MON_COMMAND_RUN: DB "RUN",0
MON_COMMAND_ADB: DB "ADB",0
MON_COMMAND_MEMTEST: DB "MEMTEST",0
MON_COMMAND_QUIT: DB "QUIT",0
MON_ARG_HEX: DB "    0x",0
MON_HELP: DB 10,"Available commands:\nHELP prints this message\nDUMP [ADDR] shows memory content\nSET [ADDR] sets memory content\nZERO [ADDR] [ADDR] sets all bytes to 0 in the specified range\nLOAD\nRUN [ADDR] executes code starting from ADDR\nADB starts Assembly Deploy Bridge\nMEMTEST checks ram boundaries\nQUIT exits",0
MON_MSG_ADB: DB 10,"Waiting for data.",0
MON_ERR_SYNTAX: DB "    Syntax error",0
; MON_RAMTEST_INTRO: DB " Checking memory... ",0
; MON_RAMTEST_RAMSTART: DB " Ram starts at 0x",0
MON_DUMP_BYTES_LINES: EQU 8
MON_DUMP_BYTES_PER_LINE: EQU 8

Monitor_main:
    ; Print welcome string
    ld bc, MON_WELCOME
    call Sys_Print
    monitor_main_loop:
        ; Newline
        ld a, 10
        call Sys_Printc
        ; Draw prompt char
        ld a, 62 ; >
        call Sys_Printc
        ; Read char from command line
        call Sys_Readc     ; blocking: returns when a character was read and placed in A reg
        call Strings_charToUpper    ; user may enter lowercase char: transform to upper
        call Sys_Printc     ; Print back the character to provide user feedback
        ; Switch case
        ld hl, MON_COMMAND_HELP
        cp (hl)  ; check incoming char is equal to command's first char
        jp z, monitor_help
        ld hl, MON_COMMAND_DUMP
        cp (hl)
        jp z, monitor_dump
        ld hl, MON_COMMAND_SET
        cp (hl)
        jp z, monitor_set
        ld hl, MON_COMMAND_ZERO
        cp (hl)
        jp z, monitor_zero
        ld hl, MON_COMMAND_LOAD
        cp (hl)
        jp z, monitor_load
        ld hl, MON_COMMAND_RUN
        cp (hl)
        jp z, monitor_run
        ld hl, MON_COMMAND_ADB
        cp (hl)
        jp z, monitor_adb
        ; ld hl, MON_COMMAND_MEMTEST
        ; cp (hl)
        ; jp z, monitor_memtest
        ld hl, MON_COMMAND_QUIT
		cp (hl)
		jp z, monitor_quit
        ; Unrecognized command: print error and beep
        ld bc, MON_ERR_SYNTAX
        call Sys_Print
        call Sys_Beep
    jp monitor_main_loop

monitor_help:
    ld bc, MON_COMMAND_HELP + 1 ; autocomplete command
    call Sys_Print

    ld bc, MON_HELP
    call Sys_Print
    jp monitor_main_loop

monitor_quit:
    ld bc, MON_COMMAND_QUIT + 1 ; autocomplete command
    call Sys_Print
	; newline
	ld a, 10
	call Sys_Printc
    ; Restores registers and re-enable interrupts: when the BREAK key is pressed, 
	; a maskable interrupt is generated and the CPU jumps to 0x38 reset vector, 
	; where if finds a call to Memory monitor (see main.asm).
	exx ; exchange registers
	ex af, af'
	; enable interrupts
	ei
	im 1 ; set interrupt mode 1 (on interrupt jumps to 0x38)
	reti ; return from interrupt


; Asks the user for a memory position and shows the following 64 bytes of memory
; @uses a, b, c, d, e, h, l
monitor_dump:
    ld bc, MON_COMMAND_DUMP + 1 ; autocomplete command
    call Sys_Print
    ; Now read the address from the user
    call monitor_arg_2byte  ; returns the read bytes in hl
    ld a, 10 ; newline
    call Sys_Printc
    ; now start displaying bytes from memory
    ld e, MON_DUMP_BYTES_LINES    ; the number of lines to display
    monitor_dump_show_bytes_loop:
        ld d, MON_DUMP_BYTES_PER_LINE*2   ; the number of bytes per line to display (*2 as we display two times the same byte: once hex and once ascii)
        ; Print current address
        ld a, h
        call monitor_printHexByte
        ld a, l
        call monitor_printHexByte
        ; print four spaces
        ld a, 32
        call Sys_Printc
        call Sys_Printc
        call Sys_Printc
        call Sys_Printc
        monitor_dump_show_bytes_line_loop:  ; counts down from 15 to 0
            ld a, d
            sub MON_DUMP_BYTES_PER_LINE + 1
            jp m, monitor_dump_show_bytes_line_loop_ascii   ; jp if
                ; if position is 8 to 15, print hex value at mem position
                ld a, (hl)
                ; print hex byte
                call monitor_printHexByte
                ; print space
                ld a, 32
                call Sys_Printc
                ; if position is 4, print a second space (to group nibbles)
                ld a, d
                cp MON_DUMP_BYTES_PER_LINE / 2 + MON_DUMP_BYTES_PER_LINE + 1
                jp nz, no_second_space
                    ; print second space
                    ld a, 32
                    call Sys_Printc
                no_second_space:
                ; move to next mem position
                inc hl
                ; decrement "nth byte on the line" counter
                dec d
                jp monitor_dump_show_bytes_line_loop
            ; if position is 0 to 7, print ascii
            monitor_dump_show_bytes_line_loop_ascii:
                ; is this the first ascii char printed in this line?
                ld a, d
                cp MON_DUMP_BYTES_PER_LINE
                jp nz, no_mempos_decr   ; no need to decrement, already done
                    ; do this only once: printing hex values we advanced the counter by 8 positions. Bring it back.
                    ld bc, MON_DUMP_BYTES_PER_LINE
                    sbc hl, bc
                    ; print 3 spaces to separate hex from ascii
                    ld a, 32
                    call Sys_Printc
                    call Sys_Printc
                    call Sys_Printc
                no_mempos_decr:
                ; print ascii
                ld a, (hl)
                call monitor_printAsciiByte
                ; print space
                ld a, 32
                call Sys_Printc
                ; if position is 12 (8+4), print a second space (to group nibbles)
                ld a, d
                cp MON_DUMP_BYTES_PER_LINE / 2 + 1
                jp nz, no_second_space2
                    ; print second space
                    ld a, 32
                    call Sys_Printc
                no_second_space2:
                ; move to next mem position
                inc hl
                ; decrement counter: if non zero continue loop
                dec d
                jp nz, monitor_dump_show_bytes_line_loop
        ; print newline
        ld a, 10
        call Sys_Printc
        ; decrement line counter
        dec e
        jp nz, monitor_dump_show_bytes_loop ; if line counter is not 0, print another line
    ; if line counter 0, finished
    jp monitor_main_loop

; Asks user for a memory position and a byte and puts the byte in memory
; @uses a, b, c, h, l
monitor_set:
    ld bc, MON_COMMAND_SET + 1 ; autocomplete command
    call Sys_Print
	; Now read the memory address to be changed from the user
    call monitor_arg_2byte  ; returns the read bytes in hl
	; Start looping memory addresses
	monitor_set_byte_loop:
		ld a, 10 ; newline
		call Sys_Printc
		; Print current address
        ld a, h
        call monitor_printHexByte
        ld a, l
        call monitor_printHexByte
        ; print two spaces
        ld a, 32
        call Sys_Printc
        call Sys_Printc
		; print previous memory content (hex)
		ld a, (hl)
		call monitor_printHexByte
		; print two spaces
        ld a, 32
        call Sys_Printc
        call Sys_Printc
		; print previous memory content (ascii)
		ld a, (hl)
		call monitor_printAsciiByte
		; print space
		ld a, 32
        call Sys_Printc
		; ask the user the new memory content
		call monitor_arg_byte	; returns the read byte in a, exit code in b
		; find if user pressed Q/ENTER
		ld c, a ; save a
		ld a, b ; exit code in a
		cp 1 ; check if user pressed Q
		jp z, monitor_main_loop	; user wants to exit
		ld a, b
		cp 2 ; check if user pressed ENTER
		jp z, monitor_set_skipbyte ; user doesn't want to replace current byte: skip
		; user didn't press Q/ENTER: he inserted a valid byte
		; print two spaces
        ld a, 32
        call Sys_Printc
        call Sys_Printc
		ld a, c ; restore valid byte in a
		call monitor_printAsciiByte ; print user-inserted byte in ascii
		ld a, c ; restore valid byte in a
		ld (hl), a ; write new byte to memory
		monitor_set_skipbyte:
		inc hl ; next memory position
	jp monitor_set_byte_loop

; Asks user for a memory range and sets all bytes to zero in that range in memory
; @uses a, b, c, h, l
monitor_zero: ; TODO: bugged, doesn't exit cycle
	ld bc, MON_COMMAND_ZERO + 1 ; autocomplete command
    call Sys_Print
	; Now read the starting memory address
    call monitor_arg_2byte  ; returns the read bytes in hl
	; starting addr in bc
	ld b, h
	ld c, l
	call monitor_arg_2byte  ; ending addr in hl	
	; Start looping memory addresses (from last to first)
	monitor_zero_loop:
		ld (hl), 0 ; set byte to 0 in memory
		dec hl	; next byte
		; check if we reached start addr (one byte at time)
		ld a, b
		cp h
		jp nz, monitor_zero_loop	; first byte is different, continue loop
		; first byte is equal, check second one
		ld a, c
		cp l
		jp nz, monitor_zero_loop	; second byte is different, continue loop
		; reached destination addr: zero the last byte and return
		ld (hl), 0 ; set byte to 0 in memory
		ret

monitor_load:
    ld bc, MON_COMMAND_LOAD + 1 ; autocomplete command
    call Sys_Print
	; TODO: When implemented, re-enable interrupts before run application
    jp monitor_main_loop

monitor_run:
    ld bc, MON_COMMAND_RUN + 1 ; autocomplete command
    call Sys_Print
    ; Now read the memory address to be executed from the user
    call monitor_arg_2byte  ; returns the read bytes in hl
    ld a, 10 ; newline
    call Sys_Printc	
	; enable interrupts
	ei
	im 1 ; set interrupt mode 1 (on interrupt jumps to 0x38)
	; pop the last entry on the stack: this is needed (as the monitor
	; runs in an interrupt) to counter-balance the missing reti statement
	pop bc
	; execute code
	jp (hl)

monitor_adb:
    ld bc, MON_COMMAND_ADB + 1 ; autocomplete command
    call Sys_Print
    ; start copying incoming data to application space
    call monitor_copyTermToAppMem
	; call monitor_enable_int    ; re-enable interrupts
    ;jp APP_SPACE    ; Start executing code


    ; ld bc, APP_SPACE
    ; call Sys_Print
    jp monitor_main_loop

; Prints "0x" and read 1 hex byte (2 hex digits, e.g. 0x8C)
; Can be cancelled with Q/ENTER
; @return a the read byte, b the exit code (0=valid byte in a, 1=Q, 2=ENTER)
; @uses a, b, c
monitor_arg_byte:
    ; Print 0x... prompt
    ld bc, MON_ARG_HEX
    call Sys_Print
    ; Read 2 digits
    call monitor_arg_byte_impl
    ret

; Prints "0x" and reads 2 hex bytes (4 hex digits e.g. 0x3F09)
; Ignores Q/ENTER keys
; @return hl the two read bytes
; @uses a, b, c, h, l
monitor_arg_2byte:
    ; Print 0x... prompt
    ld bc, MON_ARG_HEX
    call Sys_Print
    ; Read 2 digits
    call monitor_arg_byte_impl
    ld h, a ; move result to h
    ; Read 2 digits
    call monitor_arg_byte_impl
    ld l, a ; move result to l
    ret

; Read 2 hex digits
; @return a the read byte, b the exit code 
; (0 if no control key was, pressed, 1 for Q, 2 for RETURN)
; @uses a, b, c
monitor_arg_byte_impl:
    ; Receive first hex digit. Value in a, exit code in b
    call monitor_readHexDigit
	; check exit code to find if user pressed esc or return
	ld c, a ; save a
	ld a, b ; load exit code in a
	cp 0
	jp nz, monitor_arg_byte_impl_exitcode	; user pressed Q/RETURN key
	; user didn't press Q/RETURN: returned nibble is valid
	ld a, c ; restore a and discard c
    ; First hex digit is the most signif nibble, so rotate left by 4 bits
    rlca
    rlca
    rlca
    rlca
    ; the lower nibble must now be discarded
    and %11110000
    ld c, a     ; save shifted nibble in c
    ; Read second hex digit
    call monitor_readHexDigit
    ; Join the two nibbles in a single byte: second digit is already in a,
    ; so we OR with the previously shifted c and obtain the complete byte in a.
    or c
    ret

	monitor_arg_byte_impl_exitcode:
	; user pressed Q/RETURN key. Exit code is now in a.
	ld b, a	; move exit code in b
	ld a, 0 ; clear a
	ret



; Reads an hex digit (0 to 9, A to F)
; @return a the read nibble (or 0 if Q/RETURN was pressed), b the exit code 
; (0 if no control key was, pressed, 1 for Q, 2 for RETURN)
; @uses a, b
monitor_readHexDigit:
    call Sys_Readc
	; check if user pressed Q
	; if user pressed Q, return exit code 1 in b and 0 in a
	cp 81
	jp z, monitor_readHexDigit_esc
	; check if user pressed RETURN
	; if user pressed RETURN, return exit code 2 in b and 0 in a
	cp 10
	jp z, monitor_readHexDigit_return
	
    ; check if is a valid hex digit (0-9 -> ascii codes 48 to 57; A-F -> ascii codes 65 to 70)
    ; first check if is between 0 and F(ascii codes 48 to 70)
    ld b, a
    sub a, 48
    jp m, monitor_readHexDigit  ; if negative (s), ascii code is under 48: ignore char
    ld a, b
    sub a, 71   ; 71 because we want to include 70 and the result must be negative
    jp p, monitor_readHexDigit  ; if not negative (ns), ascii code is over 70: ignore it
    ; check if is a valid int (<=57)
    ld a, b
    sub a, 58
    jp p, monitor_readHexDigit_char  ; if not negative (ns), maybe is a char
    ; otherwise is a number! First print for visive feedback
    ld a, b
    call Sys_Printc
    ; then convert to its value subtracting 48
    sub a, 48
	ld b, 0 ; set b to exit code 0 to represent "valid value in a"
    ret
    monitor_readHexDigit_char:
    ; check if is A, B, C, D, E, F (ascii codes 65 to 70). We already checked it is less than 70.
    ld a, b
    sub a, 65
    jp m, monitor_readHexDigit  ; if negative (s), ascii code is under 65: ignore char
    ; otherwise is a valid char (A-F). Print for visive feedback
    ld a, b
    call Sys_Printc
    ; Its numeric value is 10 (A) to 15 (F). To obtain this, subtract 55.
    sub a, 55
	ld b, 0 ; set b to exit code 0 to represent "valid value in a"
    ret

	monitor_readHexDigit_esc:
	ld a, 0
	ld b, 1
	ret
	monitor_readHexDigit_return:
	ld a, 0
	ld b, 2
	ret

; Prints a byte in hex format: splits it in two nibbles and prints the two hex digits
; NOTE: The byte in a will be modified!
; @param a the byte to print
; @uses a, b, c
monitor_printHexByte:
    ld c, a
    ; rotate out the least significant nibble to obtain a byte with the most significant nibble
    ; in the least significant nibble position
    rrca
    rrca
    rrca
    rrca
    ; the upper nibble must now be discarded
    and %00001111
	call monitor_printHexDigit
    ld a, c
	and %00001111   ; bitwise and: set to 0 the most significant nibble and preserve the least
    call monitor_printHexDigit
	ret

; Prints an hex digit
; @param a provides the byte containing, in the LSBs, the nibble to print
; @uses a, b
monitor_printHexDigit:
    ; check the input is valid (0 to 15)
    ld b, a
    sub 16  ; subtract 16 instead of 15 cause 0 is positive
    ; if positive, the input is invalid. Do not print anything.
    ret p
    ; now check if the digit is a letter (10 to 15 -> A to F)
    ld a, b ; restore a
    sub 10
    ; if a is positive, the digit is a letter
    jp p, monitor_printHexDigit_letter
	ld a, b	; restore a
    ; add 48 (the ASCII number for 0) to obtain the corresponding number
    add 48
	call Sys_Printc
    ret
    monitor_printHexDigit_letter:
	ld a, b	; restore a
    ; to obtain the corresponding letter we should subtract 10 (so we count from A)
    ; and add 65 (the ASCII number for A). So -10+65=+55 we add only 55.
    add 55
	call Sys_Printc
    ret

; Prints an ASCII character. Similar to system Print function, but
; ignores control characters and replaces any non-printable character with a dot.
; NOTE: the a register is modified
; @param a the byte to print
; @uses a, b
monitor_printAsciiByte:
    ld b, a ; save a (it will be modified)
    ; if < 32 is a control char, non printable
    sub 32
    jp m, monitor_printAsciiByte_nonprintable
    ld a, b ; restore a
    ; if >= 127 is an extended char, may not be printable
    sub 127
    jp p, monitor_printAsciiByte_nonprintable
    ; otherwise is a printable ascii char
    ld a, b ; restore a
    call Sys_Printc
    ret
    monitor_printAsciiByte_nonprintable:
    ld a, 46 ; print dot
    call Sys_Printc
    ret

; Copy data from parallel terminal to application memory. This is tought to be used with the ADB function of the Pat80 Python Terminal.
; Uses TERM_DATA_AVAIL_REG to check if a byte is available before reading it.
; The first two received bytes (heading bytes) defines the stream length (MSB first), the rest of the bytes are copied to memory.
; The copy is completed when the number of bytes defined in the heading bytes are received.
; @uses a, b, c, d, h, l
monitor_copyTermToAppMem:
    ; d contains the current status.
    ; 2 = waiting for first heading byte
    ; 1 = waiting for second heading byte
    ; 0 = heading bytes received, now receiving binary stream
    ld d, 2
    ld hl, APP_SPACE    ; we will write in APP_SPACE
    monitor_copyTermToAppMem_loop:
        ld a, d
        cp 2    ; check if we are receiving first header byte
        jp z, monitor_copyTermToAppMem_loop_rec_head_byte_1
        ld a, d
        cp 1    ; check if we are receiving second header byte
        jp z, monitor_copyTermToAppMem_loop_rec_head_byte_2
        ; we are receiving binary stream: read byte and save to memory
        call Term_readb ; reads a byte from terminal
        ld (hl), a  ; copy byte to memory
        inc hl  ; move to next memory position
        dec bc  ; decrement remaining bytes counter
        ; check if we reached the number of bytes to be transferred
        ld a, b
        cp 0
        jp nz, monitor_copyTermToAppMem_loop   ; continue loop
        ld a, c
        cp 0
        jp nz, monitor_copyTermToAppMem_loop   ; continue loop
        ; all bytes received, return
        ret

    monitor_copyTermToAppMem_loop_rec_head_byte_1:
        ; we are receiving first header byte: read byte and save to b
        call Term_readb ; reads a byte from terminal
        ld b, a
        dec d
        jp monitor_copyTermToAppMem_loop   ; continue loop
    monitor_copyTermToAppMem_loop_rec_head_byte_2:
        ; we are receiving second header byte: read byte and save to c
        call Term_readb ; reads a byte from terminal
        ld c, a
        dec d
        jp monitor_copyTermToAppMem_loop   ; continue loop

; Runs a memory test to identify ram memory boundaries and check the ram is working.
; Starting from last memory position, writes 0xFF, reads it back, writes 0x00, reads it back.
; Exits when the first value differs from the written value (this may be caused by a bad ram
; block or the start of rom in memory map). Prints the last good address on exit.
; monitor_memtest:
;  ld bc, MON_COMMAND_MEMTEST + 1 ; autocomplete command
;  call Sys_Print
; 	; Prints intro
;  ld bc, MON_RAMTEST_INTRO
;  call Sys_Print
; 	; Starts checking
; 	ld hl, MEM_END
; 	monitor_memtest_loop:
; 		; Save current byte value for later restore
;      ld c, (hl)
;      ; Write 0xFF
; 		ld a, 0xFF
; 		ld (hl), a
; 		; Read and compare 0xFF
; 		ld a, (hl)
; 		cp 0xFF
; 		jp nz, monitor_memtest_badram
; 		; Write 0x00
; 		ld a, 0x00
; 		ld (hl), a
; 		; Read and compare 0xFF
; 		ld a, (hl)
; 		cp 0x00
; 		jp nz, monitor_memtest_badram
; 		; Memory byte is good, restore previous value
;      ld (hl), c
;      ; Next one
; 		dec hl
; 		jp monitor_memtest_loop
; 	monitor_memtest_badram:
; 		; Found a bad memory byte (or entered rom block).
; 		ld bc, MON_RAMTEST_RAMSTART
; 		call Sys_Print
; 		; Print last valid memory addr
;      inc hl
;      ld a, h
;      call monitor_printHexByte
;      ld a, l
;      call monitor_printHexByte
;      ; Newline
;      ld a, 10
;      call Sys_Printc
;      ; Back to menu
; 		jp monitor_main_loop