;----- zansi_p.asm --------------------------------------------------------
; Zephyr ANSI terminal driver.
;    Copyright (C) 1986, Thomas Hanlin III, Springfield VA.
;    Based on original code for NANSI by Daniel Kegel, Pasadena, CA.
;------------------------------------------------------------------------
; A state machine implementation of the mechanics of ANSI terminal control
; string parsing.
;
; Entered with a jump to f_escape when driver finds an escape, or
; to f_in_escape when the last string written to this device ended in the
; middle of an escape sequence.
;
; Exits by jumping to f_ANSI_exit when an escape sequence ends, or
; to f_not_ANSI when a bad escape sequence is found, or (after saving state)
; to f_loopdone when the write ends in the middle of an escape sequence.
;
; Parameters are stored as bytes in param_buffer.  If a parameter is
; omitted, it is stored as zero.  Each character in a keyboard reassignment
; command counts as one parameter.
;
; When a complete escape sequence has been parsed, the address of the
; ANSI routine to call is found in ansi_fn_table.
;
; Register usage during parsing:
;  DS:SI points to the incoming string.
;  CX holds the length remaining in the incoming string.
;  ES:DI points to the current location on the memory-mapped screen.
;  DX is number of characters remaining on the current screen line.
;  BX points to the current paramter byte being assembled from the incoming
;  string.  (Stored in cur_parm_ptr between device driver calls, if needed.)
;
; The registers are set as follows before calling the ANSI subroutine:
;  AX = max(1, value of first parameter)
;  CX = number of paramters
;  SI = offset of second parameter from CS
;  DS = CS
;  ES:DI points to the current location on the memory-mapped screen.
;  DX is number of characters remaining on the current screen line.
; The subroutine is free to trash AX, BX, CX, SI, and DS.
; It must preserve ES, and can alter DX and DI if it wants to move the
; cursor.
;
;------------------------------------------------------------------------

        ; From zansi.asm
        extrn   f_not_ANSI:near         ; exit: abort bad ANSI cmd
        extrn   f_ANSI_exit:near        ; exit: good cmd done
        extrn   f_loopdone:near         ; exit: ran out of chars. State saved.
        extrn   escvector:word          ; saved state: where to jump
        extrn   cur_parm_ptr:word       ; saved state: where to put next param
        extrn   string_term:byte        ; saved state: what ends string
        extrn   cur_x:byte, max_x:byte  ; 0 <= cur_x <= max_x
        extrn   cur_attrib:byte         ; current color/attribute

        ; from zansi_f.asm
        extrn   ansi_fn_table:word      ; ANSI subroutine table

        ; Used in zansi.asm
        public  f_escape                ; entry: found an escape
        public  f_in_escape             ; entry: restore state, keep parsing

        ; Used in zansi_i.asm and zansi_f.asm
        public  param_buffer, param_end, redef_end


code    segment byte public 'CODE'
        assume  cs:code

; More saved state
in_num          db      ?       ; true if between a digit and a semi in parse

param_buffer    dw      3000h   ; address of first byte free for new params
param_end       dw      3030h   ; address of end of free area
redef_end       dw      3030h   ; address of end of redefinition area
                ; These initialized values are only for debugging

;----- next_is -------------------------------------------------------
; Next_is is used to advance to the next state.  If there are characters
; left in the input string, we jump immediately to the new state;
; otherwise, we shut down the recognizer, and wait for the next call
; to the device driver.
next_is macro   statename
        loop    statename
        mov     ax, offset statename
        jmp     sleep
        endm

;----- sleep --------------------------------------------------------
; Remember bx and next state, then jump to device driver exit routine.
; Device driver will re-enter at f_in_escape upon next invocation
; because escvector is nonzero; parsing will then be resumed.
sleep:  mov     cs:cur_parm_ptr, bx
        mov     cs:escvector, ax
        jmp     f_loopdone

;----- f_in_escape ---------------------------------------------------
; Main loop noticed that escvector was not zero.
; Recall value of BX saved when sleep was jumped to, and jump into parser.
f_in_escape:
        mov     bx, cs:cur_parm_ptr
        jmp     word ptr cs:escvector

fbr_syntax_error_gate:          ; jumped to from inside f_bracket
        jmp     syntax_error

;----- f_escape ------------------------------------------------------
; We found an escape.  Next character should be a left bracket.
f_escape:
        next_is f_bracket

