--- @name         Notes
--- @description  Save a note, read it back, drop it in
--- @type         oneshot
--- @key          n
--- @requires     document
--- @caps         doc.read, doc.edit, data.read, data.write, ui.alert, ui.list, screen.draw

-- What your character remembers, when you cannot.
--
-- Select the line where you worked out that Marlowe limps, press Ctrl+T n, and
-- save it. Forty pages later, press Ctrl+T n again, read it back, and drop it
-- into the scene if you want it there.
--
-- THE FIRST LINE OF WHAT YOU SELECT IS THE NAME you will see in the list. So
-- select two lines to name a note something other than its own opening words:
--
--     MARLOWE
--     Limps on the left since Cairo. Will not say why.
--
-- There is no separate step for the name, because a script cannot ask you to
-- type one -- alerts and lists are all it has.
--
-- One key, one meaning: it always opens your notes. Having something selected
-- adds a line at the top of the list -- it never changes what the key does.
--
-- Notes live in notes.dat next to this script, blocks separated by a line of
-- three dashes. Edit it on a computer whenever you like.

local SEPARATOR = "---"
local MAX_NOTES = 30        -- ui.list refuses past 32; one row is the save line

local HEADER = {
  "# Character notes.",
  "# One note per block, separated by a line of three dashes.",
  "# The first line of a block is what shows in the list.",
  "",
}

-- Reading -------------------------------------------------------------------

-- nil means do not write: the file is there and we could not read it, so
-- saving would replace notes we never saw. Only "missing" is a safe first run.
local function load()
  local text, why = BYOK.data.read()
  if not text then
    if why == "missing" then return {} end
    return nil, why
  end

  local notes, block = {}, {}

  -- Blank lines at either end are spacing, not part of the note. Leading ones
  -- matter most: the header we write ends in one, so without this every note
  -- saved first in the file comes back named after an empty line.
  local function flush()
    while #block > 0 and block[1]:match("^%s*$")       do table.remove(block, 1) end
    while #block > 0 and block[#block]:match("^%s*$")  do block[#block] = nil end
    if #block > 0 then notes[#notes + 1] = table.concat(block, "\n") end
    block = {}
  end

  for line in (text .. "\n"):gmatch("([^\n]*)\n") do
    if line:match("^%s*" .. SEPARATOR .. "%s*$") then
      flush()
    elseif not line:match("^%s*#") then
      block[#block + 1] = line
    end
  end
  flush()

  return notes
end

local function save(notes)
  local out = {}
  for _, h in ipairs(HEADER) do out[#out + 1] = h end
  for i, note in ipairs(notes) do
    if i > 1 then out[#out + 1] = SEPARATOR end
    out[#out + 1] = note
  end
  return BYOK.data.write(table.concat(out, "\n") .. "\n")
end

local function title(note)
  local first = note:match("^[^\n]*") or ""
  first = first:gsub("^%s+", "")
  return (first == "") and "(untitled)" or first
end

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

-- Wrapped here rather than by the BYOK, which has no wrap for scripts to call.
-- Blank lines are kept: a note is prose, and its paragraphs are part of it.
local function wrap(s, text, width)
  local lines = {}
  for para in (text .. "\n"):gmatch("([^\n]*)\n") do
    if para:match("^%s*$") then
      lines[#lines + 1] = ""
    else
      local line = ""
      for word in para:gmatch("%S+") do
        local try = (line == "") and word or (line .. " " .. word)
        if s.textWidth(try) <= width then
          line = try
        else
          if line ~= "" then lines[#lines + 1] = line end
          line = word          -- a word wider than the pane simply overhangs
        end
      end
      if line ~= "" then lines[#lines + 1] = line end
    end
  end
  return lines
end

-- The pane keeps whatever was drawn and repaints nothing by itself, so every
-- change of scroll or prompt comes back through here.
local function paint(s, lines, top, rows, lh, prompt)
  s.clear()
  for i = 0, rows - 1 do
    local line = lines[top + i]
    if line and line ~= "" then s.text(0, (i + 1) * lh, line) end
  end

  local hintY = s.height() - 1
  s.line(0, hintY - lh - 1, s.width(), hintY - lh - 1)
  s.text(0, hintY, prompt)

  -- Plain letters: the pane's fonts cover the keyboard and little else, and an
  -- arrow the font lacks would draw as a filled diamond.
  if top + rows <= #lines then s.text(s.width() - 8,  hintY, "v") end
  if top > 1              then s.text(s.width() - 16, hintY, "^") end
end

-- Returns "insert", "delete", or nil when the writer backed out.
local function read(s, note)
  local lh    = s.lineHeight()
  local lines = wrap(s, note, s.width())
  local rows  = (s.height() // lh) - 1        -- one row goes to the prompt
  if rows < 1 then rows = 1 end
  local top   = 1

  local prompt = "ret add  d del  esc back"
  paint(s, lines, top, rows, lh, prompt)

  while true do
    local k = s.key(400)
    if not s.isOpen() then return nil end

    if k == "enter" then
      return "insert"
    elseif k == "d" then
      paint(s, lines, top, rows, lh, "delete it? y / n")
      repeat
        local answer = s.key(400)
        if not s.isOpen() then return nil end
        if answer == "y" then return "delete" end
        if answer == "n" then break end
      until false
      paint(s, lines, top, rows, lh, prompt)
    elseif k == "down" and top + rows <= #lines then
      top = top + 1
      paint(s, lines, top, rows, lh, prompt)
    elseif k == "up" and top > 1 then
      top = top - 1
      paint(s, lines, top, rows, lh, prompt)
    elseif s.damaged() then
      paint(s, lines, top, rows, lh, prompt)
    end
  end
end

-- Putting it together --------------------------------------------------------

function main()
  local notes, why = load()
  if not notes then
    BYOK.ui.alert("Notes", why == "toolarge" and "Your notes file is too big"
                                              or "Cannot read your notes")
    return
  end

  -- Read before the list: opening one is a good way to lose a selection.
  local selection = BYOK.doc.selection()

  while true do
    local items, offset = {}, 0
    if selection then
      -- Says where the name comes from, because nothing else does. A writer
      -- who reads this row once never has to be told again.
      items[1] = "+ Save (first line = name)"
      offset = 1
    end
    for _, note in ipairs(notes) do items[#items + 1] = title(note) end

    if #items == 0 then
      BYOK.ui.alert("Notes", "Select something and save it")
      return
    end

    local choice = BYOK.ui.list(items)
    if not choice then return end

    if selection and choice == 1 then
      if #notes >= MAX_NOTES then
        BYOK.ui.alert("Notes", "Full at " .. MAX_NOTES .. " notes")
        return
      end
      table.insert(notes, 1, selection)     -- newest first; you just wrote it
      if not save(notes) then
        BYOK.ui.alert("Notes", "Could not save -- too big?")
      end
      return
    end

    local index = choice - offset
    local note  = notes[index]

    if not BYOK.screen.open() then return end
    local action = read(BYOK.screen, note)
    BYOK.screen.close()

    if action == "insert" then
      -- Replaces the selection if there still is one, exactly as typing would.
      BYOK.doc.insert(note)
      return
    elseif action == "delete" then
      table.remove(notes, index)
      if not save(notes) then
        BYOK.ui.alert("Notes", "Could not save the deletion")
        return
      end
    end
    -- Anything else: escape closed the pane, so back to the list.
  end
end
