# the ink standard library

log := val => out(string(val) + '
')

scan := cb => (
  acc := ['']
  in(evt => evt.type :: {
    'end' -> cb(acc.0)
    'data' -> (
      acc.0 := acc.0 + slice(evt.data, 0, len(evt.data) - 1)
      false
    )
  })
)

# hexadecimal conversion utility functions
hToN := {0: 0, 1: 1, 2: 2, 3: 3, 4: 4, 5: 5, 6: 6, 7: 7, 8: 8, 9: 9, 'a': 10, 'b': 11, 'c': 12, 'd': 13, 'e': 14, 'f': 15}
nToH := '0123456789abcdef'

# take number, return hex string
hex := n => (sub := (p, acc) => p < 16 :: {
  true -> nToH.(p) + acc
  false -> sub(floor(p / 16), nToH.(p % 16) + acc)
})(floor(n), '')

# take hex string, return number
xeh := s => (
  # i is the num of places from the left, 0-indexed
  max := len(s)
  (sub := (i, acc) => i :: {
    max -> acc
    _ -> sub(i + 1, acc * 16 + hToN.(s.(i)))
  })(0, 0)
)

# find minimum in list
min := numbers => reduce(numbers, (acc, n) => n < acc :: {
  true -> n
  false -> acc
}, numbers.0)

# find maximum in list
max := numbers => reduce(numbers, (acc, n) => n > acc :: {
  true -> n
  false -> acc
}, numbers.0)

# like Python's range(), but no optional arguments
range := (start, end, step) => (
  span := end - start
  sub := (i, v, acc) => (v - start) / span < 1 :: {
    true -> (
      acc.(i) := v
      sub(i + 1, v + step, acc)
    )
    false -> acc
  }

  # preempt potential infinite loops
  (end - start) / step > 0 :: {
    true -> sub(0, start, [])
    false -> []
  }
)

# clamp start and end numbers to ranges, such that start < end. Utility used in slice
clamp := (start, end, min, max) => (
  start := (start < min :: {
    true -> min
    false -> start
  })
  end := (end < min :: {
    true -> min
    false -> end
  })
  end := (end > max :: {
    true -> max
    false -> end
  })
  start := (start > end :: {
    true -> end
    false -> start
  })

  {
    start: start
    end: end
  }
)

# get a substring of a given string, or sublist of a given list
slice := (s, start, end) => (
  # bounds checks
  x := clamp(start, end, 0, len(s))
  start := x.start
  max := x.end - start

  (sub := (i, acc) => i :: {
    max -> acc
    _ -> sub(i + 1, acc.(i) := s.(start + i))
  })(0, type(s) :: {
    'string' -> ''
    'composite' -> []
  })
)

# join one list to the end of another, return the original first list
append := (base, child) => (
  baseLength := len(base)
  childLength := len(child)
  (sub := i => i :: {
    childLength -> base
    _ -> (
      base.(baseLength + i) := child.(i)
      sub(i + 1)
    )
  })(0)
)

# join one list to the end of another, return the third list
join := (base, child) => append(clone(base), child)

# clone a composite value
clone := x => type(x) :: {
  'string' -> '' + x
  'composite' -> reduce(keys(x), (acc, k) => acc.(k) := x.(k), {})
  _ -> x
}

# tail recursive numeric list -> string converter
stringList := list => '[' + cat(map(list, string), ', ') + ']'

# tail recursive reversing a list
reverse := list => (sub := (acc, i, j) => j :: {
  0 -> acc.(i) := list.0
  _ -> sub(acc.(i) := list.(j), i + 1, j - 1)
})([], 0, len(list) - 1)

# tail recursive map
map := (list, f) => reduce(list, (l, item, i) => l.(i) := f(item, i), {})

# tail recursive filter
filter := (list, f) => reduce(list, (l, item, i) => f(item, i) :: {
  true -> l.len(l) := item
  _ -> l
}, [])

# tail recursive reduce
reduce := (list, f, acc) => (
  max := len(list)
  (sub := (i, acc) => i :: {
    max -> acc
    _ -> sub(i + 1, f(acc, list.(i), i))
  })(0, acc)
)

# tail recursive reduce from list end
reduceBack := (list, f, acc) => (sub := (i, acc) => i :: {
  ~1 -> acc
  _ -> sub(i - 1, f(acc, list.(i), i))
})(len(list) - 1, acc)

# flatten by depth 1
flatten := list => reduce(list, append, [])

# true iff some items in list are true
some := list => reduce(list, (acc, x) => acc | x, false)

# true iff every item in list is true
every := list => reduce(list, (acc, x) => acc & x, true)

# concatenate (join) a list of strings into a string
cat := (list, joiner) => max := len(list) :: {
  0 -> ''
  _ -> (sub := (i, acc) => i :: {
    max -> acc
    _ -> sub(i + 1, acc.len(acc) := joiner + list.(i))
  })(1, clone(list.0))
}

# for-each loop over a list
each := (list, f) => (
  max := len(list)
  (sub := i => i :: {
    max -> ()
    _ -> (
      f(list.(i), i)
      sub(i + 1)
    )
  })(0)
)

# parallel for-each loop over a list
peach := (list, f) => (
  par(map(list, (item, i) => () => f(item, i)))
)

# encode string buffer into a number list
encode := str => (
  max := len(str)
  (sub := (i, acc) => i :: {
    max -> acc
    _ -> sub(i + 1, acc.(i) := point(str.(i)))
  })(0, [])
)

# decode number list into an ascii string
decode := data => reduce(data, (acc, cp) => acc.len(acc) := char(cp), '')

# utility for reading an entire file
readFile := (path, cb) => (
  BufSize := 4096 # bytes
  (sub := (offset, acc) => read(path, offset, BufSize, evt => evt.type :: {
    'error' -> cb(())
    'data' -> (
      dataLen := len(evt.data)
      dataLen = BufSize :: {
        true -> sub(offset + dataLen, acc.len(acc) := evt.data)
        false -> cb(acc.len(acc) := evt.data)
      }
    )
  }))(0, '')
)

# utility for writing an entire file
# it's not buffered, because it's simpler, but may cause jank later
# we'll address that if/when it becomes a performance issue
writeFile := (path, data, cb) => delete(path, evt => evt.type :: {
  # write() by itself will not truncate files that are too long,
  # so we delete the file and re-write. Not efficient, but writeFile
  # is not meant for large files
  'end' -> write(path, 0, data, evt => evt.type :: {
    'error' -> cb(())
    'end' -> cb(true)
  })
  _ -> cb(())
})

# template formatting with {{ key }} constructs
format := (raw, values) => (
  # parser state
  state := {
    # current position in raw
    idx: 0
    # parser internal state:
    # 0 -> normal
    # 1 -> seen one {
    # 2 -> seen two {
    # 3 -> seen a valid }
    which: 0
    # buffer for currently reading key
    key: ''
    # result build-up buffer
    buf: ''
  }

  # helper function for appending to state.buf
  append := c => state.buf := state.buf + c

  # read next token, update state
  readNext := () => (
    c := raw.(state.idx)

    state.which :: {
      0 -> c :: {
        '{' -> state.which := 1
        _ -> append(c)
      }
      1 -> c :: {
        '{' -> state.which := 2
        # if it turns out that earlier brace was not
        # a part of a format expansion, just backtrack
        _ -> (
          append('{' + c)
          state.which := 0
        )
      }
      2 -> c :: {
        '}' -> (
          # insert key value
          state.buf := state.buf + string(values.(state.key))
          state.key := ''
          state.which := 3
        )
        # ignore spaces in keys -- not allowed
        ' ' -> ()
        _ -> state.key := state.key + c
      }
      3 -> c :: {
        '}' -> state.which := 0
        # ignore invalid inputs -- treat them as nonexistent
        _ -> ()
      }
    }

    state.idx := state.idx + 1
  )

  # main recursive sub-loop
  max := len(raw)
  (sub := () => state.idx < max :: {
    true -> (
      readNext()
      sub()
    )
    false -> state.buf
  })()
)

pipe := (x, fs) => reduce(fs, (a, f, _) => f(a), x)

{
  log: log
  scan: scan
  hToN: hToN
  nToH: nToH
  hex: hex
  xeh: xeh
  min: min
  max: max
  range: range
  clamp: clamp
  slice: slice
  append: append
  join: join
  clone: clone
  stringList: stringList
  reverse: reverse
  map: map
  filter: filter
  reduce: reduce
  reduceBack: reduceBack
  flatten: flatten
  some: some
  every: every
  cat: cat
  each: each
  peach: peach
  encode: encode
  decode: decode
  readFile: readFile
  writeFile: writeFile
  format: format
  pipe: pipe
}