let all ?(pos = 0) ?len re s : _ Seq.t =
  if pos < 0 then invalid_arg "Re.all";
  (* index of the first position we do not consider.
     !pos < limit is an invariant *)
  let limit =
    match len with
    | None -> String.length s
    | Some l ->
      if l < 0 || pos + l > String.length s then invalid_arg "Re.all";
      pos + l
  in
  (* iterate on matches. When a match is found, search for the next
     one just after its end *)
  let rec aux pos on_match () =
    if pos > limit
    then Seq.Nil (* no more matches *)
    else (
      match
        Compile.match_str ~groups:true ~partial:false re s ~pos ~len:(limit - pos)
      with
      | Match substr ->
        let p1 = Group.start_offset substr 0 |> Group.Offset.get_no_check in
        let p2 = Group.stop_offset substr 0 |> Group.Offset.get_no_check in
        if on_match && p1 = pos && p1 = p2
        then (* skip empty match right after a match *)
          aux (pos + 1) false ()
        else (
          let pos = if p1 = p2 then p2 + 1 else p2 in
          Seq.Cons (substr, aux pos (p1 <> p2)))
      | Running _ | Failed -> Seq.Nil)
  in
  aux pos false
;;

let matches ?pos ?len re s : _ Seq.t =
  all ?pos ?len re s |> Seq.map (fun sub -> Group.get sub 0)
;;

let split_full ?(pos = 0) ?len re s : _ Seq.t =
  if pos < 0 then invalid_arg "Re.split";
  let limit =
    match len with
    | None -> String.length s
    | Some l ->
      if l < 0 || pos + l > String.length s then invalid_arg "Re.split";
      pos + l
  in
  (* i: start of delimited string
     pos: first position after last match of [re]
     limit: first index we ignore (!pos < limit is an invariant) *)
  let pos0 = pos in
  let rec aux state i pos () =
    match state with
    | `Idle when pos > limit ->
      (* We had an empty match at the end of the string *)
      assert (i = limit);
      Seq.Nil
    | `Idle ->
      (match
         Compile.match_str ~groups:true ~partial:false re s ~pos ~len:(limit - pos)
       with
       | Match substr ->
         let p1 = Group.start_offset substr 0 |> Group.Offset.get_no_check in
         let p2 = Group.stop_offset substr 0 |> Group.Offset.get_no_check in
         let pos = if p1 = p2 then p2 + 1 else p2 in
         let old_i = i in
         let i = p2 in
         if old_i = p1 && p1 = p2 && p1 > pos0
         then (* Skip empty match right after a delimiter *)
           aux state i pos ()
         else if p1 > pos0
         then (
           (* string does not start by a delimiter *)
           let text = String.sub s old_i (p1 - old_i) in
           let state = `Yield (`Delim substr) in
           Seq.Cons (`Text text, aux state i pos))
         else Seq.Cons (`Delim substr, aux state i pos)
       | Running _ -> Seq.Nil
       | Failed ->
         if i < limit
         then (
           let text = String.sub s i (limit - i) in
           (* yield last string *)
           Seq.Cons (`Text text, aux state limit pos))
         else Seq.Nil)
    | `Yield x -> Seq.Cons (x, aux `Idle i pos)
  in
  aux `Idle pos pos
;;

let split ?pos ?len re s : _ Seq.t =
  let seq = split_full ?pos ?len re s in
  let rec filter seq () =
    match seq () with
    | Seq.Nil -> Seq.Nil
    | Seq.Cons (`Delim _, tl) -> filter tl ()
    | Seq.Cons (`Text s, tl) -> Seq.Cons (s, filter tl)
  in
  filter seq
;;

let split_delim ?pos ?len re s : _ Seq.t =
  let seq = split_full ?pos ?len re s in
  let rec filter ~delim seq () =
    match seq () with
    | Seq.Nil -> if delim then Seq.Cons ("", fun () -> Seq.Nil) else Seq.Nil
    | Seq.Cons (`Delim _, tl) ->
      if delim
      then Seq.Cons ("", fun () -> filter ~delim:true tl ())
      else filter ~delim:true tl ()
    | Seq.Cons (`Text s, tl) -> Seq.Cons (s, filter ~delim:false tl)
  in
  filter ~delim:true seq
;;
