(*
 * Copyright (c) 2012 Anil Madhavapeddy <anil@recoil.org>
 *
 * Permission to use, copy, modify, and distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 *)

type buffer = (char, Bigarray.int8_unsigned_elt, Bigarray.c_layout) Bigarray.Array1.t

(* Note:
 *
 * We try to maintain the property that no constructed [t] can ever point out of
 * its underlying buffer. This property is guarded by all of the constructing
 * functions and the fact that the type is private, and used by various
 * functions that would otherwise be completely unsafe.
 *
 * Furthermore, no operation on [t] is allowed to extend the view on the
 * underlying Bigarray structure, only narrowing is allowed.
 *
 * All well-intended souls are kindly invited to cross-check that the code
 * indeed maintains this invariant.
 *)

type t = {
  buffer: buffer;
  off   : int;
  len   : int;
}

let pp_t ppf t =
  Format.fprintf ppf "[%d,%d](%d)" t.off t.len (Bigarray.Array1.dim t.buffer)
let string_t ppf str =
  Format.fprintf ppf "[%d]" (String.length str)
let bytes_t ppf str =
  Format.fprintf ppf "[%d]" (Bytes.length str)

let err fmt =
  let b = Buffer.create 20 in                         (* for thread safety. *)
  let ppf = Format.formatter_of_buffer b in
  let k ppf = Format.pp_print_flush ppf (); invalid_arg (Buffer.contents b) in
  Format.kfprintf k ppf fmt

let err_of_bigarray t = err "Cstruct.of_bigarray off=%d len=%d" t
let err_sub t = err "Cstruct.sub: %a off=%d len=%d" pp_t t
let err_shift t = err "Cstruct.shift %a %d" pp_t t
let err_shiftv n = err "Cstruct.shiftv short by %d" n
let err_copy_to_string caller t = err "Cstruct.%s %a off=%d len=%d" caller pp_t t
let err_to_hex_string t = err "Cstruct.to_hex_string %a off=%d len=%d" pp_t t
let err_blit_src src dst =
  err "Cstruct.blit src=%a dst=%a src-off=%d len=%d" pp_t src pp_t dst
let err_blit_dst src dst =
  err "Cstruct.blit src=%a dst=%a dst-off=%d len=%d" pp_t src pp_t dst
let err_blit_from_string_src src dst =
  err "Cstruct.blit_from_string src=%a dst=%a src-off=%d len=%d"
    string_t src pp_t dst
let err_blit_from_string_dst src dst =
  err "Cstruct.blit_from_string src=%a dst=%a dst-off=%d len=%d"
    string_t src pp_t dst
let err_blit_from_bytes_src src dst =
  err "Cstruct.blit_from_bytes src=%a dst=%a src-off=%d len=%d"
    bytes_t src pp_t dst
let err_blit_from_bytes_dst src dst =
  err "Cstruct.blit_from_bytes src=%a dst=%a dst-off=%d len=%d"
    bytes_t src pp_t dst
let err_blit_to_bytes_src src dst =
  err "Cstruct.blit_to_bytes src=%a dst=%a src-off=%d len=%d"
    pp_t src bytes_t dst
let err_blit_to_bytes_dst src dst=
  err "Cstruct.blit_to_bytes src=%a dst=%a dst-off=%d len=%d"
    pp_t src bytes_t dst
let err_invalid_bounds f =
  err "invalid bounds in Cstruct.%s %a off=%d len=%d" f pp_t [@@inline never]
let err_split t = err "Cstruct.split %a start=%d off=%d" pp_t t
let err_iter t = err "Cstruct.iter %a i=%d len=%d" pp_t t

let of_bigarray ?(off=0) ?len buffer =
  let dim = Bigarray.Array1.dim buffer in
  let len =
    match len with
    | None     -> dim - off
    | Some len -> len in
  if off < 0 || len < 0 || off + len < 0 || off + len > dim then err_of_bigarray off len
  else { buffer; off; len }

let to_bigarray buffer =
  Bigarray.Array1.sub buffer.buffer buffer.off buffer.len

let create_unsafe len =
  let buffer = Bigarray.(Array1.create char c_layout len) in
  { buffer ; len ; off = 0 }

let check_bounds t len =
  len >= 0 && Bigarray.Array1.dim t.buffer >= len

let empty = create_unsafe 0

external check_alignment_bigstring : buffer -> int -> int -> bool = "caml_check_alignment_bigstring"

let check_alignment t alignment =
  if alignment > 0 then
    check_alignment_bigstring t.buffer t.off alignment
  else invalid_arg "check_alignment must be positive integer"

type byte = char

let byte (i:int) : byte = Char.chr i
let byte_to_int (b:byte) = int_of_char b

type uint8 = int
type uint16 = int
type uint32 = int32
type uint64 = int64

