--- @name         Cheat sheet
--- @description  Hotkeys and capture syntax, on the device
--- @type         oneshot
--- @key          h
--- @requires     none
--- @caps         screen.draw

-- The device manual's Controls and Hotkeys page, and the Studio capture
-- syntax, readable without leaving the keyboard.
--
--   Ctrl+T h opens the contents. Then:
--     1-9          jump to that section
--     Left/Right   previous / next section
--     0            back to contents
--     Up/Down      scroll one line
--     PgUp/PgDn    scroll one screen
--     Escape       close
--   The keys are shown in a footer on every page.
--
-- Everything shown is in the SECTIONS table below. Two kinds of row:
--   { "KEY", "what it does" }   two columns, key on the left
--   "plain text"                one line, wrapped to the screen
-- Keep hotkeys in step with the manual; keep capture rules in step with
-- the Studio parser.

local SECTIONS = {
  { title = "General controls", rows = {
    { "CTRL+SPACE",     "Show / hide the status bar" },
    { "CTRL+L",         "Cycle backlight brightness" },
    { "CTRL+N",         "New file in this project" },
    { "CTRL+SHIFT+N",   "New project" },
    { "CTRL+O",         "Open file list" },
    { "CTRL+SHIFT+O",   "Open project list" },
    { "CTRL+S",         "Sync now" },
    { "ESC",            "Open the menu (or close it)" },
    { "ENTER / ARROWS", "Choose in a menu" },
  }},
  { title = "Selecting & editing", rows = {
    { "SHIFT+ARROWS",     "Select text" },
    { "SHIFT+HOME / END", "Select to start / end of line" },
    { "CTRL+SHIFT+ARROW", "Add a word to the selection" },
    { "CTRL+C",           "Copy" },
    { "CTRL+X",           "Cut" },
    { "CTRL+V",           "Paste" },
    { "CTRL+Z",           "Undo the last action (one step)" },
    { "CTRL+BACKSPACE",   "Delete the word before" },
    { "CTRL+DELETE",      "Delete the word after" },
    { "TAB",              "Five spaces" },
    "The clipboard holds about 8 KB; a very large selection may not copy whole.",
  }},
  { title = "Moving around", rows = {
    { "ARROWS",         "Move the cursor" },
    { "HOME / END",     "Start / end of line" },
    { "PAGE UP / DOWN", "Up / down one screen" },
    { "CTRL+LEFT",      "Back one word" },
    { "CTRL+RIGHT",     "Forward one word" },
    { "CTRL+B",         "Top of the file (or CTRL+HOME)" },
    { "CTRL+E",         "End of the file (or CTRL+END)" },
    "The arrow buttons on the back of the device page up and down.",
  }},
  { title = "Find", rows = {
    { "CTRL+F", "Find: type the words, press ENTER" },
    { "CTRL+G", "Jump to the next match" },
    "Find is case-sensitive. It wraps to the top when it reaches the end.",
  }},
  { title = "Scripts", rows = {
    { "CTRL+T, letter", "Run the script with that letter" },
    { "CTRL+T, ?",      "Open the script list" },
    { "CTRL+.",         "Stop a running script" },
    { "ESC",            "Close a script's window" },
    "In the script list: SPACE opens a script's details, SPACE again switches a watcher On or Off, ENTER confirms.",
    "A script you have just copied to the card appears after a restart.",
  }},
  { title = "Captures: commands", rows = {
    "Type on the device; Studio files it when you sync.",
    { "::card",       "A card. title: line optional" },
    { "::wiki",       "A wiki entry. title: line optional" },
    { "::outline",    "Beats added to an outline" },
    { "::note",       "A Scratch Pad note" },
    { "::task",       "A checklist of - items" },
    { "::meta",       "Folder, tags, description for this file" },
    { "::as outline", "Whole file is an outline (line 1 only)" },
    "Add @handle after a command to route it: ::card @plot",
  }},
  { title = "Captures: rules", rows = {
    "The command sits alone on its line, at the start of the line.",
    "The body starts on the next line.",
    "A BLANK LINE ends the capture. Without it, your next paragraph is taken too.",
    "title: goes on the first body line (card and wiki).",
    "::task ends at the first line that is not a - item.",
    "Text after the command (::card The door) turns the whole thing into a plain note.",
    "@handle is letters, digits, - and _. No spaces.",
    "Upper or lower case: ::Card and ::card are the same.",
  }},
  { title = "Writing an outline", rows = {
    { "^ text",   "Header" },
    { "^^ text",  "Subheader" },
    { "--- text", "Detail" },
    "Or bullets: - for a header, two spaces then - for a subheader, four spaces then - for a detail.",
    "Three levels only. A detail needs a subheader above it.",
    "::as outline on line 1 makes the whole file one outline. An optional title: line may follow it.",
  }},
  { title = "Comments & file details", rows = {
    "[these words] ::comment your note",
    "Comment on the bracketed words. The note runs to the end of the line, so press ENTER after it.",
    "::comment your note",
    "On its own line: comments on the paragraph above.",
    "Brackets and ::comment lines are removed when you sync.",
    "",
    "::meta",
    "folder: drafts",
    "tags: @act-one, mara",
    "description: Notes from the train",
    "Each line optional. Applies to this file and is removed on sync.",
  }},
}

local MARGIN = 3
local GAP    = 6      -- between the key column and the text column

-- Layout ------------------------------------------------------------------------

