/*
  Keep track of a running note counter, and associated notes.
*/

#let note_state_prefix = "notes-"
#let note_default_group = "default"

#let note_default_display_fn(note) = {
  h(0pt, weak: true)
  super([[#note.index]])
}

#let add_note(
  // The location of the note. This is used to derive
  // what the note counter should be for this note.
  loc,
  // The note itself.
  text,
  // The offset which will be added to the 0-based counter index
  // when the index stored in the state.
  offset: 1,
  // the display function that creates the returned content from put.
  // Put can't return a pure index number because the counter
  // and state updates need to output content.
  display: note_default_display_fn,
  // The state group to track notes in.
  // A group acts independent (both counter and set of notes)
  // from other groups.
  group: note_default_group
) = {
  let s = state(note_state_prefix + group, ())
  let c = counter(note_state_prefix + group)
  // find any existing note that hasn't been printed yet,
  // containing the exact same text:
  let existing = s.at(loc).filter((n) => n.text == text)
  // If we found an existing note use that,
  // otherwise the note is the current location's counter + offset
  // and the given text (the counter is 0-based, we want 1-based indices)
  let note = if existing.len() > 0 {
    existing.first()
  } else {
    (text: text, index: c.at(loc).first() + offset, page: loc.page())
  }

  // If we didn't find an existing index, increment the counter
  // and add the note to the "notes" state.
  if existing.len() == 0 {
    c.step()
    s.update(notes => notes + (note,))
  }
  
  // Output the note marker
  display(note)
}

// get notes at specific location
#let get_notes(loc, group: note_default_group) = {
  state(note_state_prefix + group, ()).at(loc)
}

// Reset the note group to empty.
// Note: The counter does not reset by default.
#let reset_notes(group: note_default_group, reset_counter: false) = {
  if reset_counter {
    counter(note_state_prefix + group).update(0)
  }
  state(note_state_prefix + group, ()).update(())
}

//
// Helpers for nicer in-document ergonomics
//

#let render_notes(fn, group: note_default_group, reset: true, reset_counter: false) = {
  locate(loc => fn(get_notes(loc, group: group)))
  if reset {
    reset_notes(group: group, reset_counter: reset_counter)
  }
}

// Create a new note at the current location
#let note(note, ..args) = {
  locate((loc) => add_note(loc, note, ..args))
}

// The quick-start option that outputs something useful by default.
// This is a sane-defaults call to `render_notes`.
#let notes(
  size: 8pt,
  font: "Roboto",
  line: line(length: 100%, stroke: 1pt + gray),
  padding: (top: 3mm),
  alignment: bottom,
  numberings: "1",
  group: note_default_group,
  reset: true,
  reset_counter: false
) = {
  let render(notes) = {
    if notes.len() > 0 {
      set align(alignment)
      block(breakable: false, pad(..padding, {
        if line != none { line }
        set text(size: size, font: font)
        for note in notes {
          [/ #text(font: "Roboto Mono")[[#numbering(numberings, note.index)]]: #note.text]
        }
      }))
    }
  }
  render_notes(group: group, reset: reset, reset_counter: reset_counter, render)
}

//
// Examples
//

#set page(height: 5cm, width: 15cm)

= Example

- Having a flexible note-keeping system is important #note[Citation needed]
- It's pretty easy to implement with Typst #note[Fact]
- Everyone really likes the name "Typst" #note[Citation needed]



// Print notes since last print:
#notes()
#pagebreak()

These notes won't be printed on this page, so they accumulate onto next page.

- Hello World #note[Your first program]
- Foo Bar #note[Does not serve beverages]
#pagebreak()

- Orange #note[A Color]
- Blue #note[A Color]
// Notice that the "Citation needed" gets a new index
// because we've re-used it since we printed the initial "Citation needed"
- All colors are great #note[Citation needed]
#notes()
#pagebreak()

#set page(
  height: 8cm,
  footer: notes(padding: (bottom: 5mm)),
  margin: (rest: 5mm, bottom: 3cm)
)

= Footnotes

It is of course also possible to place the notes _in_ the page footer.
This is the way to implement footnotes.

- Black#note[Debateably a color]
- White#note[Debateably a color]
#pagebreak()

- Orange#note[A Color]
- Blue#note[A Color]
- Purple#note[Also a color]

#pagebreak()

= Note groups

This page still has footnotes (using the default note group)
but we can also name a new group for custom notes.

#let mynote = note.with(group: "custom")
#let mynotes = notes.with(
  group: "custom",
  font: "Comic Neue",
  size: 12pt,
  numberings: "a.",
  line: none,
  alignment: right + top
)

- This is in it's own group#mynote[Custom]
- Regular footnote here#note[Regular footnote]
- Same custom note#mynote[Custom]

#mynotes()