let debug t =
  let max_len = Bigarray.Array1.dim t.buffer in
  if t.off+t.len > max_len || t.len < 0 || t.off < 0 then (
    Format.printf "ERROR: t.off+t.len=%d %a\n%!" (t.off+t.len) pp_t t;
    assert false;
  ) else
    Format.asprintf "%a" pp_t t

let sub t off len =
  (* from https://github.com/mirage/ocaml-cstruct/pull/245

     Cstruct.sub should select what a programmer intuitively expects a
     sub-cstruct to be. I imagine holding out my hands, with the left
     representing the start offset and the right the end. I think of a
     sub-cstruct as any span within this range. If I move my left hand only to
     the right (new_start >= t.off), and my right hand only to the left
     (new_end <= old_end), and they don't cross (new_start <= new_end), then I
     feel sure the result will be a valid sub-cstruct. And if I violate any one
     of these constraints (e.g. moving my left hand further left), then I feel
     sure that the result wouldn't be something I'd consider to be a sub-cstruct.

     Wrapping considerations in modular arithmetic:

     Note that if x is non-negative, and x + y wraps, then x + y must be
     negative. This is easy to see with modular arithmetic because if y is
     negative then the two arguments will cancel to some degree the result
     cannot be further from zero than one of the arguments. If y is positive
     then x + y can wrap, but even max_int + max_int doesn't wrap all the way to
     zero.

     The three possibly-wrapping operations are:

     new_start = t.off + off. t.off is non-negative so if this wraps then
     new_start will be negative and will fail the new_start >= t.off test.

     new_end = new_start + len. The above test ensures that new_start is
     non-negative in any successful return. So if this wraps then new_end will
     be negative and will fail the new_start <= new_end test.

     old_end = t.off + t.len. This uses only the existing trusted values. It
     could only wrap if the underlying bigarray had a negative length!  *)
  let new_start = t.off + off in
  let new_end = new_start + len in
  let old_end = t.off + t.len in
  if new_start >= t.off && new_end <= old_end && new_start <= new_end then
    { t with off = new_start ; len }
  else
    err_sub t off len

let shift t amount =
  let off = t.off + amount in
  let len = t.len - amount in
  if amount < 0 || amount > t.len || not (check_bounds t (off+len)) then
    err_shift t amount
  else { t with off; len }

let rec skip_empty = function
  | t :: ts when t.len = 0 -> skip_empty ts
  | x -> x

let rec shiftv ts = function
  | 0 -> skip_empty ts
  | n ->
    match ts with
    | [] -> err_shiftv n
    | t :: ts when n >= t.len -> shiftv ts (n - t.len)
    | t :: ts -> shift t n :: ts

external unsafe_blit_bigstring_to_bigstring : buffer -> int -> buffer -> int -> int -> unit = "caml_blit_bigstring_to_bigstring" [@@noalloc]

external unsafe_blit_string_to_bigstring : string -> int -> buffer -> int -> int -> unit = "caml_blit_string_to_bigstring" [@@noalloc]

external unsafe_blit_bytes_to_bigstring : Bytes.t -> int -> buffer -> int -> int -> unit = "caml_blit_string_to_bigstring" [@@noalloc]

external unsafe_blit_bigstring_to_bytes : buffer -> int -> Bytes.t -> int -> int -> unit = "caml_blit_bigstring_to_string" [@@noalloc]

external unsafe_compare_bigstring : buffer -> int -> buffer -> int -> int -> int = "caml_compare_bigstring" [@@noalloc]

external unsafe_fill_bigstring : buffer -> int -> int -> int -> unit = "caml_fill_bigstring" [@@noalloc]

let copy_to_string caller src srcoff len =
  if len < 0 || srcoff < 0 || src.len - srcoff < len then
    err_copy_to_string caller src srcoff len
  else
    let b = Bytes.create len in
    unsafe_blit_bigstring_to_bytes src.buffer (src.off+srcoff) b 0 len;
    (* The following call is safe, since b is not visible elsewhere. *)
    Bytes.unsafe_to_string b

let copy = copy_to_string "copy"

let blit src srcoff dst dstoff len =
  if len < 0 || srcoff < 0 || src.len - srcoff < len then
    err_blit_src src dst srcoff len
  else if dstoff < 0 || dst.len - dstoff < len then
    err_blit_dst src dst dstoff len
  else
    unsafe_blit_bigstring_to_bigstring src.buffer (src.off+srcoff) dst.buffer
      (dst.off+dstoff) len

let sub_copy cstr off len : t =
  let cstr2 = create_unsafe len in
  blit cstr off cstr2 0 len;
  cstr2

let blit_from_string src srcoff dst dstoff len =
  if len < 0 || srcoff < 0 || dstoff < 0 || String.length src - srcoff < len then
    err_blit_from_string_src src dst srcoff len
  else if dst.len - dstoff < len then
    err_blit_from_string_dst src dst dstoff len
  else
    unsafe_blit_string_to_bigstring src srcoff dst.buffer (dst.off+dstoff) len

