(*---------------------------------------------------------------------------
   Copyright (c) 2016 Daniel C. Bünzli. All rights reserved.
   SPDX-License-Identifier: ISC
  ---------------------------------------------------------------------------*)

let strf = Format.asprintf

include String

let head s = if s = "" then None else Some s.[0]

(* Predicates *)

let is_prefix ~affix s =
  let len_a = length affix in
  let len_s = length s in
  if len_a > len_s then false else
  let max_idx_a = len_a - 1 in
  let rec loop i =
    if i > max_idx_a then true else
    if unsafe_get affix i <> unsafe_get s i then false else loop (i + 1)
  in
  loop 0

let is_suffix ~affix s =
  let max_idx_a = length affix - 1 in
  let max_idx_s = length s - 1 in
  if max_idx_a > max_idx_s then false else
  let rec loop i =
    if i > max_idx_a then true else
    if unsafe_get affix (max_idx_a - i) <> unsafe_get s (max_idx_s - i)
    then false
    else loop (i + 1)
  in
  loop 0

let for_all sat s =
  let max_idx = length s - 1 in
  let rec loop i =
    if i > max_idx then true else
    if sat (unsafe_get s i) then loop (i + 1) else false
  in
  loop 0

let exists sat s =
  let max_idx = length s - 1 in
  let rec loop i =
    if i > max_idx then false else
    if sat (unsafe_get s i) then true else loop (i + 1)
  in
  loop 0

(* Traversing *)

let find_byte ?(start = 0) c s =
  let max = String.length s - 1 in
  if start > max then None else
  try Some (String.index_from s start c) with Not_found -> None

(* Extracting substrings *)

let with_index_range ?(first = 0) ?last s =
  let max = String.length s - 1 in
  let last = match last with
  | None -> max
  | Some l when l > max -> max
  | Some l -> l
  in
  let first = if first < 0 then 0 else first in
  if first > last then "" else
  String.sub s first (last - first + 1)

let cut ?(rev = false) ~sep s =
  let find_index = if rev then String.rindex else String.index in
  match try Some (find_index s sep) with Not_found -> None with
  | None -> None
  | Some i ->
      Some (String.sub s 0 i, String.sub s (i+1) (String.length s - i - 1))

let cuts ?(empty = true) ~sep s =
  let no_empty = not empty in
  let rec loop acc s = match cut ~sep s with
  | Some (v, vs) -> loop (if no_empty && v = "" then acc else (v :: acc)) vs
  | None -> List.rev (if no_empty && s = "" then acc else (s :: acc))
  in
  loop [] s

(* Version strings *)

let parse_version v =
  let version =
    if is_prefix ~affix:"v" v then with_index_range ~first:1 v else v
  in
  let cut_left_plus_or_tilde s =
    let cut = match String.index_opt s '+', String.index_opt s '~' with
    | None, None -> None
    | (Some _ as i), None | None, (Some _ as i) -> i
    | Some i, Some i' -> Some (if i < i' then i else i')
    in
    match cut with
    | None -> None
    | Some i ->
        Some (with_index_range ~last:(i - 1) s, with_index_range ~first:i s)
  in
  try match cut ~sep:'.' version with
  | None -> None
  | Some (maj, rest) ->
      let maj = int_of_string maj in
      match cut ~sep:'.' rest with
      | None ->
          begin match cut_left_plus_or_tilde rest with
          | None -> Some (maj, int_of_string rest, 0, None)
          | Some (min, i) ->  Some (maj, int_of_string min, 0, Some i)
          end
      | Some (min, rest) ->
          let min = int_of_string min in
          begin match cut_left_plus_or_tilde rest with
          | None -> Some (maj, min, int_of_string rest, None)
          | Some (p, i) -> Some (maj, min, int_of_string p, Some i)
          end
  with
  | Failure _ -> None

let drop_initial_v version = match head version with
| Some ('v' | 'V') -> with_index_range ~first:1 version
| None | Some _ -> version

(* Formatters *)

let pp_text ppf s = (* was c&p from Fmt, pp_print_text is 4.02 *)
  let is_nl_or_sp c = c = '\n' || c = ' ' in
  let rec stop_at sat ~start ~max s =
    if start > max then start else
    if sat s.[start] then start else
    stop_at sat ~start:(start + 1) ~max s
  in
  let sub s start stop ~max =
    if start = stop then "" else
    if start = 0 && stop > max then s else
    String.sub s start (stop - start)
  in
  let max = String.length s - 1 in
  let rec loop start s = match stop_at is_nl_or_sp ~start ~max s with
  | stop when stop > max -> Format.pp_print_string ppf (sub s start stop ~max)
  | stop ->
      Format.pp_print_string ppf (sub s start stop ~max);
      begin match s.[stop] with
      | ' ' -> Format.pp_print_space ppf ()
      | '\n' -> Format.pp_force_newline ppf ()
      | _ -> assert false
      end;
      loop (stop + 1) s
  in
  loop 0 s