local function wrap(s, text, width)
  local out, line = {}, ""
  for word in text:gmatch("%S+") do
    local try = (line == "") and word or (line .. " " .. word)
    if s.textWidth(try) <= width then
      line = try
    else
      if line ~= "" then out[#out + 1] = line end
      line = word
    end
  end
  if line ~= "" then out[#out + 1] = line end
  if #out == 0 then out[1] = "" end
  return out
end

-- Turns a section into display lines: { x = px, text = str }. Two-column
-- rows put the key at MARGIN and the text after the widest key in the
-- section, wrapping continuation lines under the text.
local function layout(s, section, w)
  local keyw = 0
  for _, row in ipairs(section.rows) do
    if type(row) == "table" then
      local tw = s.textWidth(row[1])
      if tw > keyw then keyw = tw end
    end
  end
  local limit = (w * 45) // 100
  if keyw > limit then keyw = limit end
  local col = MARGIN + keyw + GAP

  local lines = {}
  for _, row in ipairs(section.rows) do
    if type(row) == "table" then
      local body = wrap(s, row[2], w - col - MARGIN)
      lines[#lines + 1] = { x = MARGIN, text = row[1], x2 = col, text2 = body[1] }
      for i = 2, #body do lines[#lines + 1] = { x = col, text = body[i] } end
    else
      for _, l in ipairs(wrap(s, row, w - MARGIN * 2)) do
        lines[#lines + 1] = { x = MARGIN, text = l }
      end
    end
  end
  return lines
end

local function contents(s, w)
  local lines = {}
  for i, sec in ipairs(SECTIONS) do
    lines[#lines + 1] = { x = MARGIN, text = i .. "  " .. sec.title }
  end
  return lines
end

-- Drawing ---------------------------------------------------------------------

-- The bar is a little taller than a line and the body starts a few
-- pixels under it. Drawing a letter clears its whole cell first, so a
-- body row touching the bar would punch white notches out of it.
local BAR_PAD = 2

-- Rows of body text between the title bar and the footer.
local function body_rows(s)
  local lh = s.lineHeight()
  local rows = (s.height() - (lh + BAR_PAD * 2) - lh) // lh
  if rows < 1 then rows = 1 end
  return rows
end

local function paint(s, view)
  local w, h, lh = s.width(), s.height(), s.lineHeight()
  s.clear()

  -- Title bar, inverted.
  local bar_h = lh + BAR_PAD * 2
  local title = view.index == 0 and "Cheat sheet" or SECTIONS[view.index].title
  s.text(MARGIN, BAR_PAD + lh - 1, title)
  if view.index > 0 then
    local pos = view.index .. "/" .. #SECTIONS
    s.text(w - s.textWidth(pos) - MARGIN, BAR_PAD + lh - 1, pos)
  end
  s.invert(0, 0, w - 1, bar_h - 1)

  local rows = body_rows(s)
  local top_y = bar_h + BAR_PAD
  for r = 1, rows do
    local line = view.lines[view.top + r - 1]
    if not line then break end
    local y = top_y + r * lh - 1
    if line.text ~= "" then s.text(line.x, y, line.text) end
    if line.text2 and line.text2 ~= "" then s.text(line.x2, y, line.text2) end
  end

  -- Footer: always there, so the keys are never something to hunt for.
  -- More-above / more-below markers sit at its right end.
  local hint = (view.index == 0) and "Press a number   Esc closes"
                                  or "1-9 jump   < > next   0 top   Esc"
  s.text(MARGIN, h - 1, hint)
  local markers = ""
  if view.top > 1 then markers = markers .. "^" end
  if view.top + rows - 1 < #view.lines then markers = markers .. "v" end
  if markers ~= "" then s.text(w - s.textWidth(markers) - MARGIN, h - 1, markers) end
end

local function show(s, view, index)
  view.index = index
  view.top = 1
  if index == 0 then view.lines = contents(s, s.width())
  else view.lines = layout(s, SECTIONS[index], s.width()) end
  paint(s, view)
end

function main()
  if not BYOK.screen.open() then return end
  local s = BYOK.screen
  s.setFont("small")

  local view = { index = 0, top = 1, lines = {} }
  show(s, view, 0)

  while s.isOpen() do
    local k = s.key(1000)
    if not s.isOpen() then break end
    if s.damaged() then paint(s, view) end
    if k == nil then
      -- nothing pressed; the pane keeps its pixels
    else
      local rows = body_rows(s)
      local last_top = math.max(1, #view.lines - rows + 1)
      local n = tonumber(k)
      if n and n >= 1 and n <= #SECTIONS then
        show(s, view, n)
      elseif k == "0" or k == "home" then
        show(s, view, 0)
      elseif k == "right" or k == "enter" or k == " " then
        show(s, view, (view.index % #SECTIONS) + 1)
      elseif k == "left" then
        show(s, view, (view.index - 2) % #SECTIONS + 1)
      elseif k == "down" then
        if view.top < last_top then view.top = view.top + 1 paint(s, view) end
      elseif k == "up" then
        if view.top > 1 then view.top = view.top - 1 paint(s, view) end
      elseif k == "pgdn" then
        if view.top < last_top then view.top = math.min(last_top, view.top + rows) paint(s, view) end
      elseif k == "pgup" then
        if view.top > 1 then view.top = math.max(1, view.top - rows) paint(s, view) end
      end
    end
  end
end