let blit_from_bytes src srcoff dst dstoff len =
  if len < 0 || srcoff < 0 || dstoff < 0 || Bytes.length src - srcoff < len then
    err_blit_from_bytes_src src dst srcoff len
  else if dst.len - dstoff < len then
    err_blit_from_bytes_dst src dst dstoff len
  else
    unsafe_blit_bytes_to_bigstring src srcoff dst.buffer (dst.off+dstoff) len

let blit_to_bytes src srcoff dst dstoff len =
  if len < 0 || srcoff < 0 || dstoff < 0 || src.len - srcoff < len then
    err_blit_to_bytes_src src dst srcoff len
  else if Bytes.length dst - dstoff < len then
    err_blit_to_bytes_dst src dst dstoff len
  else
    unsafe_blit_bigstring_to_bytes src.buffer (src.off+srcoff) dst dstoff len

let compare t1 t2 =
  let l1 = t1.len
  and l2 = t2.len in
  match compare l1 l2 with
  | 0 ->
    ( match unsafe_compare_bigstring t1.buffer t1.off t2.buffer t2.off l1 with
      | 0 -> 0
      | r -> if r < 0 then -1 else 1 )
  | r -> r

let equal t1 t2 = compare t1 t2 = 0

(* Note that this is only safe as long as all [t]s are coherent. *)
let memset t x = unsafe_fill_bigstring t.buffer t.off t.len x

let create len =
  let t = create_unsafe len in
  memset t 0;
  t

let set_uint8 t i c =
  if i >= t.len || i < 0 then err_invalid_bounds "set_uint8" t i 1
  else Bigarray.Array1.set t.buffer (t.off+i) (Char.unsafe_chr c)

let set_char t i c =
  if i >= t.len || i < 0 then err_invalid_bounds "set_char" t i 1
  else Bigarray.Array1.set t.buffer (t.off+i) c

let get_uint8 t i =
  if i >= t.len || i < 0 then err_invalid_bounds "get_uint8" t i 1
  else Char.code (Bigarray.Array1.get t.buffer (t.off+i))

let get_char t i =
  if i >= t.len || i < 0 then err_invalid_bounds "get_char" t i 1
  else Bigarray.Array1.get t.buffer (t.off+i)


external ba_set_int16 : buffer -> int -> uint16 -> unit = "%caml_bigstring_set16u"
external ba_set_int32 : buffer -> int -> uint32 -> unit = "%caml_bigstring_set32u"
external ba_set_int64 : buffer -> int -> uint64 -> unit = "%caml_bigstring_set64u"
external ba_get_int16 : buffer -> int -> uint16 = "%caml_bigstring_get16u"
external ba_get_int32 : buffer -> int -> uint32 = "%caml_bigstring_get32u"
external ba_get_int64 : buffer -> int -> uint64 = "%caml_bigstring_get64u"

external swap16 : int -> int = "%bswap16"
external swap32 : int32 -> int32 = "%bswap_int32"
external swap64 : int64 -> int64 = "%bswap_int64"

let set_uint16 swap p t i c =
  if i > t.len - 2 || i < 0 then err_invalid_bounds (p ^ ".set_uint16") t i 2
  else ba_set_int16 t.buffer (t.off+i) (if swap then swap16 c else c) [@@inline]

let set_uint32 swap p t i c =
  if i > t.len - 4 || i < 0 then err_invalid_bounds (p ^ ".set_uint32") t i 4
  else ba_set_int32 t.buffer (t.off+i) (if swap then swap32 c else c) [@@inline]

let set_uint64 swap p t i c =
  if i > t.len - 8 || i < 0 then err_invalid_bounds (p ^ ".set_uint64") t i 8
  else ba_set_int64 t.buffer (t.off+i) (if swap then swap64 c else c) [@@inline]

let get_uint16 swap p t i =
  if i > t.len - 2 || i < 0 then err_invalid_bounds (p ^ ".get_uint16") t i 2
  else
    let r = ba_get_int16 t.buffer (t.off+i) in
    if swap then swap16 r else r [@@inline]

let get_uint32 swap p t i =
  if i > t.len - 4 || i < 0 then err_invalid_bounds (p ^ ".get_uint32") t i 4
  else
    let r = ba_get_int32 t.buffer (t.off+i) in
    if swap then swap32 r else r [@@inline]

let get_uint64 swap p t i =
  if i > t.len - 8 || i < 0 then err_invalid_bounds (p ^ ".get_uint64") t i 8
  else
    let r = ba_get_int64 t.buffer (t.off+i) in
    if swap then swap64 r else r [@@inline]

