(*
   RE - A regular expression library

   Copyright (C) 2001 Jerome Vouillon
   email: Jerome.Vouillon@pps.jussieu.fr

   This library is free software; you can redistribute it and/or
   modify it under the terms of the GNU Lesser General Public
   License as published by the Free Software Foundation, with
   linking exception; either version 2.1 of the License, or (at
   your option) any later version.

   This library is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
   Lesser General Public License for more details.

   You should have received a copy of the GNU Lesser General Public
   License along with this library; if not, write to the Free Software
   Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*)

module Re = Core

exception Parse_error = Parse_buffer.Parse_error

type enclosed =
  | Char of char
  | Range of char * char

type piece =
  | Exactly of char
  | Any_of of enclosed list
  | Any_but of enclosed list
  | One
  | Many
  | ManyMany

type t = piece list

let of_string ~double_asterisk s : t =
  let buf = Parse_buffer.create s in
  let eos () = Parse_buffer.eos buf in
  let read c = Parse_buffer.accept buf c in
  let char () =
    ignore (read '\\' : bool);
    if eos () then raise Parse_error;
    Parse_buffer.get buf
  in
  let enclosed () : enclosed list =
    let rec loop s =
      (* This returns the list in reverse order, but order isn't important
         anyway *)
      if s <> [] && read ']'
      then s
      else (
        let c = char () in
        if not (read '-')
        then loop (Char c :: s)
        else if read ']'
        then Char c :: Char '-' :: s
        else (
          let c' = char () in
          loop (Range (c, c') :: s)))
    in
    loop []
  in
  let piece acc =
    if double_asterisk && Parse_buffer.accept_s buf "/**"
    then ManyMany :: (if eos () then Exactly '/' :: acc else acc)
    else if read '*'
    then (if double_asterisk && read '*' then ManyMany else Many) :: acc
    else if read '?'
    then One :: acc
    else if not (read '[')
    then Exactly (char ()) :: acc
    else if read '^' || read '!'
    then Any_but (enclosed ()) :: acc
    else Any_of (enclosed ()) :: acc
  in
  let rec loop pieces = if eos () then List.rev pieces else loop (piece pieces) in
  loop []
;;

let mul l l' = List.flatten (List.map (fun s -> List.map (fun s' -> s ^ s') l') l)

let explode str =
  let l = String.length str in
  let rec expl inner s i acc beg =
    if i >= l
    then (
      if inner then raise Parse_error;
      mul beg [ String.sub str s (i - s) ], i)
    else (
      match str.[i] with
      | '\\' -> expl inner s (i + 2) acc beg
      | '{' ->
        let t, i' = expl true (i + 1) (i + 1) [] [ "" ] in
        expl inner i' i' acc (mul beg (mul [ String.sub str s (i - s) ] t))
      | ',' when inner ->
        expl inner (i + 1) (i + 1) (mul beg [ String.sub str s (i - s) ] @ acc) [ "" ]
      | '}' when inner -> mul beg [ String.sub str s (i - s) ] @ acc, i + 1
      | _ -> expl inner s (i + 1) acc beg)
  in
  List.rev (fst (expl false 0 0 [] [ "" ]))
;;

module State = struct
  type t =
    { re_pieces : Re.t list (* last piece at head of list. *)
    ; remaining : piece list (* last piece at tail of list. *)
    ; am_at_start_of_pattern : bool (* true at start of pattern *)
    ; am_at_start_of_component : bool
        (* true at start of pattern or immediately
           after '/' *)
    ; pathname : bool
    ; match_backslashes : bool
    ; period : bool
    }

  let create ~period ~pathname ~match_backslashes remaining =
    { re_pieces = []
    ; am_at_start_of_pattern = true
    ; am_at_start_of_component = true
    ; pathname
    ; match_backslashes
    ; period
    ; remaining
    }
  ;;

  let explicit_period t =
    t.period && (t.am_at_start_of_pattern || (t.am_at_start_of_component && t.pathname))
  ;;

  let explicit_slash t = t.pathname
  let slashes t = if t.match_backslashes then [ '/'; '\\' ] else [ '/' ]

  let append ?(am_at_start_of_component = false) t piece =
    { t with
      re_pieces = piece :: t.re_pieces
    ; am_at_start_of_pattern = false
    ; am_at_start_of_component
    }
  ;;

  let to_re t = Re.seq (List.rev t.re_pieces)

  let next t =
    match t.remaining with
    | [] -> None
    | piece :: remaining -> Some (piece, { t with remaining })
  ;;
end

let one ~explicit_slash ~slashes ~explicit_period =
  Re.compl
    (List.concat
       [ (if explicit_slash then List.map Re.char slashes else [])
       ; (if explicit_period then [ Re.char '.' ] else [])
       ])
;;

let enclosed enclosed =
  match enclosed with
  | Char c -> Re.char c
  | Range (low, high) -> Re.rg low high
;;

let enclosed_set ~explicit_slash ~slashes ~explicit_period kind set =
  let set = List.map enclosed set in
  let enclosure =
    match kind with
    | `Any_of -> Re.alt set
    | `Any_but -> Re.compl set
  in
  Re.inter [ enclosure; one ~explicit_slash ~slashes ~explicit_period ]
;;

let exactly state c =
  let slashes = State.slashes state in
  let am_at_start_of_component = List.mem c slashes in
  let chars = if am_at_start_of_component then slashes else [ c ] in
  State.append state (Re.alt (List.map Re.char chars)) ~am_at_start_of_component
;;

let many_many state =
  let explicit_period = state.State.period && state.State.pathname in
  let first_explicit_period = State.explicit_period state in
  let slashes = State.slashes state in
  let match_component ~explicit_period =
    Re.seq
      [ one ~explicit_slash:true ~slashes ~explicit_period
      ; Re.rep (one ~explicit_slash:true ~slashes ~explicit_period:false)
      ]
  in
  (* We must match components individually when [period] flag is set,
     making sure to not match ["foo/.bar"]. *)
  State.append
    state
    (Re.seq
       [ Re.opt (match_component ~explicit_period:first_explicit_period)
       ; Re.rep
           (Re.seq
              [ Re.alt (List.map Re.char slashes)
              ; Re.opt (match_component ~explicit_period)
              ])
       ])
;;

let many (state : State.t) =
  let explicit_slash = State.explicit_slash state in
  let explicit_period = State.explicit_period state in
  let slashes = State.slashes state in
  (* Whether we must explicitly match period depends on the surrounding
     characters, but slashes are easy to explicit match. This conditional
     splits out some simple cases. *)
  if not explicit_period
  then State.append state (Re.rep (one ~explicit_slash ~slashes ~explicit_period))
  else if not explicit_slash
  then
    (* In this state, we explicitly match periods only at the very beginning *)
    State.append
      state
      (Re.opt
         (Re.seq
            [ one ~explicit_slash:false ~slashes ~explicit_period
            ; Re.rep (one ~explicit_slash:false ~slashes ~explicit_period:false)
            ]))
  else (
    let not_empty =
      Re.seq
        [ one ~explicit_slash:true ~slashes ~explicit_period:true
        ; Re.rep (one ~explicit_slash:true ~slashes ~explicit_period:false)
        ]
    in
    (* [maybe_empty] is the default translation of Many, except in some special
       cases. *)
    let maybe_empty = Re.opt not_empty in
    let enclosed_set state kind set =
      State.append
        state
        (Re.alt
           [ enclosed_set kind set ~explicit_slash:true ~slashes ~explicit_period:true
           ; Re.seq
               [ not_empty
               ; (* Since [not_empty] matched, subsequent dots are not leading. *)
                 enclosed_set
                   kind
                   set
                   ~explicit_slash:true
                   ~slashes
                   ~explicit_period:false
               ]
           ])
    in
    let rec lookahead state =
      match State.next state with
      | None -> State.append state maybe_empty
      (* glob ** === glob * . *)
      | Some (Many, state) -> lookahead state
      | Some (Exactly c, state) ->
        let state = State.append state (if c = '.' then not_empty else maybe_empty) in
        exactly state c
      (* glob *? === glob ?* *)
      | Some (One, state) -> State.append state not_empty
      | Some (Any_of enclosed, state) -> enclosed_set state `Any_of enclosed
      | Some (Any_but enclosed, state) -> enclosed_set state `Any_but enclosed
      (* * then ** === ** *)
      | Some (ManyMany, state) -> many_many state
    in
    lookahead state)
;;

let piece state piece =
  let explicit_slash = State.explicit_slash state in
  let explicit_period = State.explicit_period state in
  let slashes = State.slashes state in
  match piece with
  | One -> State.append state (one ~explicit_slash ~slashes ~explicit_period)
  | Many -> many state
  | Any_of enclosed ->
    State.append
      state
      (enclosed_set `Any_of ~explicit_slash ~slashes ~explicit_period enclosed)
  | Any_but enclosed ->
    State.append
      state
      (enclosed_set `Any_but ~explicit_slash ~slashes ~explicit_period enclosed)
  | Exactly c -> exactly state c
  | ManyMany -> many_many state
;;

let glob ~pathname ~match_backslashes ~period glob =
  let rec loop state =
    match State.next state with
    | None -> State.to_re state
    | Some (p, state) -> loop (piece state p)
  in
  loop (State.create ~pathname ~match_backslashes ~period glob)
;;

let glob
  ?(anchored = false)
  ?(pathname = true)
  ?(match_backslashes = false)
  ?(period = true)
  ?(expand_braces = false)
  ?(double_asterisk = true)
  s
  =
  let to_re s =
    let re = glob ~pathname ~match_backslashes ~period (of_string ~double_asterisk s) in
    if anchored then Re.whole_string re else re
  in
  if expand_braces then Re.alt (List.map to_re (explode s)) else to_re s
;;

let glob_result
  ?anchored
  ?pathname
  ?match_backslashes
  ?period
  ?expand_braces
  ?double_asterisk
  s
  =
  match
    glob ?anchored ?pathname ?match_backslashes ?period ?expand_braces ?double_asterisk s
  with
  | re -> Ok re
  | exception Parse_error -> Error `Parse_error
;;

let glob' ?anchored period s = glob ?anchored ~period s
let globx ?anchored s = glob ?anchored ~expand_braces:true s
let globx' ?anchored period s = glob ?anchored ~expand_braces:true ~period s
