## the basic editor data structure, and how it displays text to the screen

# temporary main for this layer: just render the given text at the given
# screen dimensions, then stop
recipe! main text:address:shared:array:character [
  local-scope
  load-ingredients
  open-console
  hide-screen 0/screen
  new-editor text, 0/screen, 0/left, 5/right
  show-screen 0/screen
  wait-for-event 0/console
  close-console
]

scenario editor-initially-prints-text-to-screen [
  assume-screen 10/width, 5/height
  run [
    1:address:shared:array:character <- new [abc]
    new-editor 1:address:shared:array:character, screen:address:shared:screen, 0/left, 10/right
  ]
  screen-should-contain [
    # top line of screen reserved for menu
    .          .
    .abc       .
    .          .
  ]
]

container editor-data [
  # editable text: doubly linked list of characters (head contains a special sentinel)
  data:address:shared:duplex-list:character
  top-of-screen:address:shared:duplex-list:character
  bottom-of-screen:address:shared:duplex-list:character
  # location before cursor inside data
  before-cursor:address:shared:duplex-list:character

  # raw bounds of display area on screen
  # always displays from row 1 (leaving row 0 for a menu) and at most until bottom of screen
  left:number
  right:number
  bottom:number
  # raw screen coordinates of cursor
  cursor-row:number
  cursor-column:number
]

# creates a new editor widget and renders its initial appearance to screen
#   top/left/right constrain the screen area available to the new editor
#   right is exclusive
recipe new-editor s:address:shared:array:character, screen:address:shared:screen, left:number, right:number -> result:address:shared:editor-data, screen:address:shared:screen [
  local-scope
  load-ingredients
  # no clipping of bounds
  right <- subtract right, 1
  result <- new editor-data:type
  # initialize screen-related fields
  x:address:number <- get-address *result, left:offset
  *x <- copy left
  x <- get-address *result, right:offset
  *x <- copy right
  # initialize cursor
  x <- get-address *result, cursor-row:offset
  *x <- copy 1/top
  x <- get-address *result, cursor-column:offset
  *x <- copy left
  init:address:address:shared:duplex-list:character <- get-address *result, data:offset
  *init <- push 167/§, 0/tail
  top-of-screen:address:address:shared:duplex-list:character <- get-address *result, top-of-screen:offset
  *top-of-screen <- copy *init
  y:address:address:shared:duplex-list:character <- get-address *result, before-cursor:offset
  *y <- copy *init
  result <- insert-text result, s
  # initialize cursor to top of screen
  y <- get-address *result, before-cursor:offset
  *y <- copy *init
  # initial render to screen, just for some old tests
  _, _, screen, result <- render screen, result
  <editor-initialization>
]

recipe insert-text editor:address:shared:editor-data, text:address:shared:array:character -> editor:address:shared:editor-data [
  local-scope
  load-ingredients
  # early exit if text is empty
  reply-unless text, editor/same-as-ingredient:0
  len:number <- length *text
  reply-unless len, editor/same-as-ingredient:0
  idx:number <- copy 0
  # now we can start appending the rest, character by character
  curr:address:shared:duplex-list:character <- get *editor, data:offset
  {
    done?:boolean <- greater-or-equal idx, len
    break-if done?
    c:character <- index *text, idx
    insert c, curr
    # next iter
    curr <- next curr
    idx <- add idx, 1
    loop
  }
  reply editor/same-as-ingredient:0
]

scenario editor-initializes-without-data [
  assume-screen 5/width, 3/height
  run [
    1:address:shared:editor-data <- new-editor 0/data, screen:address:shared:screen, 2/left, 5/right
    2:editor-data <- copy *1:address:shared:editor-data
  ]
  memory-should-contain [
    # 2 (data) <- just the § sentinel
    # 3 (top of screen) <- the § sentinel
    4 <- 0  # bottom-of-screen; null since text fits on screen
    # 5 (before cursor) <- the § sentinel
    6 <- 2  # left
    7 <- 4  # right  (inclusive)
    8 <- 1  # bottom
    9 <- 1  # cursor row
    10 <- 2  # cursor column
  ]
  screen-should-contain [
    .     .
    .     .
    .     .
  ]
]