module BE = struct
  let set_uint16 t i c = set_uint16 (not Sys.big_endian) "BE" t i c [@@inline]
  let set_uint32 t i c = set_uint32 (not Sys.big_endian) "BE" t i c [@@inline]
  let set_uint64 t i c = set_uint64 (not Sys.big_endian) "BE" t i c [@@inline]
  let get_uint16 t i = get_uint16 (not Sys.big_endian) "BE" t i [@@inline]
  let get_uint32 t i = get_uint32 (not Sys.big_endian) "BE" t i [@@inline]
  let get_uint64 t i = get_uint64 (not Sys.big_endian) "BE" t i [@@inline]
end

module LE = struct
  let set_uint16 t i c = set_uint16 Sys.big_endian "LE" t i c [@@inline]
  let set_uint32 t i c = set_uint32 Sys.big_endian "LE" t i c [@@inline]
  let set_uint64 t i c = set_uint64 Sys.big_endian "LE" t i c [@@inline]
  let get_uint16 t i = get_uint16 Sys.big_endian "LE" t i [@@inline]
  let get_uint32 t i = get_uint32 Sys.big_endian "LE" t i [@@inline]
  let get_uint64 t i = get_uint64 Sys.big_endian "LE" t i [@@inline]
end

module HE = struct
  let set_uint16 t i c = set_uint16 false "HE" t i c [@@inline]
  let set_uint32 t i c = set_uint32 false "HE" t i c [@@inline]
  let set_uint64 t i c = set_uint64 false "HE" t i c [@@inline]
  let get_uint16 t i = get_uint16 false "HE" t i [@@inline]
  let get_uint32 t i = get_uint32 false "HE" t i [@@inline]
  let get_uint64 t i = get_uint64 false "HE" t i [@@inline]
end

let length { len ; _ } = len

(** [sum_lengths ~caller acc l] is [acc] plus the sum of the lengths
    of the elements of [l].  Raises [Invalid_argument caller] if
    arithmetic overflows. *)
let rec sum_lengths_aux ~caller acc = function
  | [] -> acc
  | h :: t ->
     let sum = length h + acc in
     if sum < acc then invalid_arg caller
     else sum_lengths_aux ~caller sum t

let sum_lengths ~caller l = sum_lengths_aux ~caller 0 l

let lenv l = sum_lengths ~caller:"Cstruct.lenv" l

let copyv ts =
  let sz = sum_lengths ~caller:"Cstruct.copyv" ts in
  let dst = Bytes.create sz in
  let _ = List.fold_left
    (fun off src ->
      let x = length src in
      unsafe_blit_bigstring_to_bytes src.buffer src.off dst off x;
      off + x
    ) 0 ts in
  (* The following call is safe, since dst is not visible elsewhere. *)
  Bytes.unsafe_to_string dst

let fillv ~src ~dst =
  let rec aux dst n = function
    | [] -> n, []
    | hd::tl ->
        let avail = length dst in
        let first = length hd in
        if first <= avail then (
          blit hd 0 dst 0 first;
          aux (shift dst first) (n + first) tl
        ) else (
          blit hd 0 dst 0 avail;
          let rest_hd = shift hd avail in
          (n + avail, rest_hd :: tl)
        ) in
  aux dst 0 src

let to_string ?(off=0) ?len:sz t =
  let len = match sz with None -> length t - off | Some l -> l in
  copy_to_string "to_string" t off len

let to_hex_string ?(off=0) ?len:sz t : string =
  let[@inline] nibble_to_char (i:int) : char =
    if i < 10 then
      Char.chr (i + Char.code '0')
    else
      Char.chr (i - 10 + Char.code 'a')
  in

  let len = match sz with None -> length t - off | Some l -> l in
  if len < 0 || off < 0 || t.len - off < len then
    err_to_hex_string t off len
  else (
    let out = Bytes.create (2 * len) in
    for i=0 to len-1 do
      let c = Char.code @@ Bigarray.Array1.get t.buffer (i+t.off+off) in
      Bytes.set out (2*i) (nibble_to_char (c lsr 4));
      Bytes.set out (2*i+1) (nibble_to_char (c land 0xf));
    done;
    Bytes.unsafe_to_string out
  )


let to_bytes ?off ?len t =
  Bytes.unsafe_of_string (to_string ?off ?len t)

let [@inline always] of_data_abstract blitfun lenfun ?allocator ?(off=0) ?len buf =
  let buflen =
    match len with
    | None -> lenfun buf - off
    | Some len -> len in
  match allocator with
  | None ->
    let c = create_unsafe buflen in
    blitfun buf off c 0 buflen;
    c
  | Some fn ->
    let c = fn buflen in
    blitfun buf off c 0 buflen;
    { c with len = buflen }

let of_string ?allocator ?off ?len buf =
  of_data_abstract blit_from_string String.length ?allocator ?off ?len buf

let of_bytes ?allocator ?off ?len buf =
  of_data_abstract blit_from_bytes Bytes.length ?allocator ?off ?len buf

