open Base
open Ppxlib
open Ast_builder.Default

(* returns the index of the conversion spec (unless the end of string is reached) *)
let rec skip_over_format_flags fmt i =
  if i >= String.length fmt
  then `Eoi
  else match fmt.[i] with
    | '*' | '#' | '-' | ' ' | '+' | '_' | '0'..'9' | '.' ->
      skip_over_format_flags fmt (i + 1)
    | _ -> `Ok i

(* doesn't check to make sure the format string is well-formed *)
(* Formats with subformats are skipped for the following reasons:

   One is that they are hard to understand and not often used.

   Another is that subformats like "%(%{Module}%)" won't work, since it
   is impossible to produce a format of type [(Module.t -> 'a,...) format].
*)
let has_subformats (fmt:string) =
  let lim = String.length fmt - 1 in
  let rec loop i =
    if i > lim
    then false
    else
    if Char.equal fmt.[i] '%' then
      match skip_over_format_flags fmt (i + 1) with
      | `Eoi -> false
      | `Ok i ->
        match fmt.[i] with
        | '(' | ')' | '}' -> true
        | _ -> loop (i + 1)
    else loop (i + 1)
  in
  loop 0

(* returns a list of strings where even indexed elements are parts of the format string
   that the preprocessor won't touch and odd indexed elements are the contents of %{...}
   specifications. *)
let explode ~loc (s:string) =
  let len = String.length s in
  (* for cases where we can't parse the string with custom format specifiers, consider
     the string as a regular format string *)
  let as_normal_format_string = [s] in
  if has_subformats s
  then as_normal_format_string
  else
    let sub from to_ = String.sub s ~pos:from ~len:(to_ - from) in
    let rec loop acc from to_ =
      assert (List.length acc % 2 = 0);
      if to_ >= len
      then List.rev (
        if from >= len
        then acc
        else sub from len :: acc
      )
      else
      if Char.(<>) s.[to_] '%'
      then loop acc from (to_ + 1)
      else
        match skip_over_format_flags s (to_ + 1) with
        | `Eoi -> as_normal_format_string
        | `Ok i ->
          match s.[i] with
          | '[' ->
            (* Scan char sets are not allowed by printf-like functions. So we might as
               well disallow them at compile-time so that we can reuse them as magic
               format strings in this implementation. *)
            Location.raise_errorf ~loc
              "ppx_custom_printf: scan char sets are not allowed in \
               custom format strings"
          | '{' ->
            if to_ + 1 <> i then
              Location.raise_errorf ~loc
                "ppx_custom_printf: unexpected format flags before \
                 %%{} specification in %S" s;
            begin match String.index_from s (to_ + 2) '}' with
            | None -> as_normal_format_string
            | Some i ->
              let l =
                sub (to_ + 2) i
                :: sub from to_
                :: acc
              in
              loop l (i + 1) (i + 1)
            end
          | _ ->
            loop acc from (i + 1) (* skip the conversion spec *)
    in
    loop [] 0 0

let processed_format_string ~exploded_format_string =
  let l =
    let rec loop i l =
      match l with
      | s1 :: _s2 :: l -> s1 :: Printf.sprintf "%%%d[.]" i :: loop (i + 1) l
      | [s1] -> [s1]
      | [] -> []
    in
    loop 0 exploded_format_string
  in
  String.concat l ~sep:""

let rec evens = function
  | [] | [_] as l -> l
  | x :: _ :: l -> x :: evens l

let odds = function
  | [] -> []
  | _ :: l -> evens l

(* Returns a pair of:

   - a format string, which is [s] where all custom format specifications have been
     replaced by ["%" ^ string_of_int index ^ "[.]"] where [index] is the number of
     the custom format specification, starting from 0. This string can be passed directly
     to [CamlinternalFormat.fmt_ebb_of_string]
   - an array of custom format specifications, in the order they appear in the original
     string
*)
let extract_custom_format_specifications ~loc s =
  let exploded_format_string = explode ~loc s in
  let processed = processed_format_string ~exploded_format_string in
  let custom_specs = Array.of_list (odds exploded_format_string) in
  (processed, custom_specs)
;;

let gen_symbol = gen_symbol ~prefix:"_custom_printf"

let is_space = function
  | ' ' | '\t' | '\n' | '\r' -> true
  | _ -> false
;;

let strip s =
  let a = ref 0 in
  let b = ref (String.length s - 1) in
  while !a <= !b && is_space s.[!a] do Int.incr a done;
  while !a <= !b && is_space s.[!b] do Int.decr b done;
  if !a > !b then "" else String.sub s ~pos:!a ~len:(!b - !a + 1)
;;

let string_to_expr ~loc s =
  let sexp_converter_opt =
    match String.lsplit2 s ~on:':' with
    | None -> None
    | Some ("sexp", colon_suffix) ->
      Some ([%expr  Ppx_sexp_conv_lib.Sexp.to_string_hum ], colon_suffix)
    | Some (colon_prefix, colon_suffix) ->
      match String.chop_prefix colon_prefix ~prefix:"sexp#" with
      | None -> None
      | Some hash_suffix ->
        Some (pexp_ident ~loc
                (Located.mk ~loc
                   (Longident.parse ("Ppx_sexp_conv_lib.Sexp.to_string_" ^ hash_suffix))),
              colon_suffix)
  in
  match sexp_converter_opt with
  | Some (sexp_converter, unparsed_type) ->
    let lexbuf = Lexing.from_string unparsed_type in
    (* ~loc is the position of the string, not the position of the %{bla} group we're
       looking at. The format strings don't contain location information, so we can't
       actually find the proper positions. *)
    lexbuf.lex_abs_pos <- loc.loc_start.pos_cnum;
    lexbuf.lex_curr_p <- loc.loc_start;
    let ty = Parse.core_type lexbuf in
    let e = Ppx_sexp_conv_expander.Sexp_of.core_type ty in
    let arg = gen_symbol () in
    pexp_fun ~loc Nolabel None (pvar ~loc arg)
      (eapply ~loc sexp_converter [eapply ~loc e [evar ~loc arg]])
  | None ->
    let fail loc =
      Location.raise_errorf ~loc
        "ppx_custom_printf: string %S should be of the form <Module>, \
         <Module>.<identifier>, <Module>#identifier, sexp:<type>, or sexp#mach:<type>"
        s
    in
    let s, has_hash_suffix, to_string =
      match String.lsplit2 s ~on:'#' with
      | None -> s, false, "to_string"
      | Some (s, hash_suffix) -> s, true, "to_string_" ^ hash_suffix
    in
    let to_string_id : Longident.t =
      let s = strip s in
      match s with
      | "" -> Lident to_string
      | _ ->
        match Longident.parse s with
        | Lident n | Ldot (_, n) as id ->
          if String.(<>) n "" && Char.equal (Char.uppercase n.[0]) n.[0] then
            Longident.Ldot (id, to_string)
          else if not has_hash_suffix then
            id
          else
            fail loc
        | _ -> fail loc
    in
    let func = pexp_ident ~loc (Located.mk ~loc to_string_id) in
    (* Eta-expand as the to_string function might take optional arguments *)
    let arg = gen_symbol () in
    pexp_fun ~loc Nolabel None (pvar ~loc arg) (eapply ~loc func [evar ~loc arg])

class lifter ~loc ~custom_specs = object(self)
  inherit [expression] Format_lifter.lift as super
  inherit Ppxlib_metaquot_lifters.expression_lifters loc

  method! fmt
    : type f0 f1 f2 f3 f4 f5. (f0 -> expression)
      -> (f1 -> expression)
      -> (f2 -> expression)
      -> (f3 -> expression)
      -> (f4 -> expression)
      -> (f5 -> expression)
      -> (f0, f1, f2, f3, f4, f5) CamlinternalFormatBasics.fmt
      -> expression
    = fun f0 f1 f2 f3 f4 f5 fmt ->
      let open CamlinternalFormatBasics in
      match fmt with
      (* Recognize the special form "%index[...whatever...]" *)
      | Scan_char_set (Some idx, _, fmt)
        (* [custom_specs] is empty if [explode] couldn't parse the string. In this case we
           can have some scar char sets left. *)
        when idx >= 0 && idx < Array.length custom_specs ->
        let rest =
          self#fmt (fun _ -> assert false) f1 f2 f3 f4 f5
            fmt
        in
        let func = string_to_expr ~loc custom_specs.(idx) in
        [%expr
          Custom(Custom_succ
                   Custom_zero,
                 (fun () -> [%e func]),
                 [%e rest])
        ]
      | _ ->
        super#fmt f0 f1 f2 f3 f4 f5 fmt
end

let expand_format_string ~loc fmt_string =
  let processed_fmt_string, custom_specs =
    extract_custom_format_specifications ~loc fmt_string
  in
  let (CamlinternalFormat.Fmt_EBB fmt) =
    try
      CamlinternalFormat.fmt_ebb_of_string processed_fmt_string
    with e ->
      Location.raise_errorf ~loc "%s"
        (match e with
         (* [fmt_ebb_of_string] normally raises [Failure] on invalid input *)
         | Failure msg -> msg
         | e -> Exn.to_string e)
  in
  let lifter = new lifter ~loc ~custom_specs in
  let format6 = CamlinternalFormatBasics.Format (fmt, fmt_string) in
  let phantom _ = assert false in
  let e =
    lifter#format6
      phantom phantom phantom phantom phantom phantom format6
  in
  [%expr ([%e e] : (_, _, _, _, _, _) CamlinternalFormatBasics.format6)]

let expand e =
  match e.pexp_desc with
  | Pexp_apply ({ pexp_attributes = ident_attrs; _ },
                [ (Nolabel, { pexp_desc = Pexp_constant (Pconst_string (str, _, _))
                            ; pexp_loc = loc; pexp_loc_stack = _
                            ; pexp_attributes = str_attrs }) ]) ->
    assert_no_attributes ident_attrs;
    assert_no_attributes str_attrs;
    let e' = expand_format_string ~loc str in
    Some { e' with pexp_attributes = Merlin_helpers.hide_attribute :: e.pexp_attributes }
  | _ -> None
;;

let () =
  Driver.register_transformation "custom_printf"
    ~rules:[ Context_free.Rule.special_function "!" expand ]
;;