# Assumes cursor should be at coordinates (cursor-row, cursor-column) and
# updates before-cursor to match. Might also move coordinates if they're
# outside text.
recipe render screen:address:shared:screen, editor:address:shared:editor-data -> last-row:number, last-column:number, screen:address:shared:screen, editor:address:shared:editor-data [
  local-scope
  load-ingredients
  reply-unless editor, 1/top, 0/left, screen/same-as-ingredient:0, editor/same-as-ingredient:1
  left:number <- get *editor, left:offset
  screen-height:number <- screen-height screen
  right:number <- get *editor, right:offset
  # traversing editor
  curr:address:shared:duplex-list:character <- get *editor, top-of-screen:offset
  prev:address:shared:duplex-list:character <- copy curr  # just in case curr becomes null and we can't compute prev
  curr <- next curr
  # traversing screen
  +render-loop-initialization
  color:number <- copy 7/white
  row:number <- copy 1/top
  column:number <- copy left
  cursor-row:address:number <- get-address *editor, cursor-row:offset
  cursor-column:address:number <- get-address *editor, cursor-column:offset
  before-cursor:address:address:shared:duplex-list:character <- get-address *editor, before-cursor:offset
  screen <- move-cursor screen, row, column
  {
    +next-character
    break-unless curr
    off-screen?:boolean <- greater-or-equal row, screen-height
    break-if off-screen?
    # update editor-data.before-cursor
    # Doing so at the start of each iteration ensures it stays one step behind
    # the current character.
    {
      at-cursor-row?:boolean <- equal row, *cursor-row
      break-unless at-cursor-row?
      at-cursor?:boolean <- equal column, *cursor-column
      break-unless at-cursor?
      *before-cursor <- copy prev
    }
    c:character <- get *curr, value:offset
    <character-c-received>
    {
      # newline? move to left rather than 0
      newline?:boolean <- equal c, 10/newline
      break-unless newline?
      # adjust cursor if necessary
      {
        at-cursor-row?:boolean <- equal row, *cursor-row
        break-unless at-cursor-row?
        left-of-cursor?:boolean <- lesser-than column, *cursor-column
        break-unless left-of-cursor?
        *cursor-column <- copy column
        *before-cursor <- prev curr
      }
      # clear rest of line in this window
      clear-line-delimited screen, column, right
      # skip to next line
      row <- add row, 1
      column <- copy left
      screen <- move-cursor screen, row, column
      curr <- next curr
      prev <- next prev
      loop +next-character:label
    }
    {
      # at right? wrap. even if there's only one more letter left; we need
      # room for clicking on the cursor after it.
      at-right?:boolean <- equal column, right
      break-unless at-right?
      # print wrap icon
      wrap-icon:character <- copy 8617/loop-back-to-left
      print screen, wrap-icon, 245/grey
      column <- copy left
      row <- add row, 1
      screen <- move-cursor screen, row, column
      # don't increment curr
      loop +next-character:label
    }
    print screen, c, color
    curr <- next curr
    prev <- next prev
    column <- add column, 1
    loop
  }
  # save first character off-screen
  bottom-of-screen:address:address:shared:duplex-list:character <- get-address *editor, bottom-of-screen:offset
  *bottom-of-screen <- copy curr
  # is cursor to the right of the last line? move to end
  {
    at-cursor-row?:boolean <- equal row, *cursor-row
    cursor-outside-line?:boolean <- lesser-or-equal column, *cursor-column
    before-cursor-on-same-line?:boolean <- and at-cursor-row?, cursor-outside-line?
    above-cursor-row?:boolean <- lesser-than row, *cursor-row
    before-cursor?:boolean <- or before-cursor-on-same-line?, above-cursor-row?
    break-unless before-cursor?
    *cursor-row <- copy row
    *cursor-column <- copy column
    *before-cursor <- copy prev
  }
  bottom:address:number <- get-address *editor, bottom:offset
  *bottom <- copy row
  reply row, column, screen/same-as-ingredient:0, editor/same-as-ingredient:1
]

recipe clear-line-delimited screen:address:shared:screen, column:number, right:number -> screen:address:shared:screen [
  local-scope
  load-ingredients
  space:character <- copy 32/space
  bg-color:number, bg-color-found?:boolean <- next-ingredient
  {
    # default bg-color to black
    break-if bg-color-found?
    bg-color <- copy 0/black
  }
  {
    done?:boolean <- greater-than column, right
    break-if done?
    screen <- print screen, space, 7/white, bg-color  # foreground color is mostly unused except if the cursor shows up at this cell
    column <- add column, 1
    loop
  }
]

recipe clear-screen-from screen:address:shared:screen, row:number, column:number, left:number, right:number -> screen:address:shared:screen [
  local-scope
  load-ingredients
  # if it's the real screen, use the optimized primitive
  {
    break-if screen
    clear-display-from row, column, left, right
    reply screen/same-as-ingredient:0
  }
  # if not, go the slower route
  screen <- move-cursor screen, row, column
  clear-line-delimited screen, column, right
  clear-rest-of-screen screen, row, left, right
  reply screen/same-as-ingredient:0
]