let of_hex ?(off=0) ?len str =
  let str =
    let l = match len with None -> String.length str - off | Some l -> l in
    String.sub str off l
  in
  let string_fold ~f ~z str =
    let st = ref z in
    ( String.iter (fun c -> st := f !st c) str  ; !st )
  in
  let hexdigit p = function
    | 'a' .. 'f' as x -> int_of_char x - 87
    | 'A' .. 'F' as x -> int_of_char x - 55
    | '0' .. '9' as x -> int_of_char x - 48
    | x ->
      Format.ksprintf invalid_arg "of_hex: invalid character at pos %d: %C" p x
  in
  let whitespace = function
    | ' ' | '\t' | '\r' | '\n' -> true
    | _ -> false
  in
  match
    string_fold
      ~f:(fun (cs, i, p, acc) ->
          let p' = succ p in
          function
          | char when whitespace char -> (cs, i, p', acc)
          | char ->
            match acc, hexdigit p char with
            | (None  , x) -> (cs, i, p', Some (x lsl 4))
            | (Some y, x) -> set_uint8 cs i (x lor y) ; (cs, succ i, p', None))
      ~z:(create_unsafe (String.length str lsr 1), 0, 0, None)
      str
  with
  | _ , _, _, Some _ ->
    Format.ksprintf invalid_arg "of_hex: odd numbers of characters"
  | cs, i, _, _ -> sub cs 0 i

let hexdump_pp fmt t =
  let before fmt =
    function
    | 0 -> ()
    | 8 -> Format.fprintf fmt "  ";
    | _ -> Format.fprintf fmt " "
  in
  let after fmt =
    function
    | 15 -> Format.fprintf fmt "@;"
    |  _ -> ()
  in
  Format.pp_open_vbox fmt 0 ;
  for i = 0 to length t - 1 do
    let column = i mod 16 in
    let c = Char.code (Bigarray.Array1.get t.buffer (t.off+i)) in
    Format.fprintf fmt "%a%.2x%a" before column c after column
  done ;
  Format.pp_close_box fmt ()

let hexdump = Format.printf "@\n%a@." hexdump_pp

let hexdump_to_buffer buf t =
  let f = Format.formatter_of_buffer buf in
  Format.fprintf f "@\n%a@." hexdump_pp t

let split ?(start=0) t off =
  try
    let header =sub t start off in
    let body = sub t (start+off) (length t - off - start) in
    header, body
  with Invalid_argument _ -> err_split t start off

type 'a iter = unit -> 'a option
let iter lenfn pfn t =
  let body = ref (Some t) in
  let i = ref 0 in
  fun () ->
    match !body with
      |Some buf when length buf = 0 ->
        body := None;
        None
      |Some buf -> begin
        match lenfn buf with
        |None ->
          body := None;
          None
        |Some plen ->
          incr i;
          let p,rest =
            try split buf plen with Invalid_argument _ -> err_iter buf !i plen
          in
          body := Some rest;
          Some (pfn p)
      end
      |None -> None

let rec fold f next acc = match next () with
  | None -> acc
  | Some v -> fold f next (f acc v)

let append cs1 cs2 =
  let l1 = length cs1 and l2 = length cs2 in
  let cs = create_unsafe (l1 + l2) in
  blit cs1 0 cs 0  l1 ;
  blit cs2 0 cs l1 l2 ;
  cs

let concat = function
  | []   -> create_unsafe 0
  | [cs] -> cs
  | css  ->
      let result = create_unsafe (sum_lengths ~caller:"Cstruct.concat" css) in
      let aux off cs =
        let n = length cs in
        blit cs 0 result off n ;
        off + n in
      ignore @@ List.fold_left aux 0 css ;
      result

let rev t =
  let n = length t in
  let out = create_unsafe n in
  for i_src = 0 to n - 1 do
    let byte = get_uint8 t i_src in
    let i_dst = n - 1 - i_src in
    set_uint8 out i_dst byte
  done;
  out

(* Convenience function. *)

external unsafe_blit_string_to_bigstring
  : string -> int -> buffer -> int -> int -> unit
  = "caml_blit_string_to_bigstring"
[@@noalloc]

let get { buffer; off; len; } zidx =
  if zidx < 0 || zidx >= len then invalid_arg "index out of bounds" ;
  Bigarray.Array1.get buffer (off + zidx)

let get_byte { buffer; off; len; } zidx =
  if zidx < 0 || zidx >= len then invalid_arg "index out of bounds" ;
  Char.code (Bigarray.Array1.get buffer (off + zidx))

let string ?(off= 0) ?len str =
  let str_len = String.length str in
  let len = match len with None -> str_len | Some len -> len in
  if off < 0 || len < 0 || off + len > str_len then invalid_arg "index out of bounds" ;
  let buffer = Bigarray.(Array1.create char c_layout str_len) in
  unsafe_blit_string_to_bigstring str 0 buffer 0 str_len ;
  of_bigarray ~off ~len buffer

let buffer ?(off= 0) ?len buffer =
  let buffer_len = Bigarray.Array1.dim buffer in
  let len = match len with None -> buffer_len - off | Some len -> len in
  if off < 0 || len < 0 || off + len > buffer_len then invalid_arg "index out of bounds" ;
  of_bigarray ~off ~len buffer

let start_pos { off; _ } = off
let stop_pos { off; len; _ } = off + len

let head ?(rev= false) ({ len; _ } as cs) =
  if len = 0 then None
  else Some (get_char cs (if rev then len - 1 else 0))

let tail ?(rev= false) ({ buffer; off; len; } as cs) =
  if len = 0 then cs
  else if rev then of_bigarray ~off ~len:(len - 2) buffer
  else of_bigarray ~off:(off + 1) ~len:(len - 1) buffer

let is_empty { len; _ } = len = 0

let is_prefix ~affix:({ len= alen; _ } as affix)
    ({ len; _ } as cs) =
  if alen > len then false
  else
    let max_zidx = alen - 1 in
    let rec loop i =
      if i > max_zidx then true
      else if get_char affix i <> get_char cs i
      then false else loop (succ i) in
    loop 0

let is_infix ~affix:({ len= alen; _ } as affix)
    ({ len; _ } as cs) =
  if alen > len then false
  else
    let max_zidx_a = alen - 1 in
    let max_zidx_s = len - alen in
    let rec loop i k =
      if i > max_zidx_s then false
      else if k > max_zidx_a then true
      else if k > 0 then
        if get_char affix k = get_char cs (i + k)
        then loop i (succ k)
        else loop (succ i) 0
      else if get_char affix 0 = get_char cs i
      then loop i 1
      else loop (succ i) 0 in
    loop 0 0

let is_suffix ~affix:({ len= alen; _ } as affix)
    ({ len; _ } as cs) =
  if alen > len then false
  else
    let max_zidx = alen - 1 in
    let max_zidx_a = alen - 1 in
    let max_zidx_s = len - 1 in
    let rec loop i =
      if i > max_zidx then true
      else if get_char affix (max_zidx_a - i) <> get_char cs (max_zidx_s - i)
      then false else loop (succ i) in
    loop 0

let for_all sat cs =
  let rec go acc i =
    if i < length cs
    then go (sat (get_char cs i) && acc) (succ i)
    else acc in
  go true 0

let exists sat cs =
  let rec go acc i =
    if i < length cs
    then go (sat (get_char cs i) || acc) (succ i)
    else acc in
  go false 0

let start { buffer; off; _ } =
  of_bigarray buffer ~off ~len:0

let stop { buffer; off; len; } =
  of_bigarray buffer ~off:(off + len) ~len:0

let is_white = function ' ' | '\t' .. '\r' -> true | _ -> false

let trim ?(drop = is_white) ({ buffer; off; len; } as cs) =
  if len = 0 then cs
  else
    let max_zpos = len in
    let max_zidx = len - 1 in
    let rec left_pos i =
      if i > max_zidx then max_zpos
      else if drop (get_char cs i) then left_pos (succ i) else i in
    let rec right_pos i =
      if i < 0 then 0
      else if drop (get_char cs i) then right_pos (pred i) else succ i in
    let left = left_pos 0 in
    if left = max_zpos
    then of_bigarray buffer ~off:((off * 2 + len) / 2) ~len:0
    else
      let right = right_pos max_zidx in
      if left = 0 && right = max_zpos then cs
      else of_bigarray buffer ~off:(off + left) ~len:(right - left)

let fspan ~min ~max ~sat ({ buffer= v; off; len; } as cs) =
  if min < 0 then invalid_arg "span: negative min" ;
  if max < 0 then invalid_arg "span: negative max" ;
  if min > max || max = 0 then (buffer ~off:off ~len:0 v, cs)
  else
    let max_zidx = len - 1 in
    let max_zidx =
      let k = max - 1 in
      if k > max_zidx || k < 0 then max_zidx else k in
    let need_zidx = min in
    let rec loop i =
      if i <= max_zidx && sat (get_char cs i) then loop (i + 1)
      else if i < need_zidx || i = 0 then buffer ~off:off ~len:0 v, cs
      else if i = len then (cs, buffer ~off:(off + len) ~len:0 v)
      else buffer ~off:off ~len:i v, buffer ~off:(off + i) ~len:(len - i) v in
    loop 0

let rspan ~min ~max ~sat ({ buffer= v; off; len; } as cs) =
  if min < 0 then invalid_arg "span: negative min" ;
  if max < 0 then invalid_arg "span: negative max" ;
  if min > max || max = 0 then (cs, buffer ~off:(off + len) ~len:0 v)
  else
    let max_zidx = len - 1 in
    let min_zidx =
      let k = len - max in if k < 0 then 0 else k in
    let need_zidx = len - min - 1 in
    let rec loop i =
      if i >= min_zidx && sat (get_char cs i) then loop (i - 1)
      else if i > need_zidx || i = max_zidx then (cs, buffer ~off:(off + len) ~len:0 v)
      else if i < 0 then (buffer ~off:off ~len:0 v, cs)
      else (buffer ~off:off ~len:(i + 1) v, buffer ~off:(off + i + 1) ~len:(len - (i + 1)) v) in
    loop max_zidx

let span ?(rev= false) ?(min= 0) ?(max= max_int) ?(sat= fun _ -> true) cs =
  match rev with
  | true  -> rspan ~min ~max ~sat cs
  | false -> fspan ~min ~max ~sat cs

let take ?(rev= false) ?min ?max ?sat cs =
  (if rev then snd else fst) @@ span ~rev ?min ?max ?sat cs

let drop ?(rev= false) ?min ?max ?sat cs =
  (if rev then fst else snd) @@ span ~rev ?min ?max ?sat cs

let fcut ~sep:({ len= sep_len; _ } as sep)
    ({ buffer= v; off; len; } as cs) =
  if sep_len = 0 then invalid_arg "cut: empty separator" ;
  let max_sep_zidx = sep_len - 1 in
  let max_s_zidx = len - sep_len in
  let rec check_sep i k =
    if k > max_sep_zidx
    then Some (buffer ~off:off ~len:i v,
               buffer ~off:(off + i + sep_len) ~len:(len - i - sep_len) v)
    else if get_char cs (i + k) = get_char sep k
    then check_sep i (k + 1)
    else scan (i + 1)
  and scan i =
    if i > max_s_zidx then None
    else if get_char cs i = get_char sep 0
    then check_sep i 1
    else scan (i + 1) in
  scan 0

let rcut ~sep:({ len= sep_len; _ } as sep) ({ buffer= v; off; len; } as cs) =
  if sep_len = 0 then invalid_arg "cut: empty separator" ;
  let max_sep_zidx = sep_len - 1 in
  let max_s_zidx = len - 1 in
  let rec check_sep i k =
    if k > max_sep_zidx then Some (buffer ~off:off ~len:i v,
                                   buffer ~off:(off + i + sep_len) ~len:(len - i - sep_len) v)
    else if get_char cs (i + k) = get_char sep k
    then check_sep i (k + 1)
    else rscan (i - 1)
  and rscan i =
    if i < 0 then None
    else if get_char cs i = get_char sep 0
    then check_sep i 1
    else rscan (i - 1) in
  rscan (max_s_zidx - max_sep_zidx)

let cut ?(rev= false) ~sep cs = match rev with
  | true  -> rcut ~sep cs
  | false -> fcut ~sep cs

let add_sub ~no_empty buf ~off ~len acc =
  if len = 0
  then ( if no_empty then acc else buffer ~off ~len buf :: acc )
  else buffer ~off ~len buf :: acc

let fcuts ~no_empty ~sep:({ len= sep_len; _ } as sep)
      ({ buffer; off; len; } as cs) =
  if sep_len = 0 then invalid_arg "cuts: empty separator" ;
  let max_sep_zidx = sep_len - 1 in
  let max_s_zidx = len - sep_len in
  let rec check_sep zanchor i k acc =
    if k > max_sep_zidx
    then
      let new_start = i + sep_len in
      scan new_start new_start (add_sub ~no_empty buffer ~off:(off + zanchor) ~len:(i - zanchor) acc)
    else
      if get_char cs (i + k) = get_char sep k
      then check_sep zanchor i (k + 1) acc
      else scan zanchor (i + 1) acc
  and scan zanchor i acc =
    if i > max_s_zidx
    then
      if zanchor = 0 then (if no_empty && len = 0 then [] else [ cs ])
      else List.rev (add_sub ~no_empty buffer ~off:(off + zanchor) ~len:(len - zanchor) acc)
    else
      if get_char cs i = get_char sep 0
      then check_sep zanchor i 1 acc
      else scan zanchor (i + 1) acc in
  scan 0 0 []

let rcuts ~no_empty ~sep:({ len= sep_len; _ } as sep)
      ({ buffer; len; _ } as cs) =
  if sep_len = 0 then invalid_arg "cuts: empty separator" ;
  let s_len = len in
  let max_sep_zidx = sep_len - 1 in
  let max_s_zidx = len - 1 in
  let rec check_sep zanchor i k acc =
    if k > max_sep_zidx
    then let off = i + sep_len in
         rscan i (i - sep_len) (add_sub ~no_empty buffer ~off ~len:(zanchor - off) acc)
    else
      if get_char cs (i + k) = get_char cs k
      then check_sep zanchor i (k + 1) acc
      else rscan zanchor (i - 1) acc
  and rscan zanchor i acc =
    if i < 0 then
      if zanchor = s_len then ( if no_empty && s_len = 0 then [] else [ cs ])
      else add_sub ~no_empty buffer ~off:0 ~len:zanchor acc
    else
      if get_char cs i = get_char sep 0
      then check_sep zanchor i 1 acc
      else rscan zanchor (i - 1) acc in
  rscan s_len (max_s_zidx - max_sep_zidx) []

let cuts ?(rev= false) ?(empty= true) ~sep cs = match rev with
  | true  -> rcuts ~no_empty:(not empty) ~sep cs
  | false -> fcuts ~no_empty:(not empty) ~sep cs

let fields ?(empty= false) ?(is_sep= is_white) ({ buffer; off; len; } as cs) =
  let no_empty = not empty in
  let max_pos = len in
  let rec loop i end_pos acc =
    if i < 0 then begin
        if end_pos = len
        then ( if no_empty && len = 0 then [] else [ cs ])
        else add_sub ~no_empty buffer ~off:off ~len:(end_pos - (i + 1)) acc
      end else begin
        if not (is_sep (get_char cs i))
        then loop (i - 1) end_pos acc
        else loop (i - 1) i (add_sub ~no_empty buffer ~off:(off + i + 1) ~len:(end_pos - (i + 1)) acc)
      end in
  loop (max_pos - 1) max_pos []

let ffind sat ({ buffer= v; len; _ } as cs) =
  let max_idx = len - 1 in
  let rec loop i =
    if i > max_idx then None
    else if sat (get_char cs i)
    then Some (buffer ~off:i ~len:1 v)
    else loop (i + 1) in
  loop 0

let rfind sat ({ buffer= v; len; _ } as cs) =
  let rec loop i =
    if i < 0 then None
    else if sat (get_char cs i)
    then Some (buffer ~off:i ~len:1 v)
    else loop (i - 1) in
  loop (len - 1)

let find ?(rev= false) sat cs = match rev with
  | true  -> rfind sat cs
  | false -> ffind sat cs

let ffind_sub ~sub:({ len= sub_len; _ } as sub) ({ buffer= v; off; len; } as cs) =
  if sub_len > len then None
  else
    let max_zidx_sub = sub_len - 1 in
    let max_zidx_s = len - sub_len in
    let rec loop i k =
      if i > max_zidx_s then None
      else if k > max_zidx_sub then Some (buffer v ~off:(off + i) ~len:sub_len)
      else if k > 0
      then ( if get_char sub k = get_char cs (i + k)
             then loop i (k + 1)
             else loop (i + 1) 0 )
      else if get_char sub 0 = get_char cs i
      then loop i 1
      else loop (i + 1) 0 in
    loop 0 0

let rfind_sub ~sub:({ len= sub_len; _ } as sub) ({ buffer= v; len; _ } as cs) =
  if sub_len > len then None
  else
    let max_zidx_sub = sub_len - 1 in
    let rec loop i k =
      if i < 0 then None
      else if k > max_zidx_sub then Some (buffer v ~off:i ~len:sub_len)
      else if k > 0
      then ( if get_char sub k = get_char cs (i + k)
             then loop i (k + 1)
             else loop (i - 1) 0 )
      else if get_char sub 0 = get_char cs i
      then loop i 1
      else loop (i - 1) 0 in
    loop (len - sub_len) 0

let find_sub ?(rev= false) ~sub cs = match rev with
  | true  -> rfind_sub ~sub cs
  | false -> ffind_sub ~sub cs

let filter sat ({ len; _ } as cs) =
  if len = 0 then empty
  else
    let b = create len in
    let max_zidx = len - 1 in
    let rec loop b k i =
      if i > max_zidx
      then (if k = len then b else sub b 0 k)
      else
        let chr = get_char cs i in
        if sat chr then ( set_char b k chr ; loop b (k + 1) (i + 1))
        else loop b k (i + 1) in
    loop b 0 0

let filter_map f ({ len; _ } as cs) =
  if len = 0 then empty
  else
    let b = create len in
    let max_zidx = len - 1 in
    let rec loop b k i =
      if i > max_zidx
      then (if k = len then b else sub b 0 k)
      else match f (get_char cs i) with
           | Some chr ->
              set_char b i chr ;
              loop b (k + 1) (i + 1)
           | None ->
              loop b k (i + 1) in
    loop b 0 0

let map f ({ len; _ } as cs) =
  if len = 0 then empty
  else
    let b = create len in
    for i = 0 to len - 1 do
      set_char b i (f (get_char cs i))
    done ; b

let mapi f ({ len; _ } as cs) =
  if len = 0 then empty
  else
    let b = create len in
    for i = 0 to len - 1 do
      set_char b i (f i (get_char cs i))
    done ; b