;----- f_bracket -----------------------------------------------------
; Last char was an escape.  This one should be a [; if not, print it.
; Next char should begin a parameter string.
f_bracket:
        lodsb
        cmp     al, '['
        jnz     fbr_syntax_error_gate
        ; Set up for getting a parameter string.
        mov     bx, cs:param_buffer
        mov     byte ptr cs:[bx], 0
        mov     cs:in_num, 0
        next_is f_get_args

;----- f_get_args ---------------------------------------------------
; Last char was a [.  If the current char is a '=' or a '?', eat it.
; In any case, proceed to f_get_param.
; This is only here to strip off the strange chars that follow [ in
; the SET/RESET MODE escape sequence.
f_get_args:
        lodsb
        cmp     al, '='
        jz      fga_ignore
        cmp     al, '?'
        jz      fga_ignore
                dec     si              ; let f_get_param fetch al again
                jmp     short f_get_param
fga_ignore:
        next_is f_get_param

;----- f_get_param ---------------------------------------------------
; Last char was one of the four characters "]?=;".
; We are getting the first digit of a parameter, a quoted string,
; a ;, or a command.
f_get_param:
        lodsb
        cmp     al, '0'
        jb      fgp_may_quote
        cmp     al, '9'
        ja      fgp_may_quote
                ; It's the first digit.  Initialize current parameter with it.
                sub     al, '0'
                mov     byte ptr cs:[bx], al
                mov     cs:in_num, 1    ; set flag for sensing at cmd exec
                next_is f_in_param
fgp_may_quote:
        cmp     al, '"'
        jz      fgp_isquote
        cmp     al, "'"
        jnz     fgp_semi_or_cmd         ; jump to code shared with f_in_param
fgp_isquote:
        mov     cs:string_term, al      ; save it for end of string
        next_is f_get_string            ; and read string into param_buffer

;----- f_get_string -------------------------------------
; Last character was a quote or a string element.
; Get characters until ending quote found.
f_get_string:
        lodsb
        cmp     al, cs:string_term
        jz      fgs_init_next_param
        mov     byte ptr cs:[bx], al
        cmp     bx, cs:param_end
        adc     bx, 0                   ; if bx<param_end bx++;
        next_is f_get_string
; Ending quote was found.
fgs_init_next_param:
        mov     byte ptr cs:[bx], 0     ; initialize new parameter
        ; | Eat following semicolon, if any.
        next_is f_eat_semi

;----- f_eat_semi -------------------------------------
; Last character was an ending quote.
; If this char is a semi, eat it; else unget it.
; Next state is always f_get_param.
f_eat_semi:
        lodsb
        cmp     al, ';'
        jz      fes_eaten
        inc     cx
        dec     si
fes_eaten:
        next_is f_get_param

;----- syntax_error ---------------------------------------
; A character was rejected by the state machine.  Exit to
; main loop, and print offending character.  Let main loop
; decrement CX (length of input string).
syntax_error:
        mov     cs:escvector, 0
        mov     ah, cs:cur_attrib
        jmp     f_not_ANSI      ; exit, print offending char

;------ f_in_param -------------------------------------
; Last character was a digit.
; Looking for more digits, a semicolon, or a command character.
f_in_param:
        lodsb
        cmp     al, '0'
        jb      fgp_semi_or_cmd
        cmp     al, '9'
        ja      fgp_semi_or_cmd
        ; It's another digit.  Add into current parameter.
        sub     al, '0'
        xchg    byte ptr cs:[bx], al
        push    dx
        mov     dl, 10
        mul     dl
        pop     dx
        add     byte ptr cs:[bx], al
        next_is f_in_param
        ; Code common to states get_param and in_param.
        ; Accepts a semicolon or a command letter.
fgp_semi_or_cmd:
        cmp     al, ';'
        jnz     fgp_not_semi
        cmp     bx, cs:param_end        ; prepare for next param-
        adc     bx, 0                   ; if bp<param_end bp++;
        ; Set new param to zero, enter state f_get_param.
        mov     cs:in_num, 0            ; no longer inside number
        jmp     fgs_init_next_param     ; spaghetti code attack!
fgp_not_semi:
        ; It must be a command letter.
        cmp     al, '@'
        jb      syntax_error
        cmp     al, 'z'
        ja      syntax_error
        cmp     al, 'Z'
        jbe     fgp_is_cmd
        cmp     al, 'a'
        jb      syntax_error
        ; It's a lower-case command letter.
        ; Remove hole between Z and a to save space in table.
        sub     al, 'a'-'['
fgp_is_cmd:
        ; It's a command letter.  Save registers, convert letter
        ; into address of routine, set up new register usage, call routine.
        push    si                      ; These three registers hold info
        push    cx                      ; having to do with the input string,
        push    ds                      ; which has no interest at all to the
                                        ; control routine.

        push    cs
        pop     ds                      ; ds is now cs

        sub     al, '@'                 ; first command is @: insert chars
        cbw
        shl     ax,1
        add     ax, offset ansi_fn_table
        ; ax is now pointer to command routine address in table

        mov     cx, bx
        mov     si, param_buffer        ; si is now pointer to parameters
        sub     cx, si                  ;
        test    in_num, 1
        jz      fip_out_num
        inc     cx
fip_out_num:                            ; cx is now # of parameters

        mov     bx,ax                   ; save pointer to routine in bx

        ; Calculate cur_x from DX.
        mov     al, max_x
        inc     ax
        sub     al, dl
        mov     cur_x, al

        ; Get first parameter into AX; if defaulted, set it to 1.
        xor     ah,ah
        lodsb
        or      al, al
        jnz     fgp_callem
        inc     ax
fgp_callem:
        ; Finally, call the command subroutine.
        call    word ptr [bx]

        pop     ds
        pop     cx
        pop     si

        mov     ah, cs:cur_attrib       ; Prepare for STOSW.
        mov     cs:escvector, 0         ; No longer parsing escape sequence.
        ; Set flags for reentry at loopnz
        or      dx, dx                  ; "Any columns left on line?"
        ; Re-enter at bottom of main loop.
        jmp     f_ansi_exit


code    ends
        end