recipe clear-rest-of-screen screen:address:shared:screen, row:number, left:number, right:number -> screen:address:shared:screen [
  local-scope
  load-ingredients
  row <- add row, 1
  screen <- move-cursor screen, row, left
  screen-height:number <- screen-height screen
  {
    at-bottom-of-screen?:boolean <- greater-or-equal row, screen-height
    break-if at-bottom-of-screen?
    screen <- move-cursor screen, row, left
    clear-line-delimited screen, left, right
    row <- add row, 1
    loop
  }
]

scenario editor-initially-prints-multiple-lines [
  assume-screen 5/width, 5/height
  run [
    s:address:shared:array:character <- new [abc
def]
    new-editor s:address:shared:array:character, screen:address:shared:screen, 0/left, 5/right
  ]
  screen-should-contain [
    .     .
    .abc  .
    .def  .
    .     .
  ]
]

scenario editor-initially-handles-offsets [
  assume-screen 5/width, 5/height
  run [
    s:address:shared:array:character <- new [abc]
    new-editor s:address:shared:array:character, screen:address:shared:screen, 1/left, 5/right
  ]
  screen-should-contain [
    .     .
    . abc .
    .     .
  ]
]

scenario editor-initially-prints-multiple-lines-at-offset [
  assume-screen 5/width, 5/height
  run [
    s:address:shared:array:character <- new [abc
def]
    new-editor s:address:shared:array:character, screen:address:shared:screen, 1/left, 5/right
  ]
  screen-should-contain [
    .     .
    . abc .
    . def .
    .     .
  ]
]

scenario editor-initially-wraps-long-lines [
  assume-screen 5/width, 5/height
  run [
    s:address:shared:array:character <- new [abc def]
    new-editor s:address:shared:array:character, screen:address:shared:screen, 0/left, 5/right
  ]
  screen-should-contain [
    .     .
    .abc ↩.
    .def  .
    .     .
  ]
  screen-should-contain-in-color 245/grey [
    .     .
    .    ↩.
    .     .
    .     .
  ]
]

scenario editor-initially-wraps-barely-long-lines [
  assume-screen 5/width, 5/height
  run [
    s:address:shared:array:character <- new [abcde]
# Helpers for parsing SubX words, with their rules for hex, labels and metadata.

== code
#   instruction                     effective address                                                   register    displacement    immediate
# . op          subop               mod             rm32          base        index         scale       r32
# . 1-3 bytes   3 bits              2 bits          3 bits        3 bits      3 bits        2 bits      2 bits      0/1/2/4 bytes   0/1/2/4 bytes

has-metadata?:  # word : (address slice), s : (address string) -> eax : boolean
    # pseudocode:
    #   var twig : &slice = next-token-from-slice(word->start, word->end, '/')  # skip name
    #   curr = twig->end
    #   while true
    #     twig = next-token-from-slice(curr, word->end, '/')
    #     if (twig.empty()) break
    #     if (slice-equal?(twig, s)) return true
    #     curr = twig->end
    #   return false
    # . prolog
    55/push-ebp
    89/copy                         3/mod/direct    5/rm32/ebp    .           .             .           4/r32/esp   .               .                 # copy esp to ebp
    # . save registers
    51/push-ecx
    52/push-edx
    56/push-esi
    57/push-edi
    # esi = word
    8b/copy                         1/mod/*+disp8   5/rm32/ebp    .           .             .           6/r32/esi   8/disp8         .                 # copy *(ebp+8) to esi
    # edx = word->end
    8b/copy                         1/mod/*+disp8   6/rm32/esi    .           .             .           2/r32/edx   4/disp8         .                 # copy *(esi+4) to edx
    # var twig/edi : (address slice) = {0, 0}
    68/push  0/imm32/end
    68/push  0/imm32/start
    89/copy                         3/mod/direct    7/rm32/edi    .           .             .           4/r32/esp   .               .                 # copy esp to edi
    # next-token-from-slice(word->start, word->end, '/', twig)
    # . . push args
    57/push-edi
    68/push  0x2f/imm32/slash
    52/push-edx
    ff          6/subop/push        0/mod/indirect  6/rm32/esi    .           .             .           .           .               .                 # push *esi
    # . . call
    e8/call  next-token-from-slice/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               0x10/imm32        # add to esp
    # curr/ecx = twig->end
    8b/copy                         1/mod/*+disp8   7/rm32/edi    .           .             .           1/r32/ecx   4/disp8         .                 # copy *(edi+4) to ecx
$has-metadata?:loop:
    # next-token-from-slice(curr, word->end, '/', twig)
    # . . push args
    57/push-edi
    68/push  0x2f/imm32/slash
    52/push-edx
    51/push-ecx
    # . . call
    e8/call  next-token-from-slice/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .