(*
 * optParse - Functions for parsing command line arguments.
 * Copyright (C) 2004 Bardur Arantsson
 *
 * Heavily influenced by the optparse.py module from the Python
 * standard library, but with lots of adaptation to the 'Ocaml Way'
 *
 *
 * 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; either
 * version 2.1 of the License, or (at your option) any later version,
 * with the special exception on linking described in file LICENSE.
 *
 * 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
 *)
open Printf
open ExtString
open ExtList


let terminal_width =
  try 
    int_of_string (Sys.getenv "COLUMNS")    (* Might as well use it if it's there... *)
  with
    Failure _ -> 80
  | Not_found -> 80

module GetOpt =
  struct

    type action = string -> string list -> unit
    type long_opt = string * int * action
    type short_opt = char * int * action

    exception Error of (string * string)

    let split1 haystack needle =
      try 
        let (h, x) = String.split haystack needle in h, [x] 
      with
        Invalid_string -> haystack, []

    let find_opt format_name options s =
      let rec loop l =
        match l with
          (x, y, z) :: t -> if x = s then x, y, z else loop t
        | [] -> raise (Error (format_name s, "no such option"))
      in
      loop options

    let find_short_opt options = find_opt (fun c -> sprintf "-%c" c) options

    let find_long_opt options = find_opt (fun s -> "--" ^ s) options

    let parse other find_short_opt find_long_opt args =
      let rec loop args =
        let rec gather_args name n args =
          try 
            List.split_nth n args 
          with
            List.Invalid_index _ ->
              raise (Error (name, "missing required arguments"))
        in
        let gather_long_opt s args =
          let (h, t) = split1 s "=" in
          let (_, nargs, action) = find_long_opt (String.slice ~first:2 h) in
          let (accum, args') = gather_args h (nargs - List.length t) args in
          action h (t @ accum); args'
        in
        let rec gather_short_opt_concat seen_args s k args =
          if k < String.length s then
            let ostr = sprintf "-%c" s.[k]
            and (_, nargs, action) = find_short_opt s.[k] in
            if nargs = 0 then
              begin
                action ostr [];
                gather_short_opt_concat seen_args s (k + 1) args
              end
            else if not seen_args then
              let (accum, args') = gather_args ostr nargs args in
              action ostr accum; gather_short_opt_concat true s (k + 1) args'
            else
              raise
                (Error
                   (sprintf "-%c" s.[k],
                    sprintf "option list '%s' already contains an option requiring an argument"
                      s))
          else args
        in
        let gather_short_opt s k args =
          let ostr = sprintf "-%c" s.[k] in
          let (_, nargs, action) = find_short_opt s.[k] in
          if nargs = 0 then gather_short_opt_concat false s k args
          else
            let (accum, args') =
              let h = String.slice ~first:(k+1) s in
              if String.length h = 0 then gather_args ostr nargs args
              else
                let (t, args'') = gather_args ostr (nargs - 1) args in
                h :: t, args''
            in
            action ostr accum; args'
        in
        match args with
          [] -> []
        | arg :: args' ->
            if arg = "--" then args'
            else if String.starts_with arg "--" then
              loop (gather_long_opt arg args')
            else if arg = "-" then begin other arg; loop args' end
            else if String.starts_with arg "-" then
              loop (gather_short_opt arg 1 args')
            else begin other arg; loop args' end
      in
      let args' = loop args in List.iter other args'
  end


module Opt =
  struct

    exception No_value
    exception Option_error of string * string
    exception Option_help

    type 'a t = { 
      option_set : string -> string list -> unit;
      option_set_value : 'a -> unit;
      option_get : unit -> 'a option;
      option_metavars : string list;
      option_defhelp : string option 
    }

    let get opt =
      match opt.option_get () with
        Some x -> x
      | None -> raise No_value

    let set opt v =
      opt.option_set_value v

    let is_set opt = Option.is_some (opt.option_get ())

    let opt opt = opt.option_get ()

    let value_option metavar default coerce errfmt =
      let data = ref default in
      {
        option_metavars = [metavar]; 
        option_defhelp = None;
        option_get = (fun _ -> !data);
        option_set_value = (fun x -> data := Some x);
        option_set =
         (fun option args ->
            let arg = List.hd args in
              try 
                data := Some (coerce arg)
              with
                  exn -> raise (Option_error (option, errfmt exn arg)))
      }

    let callback_option metavar coerce errfmt f =
      {
        option_metavars = [metavar]; 
        option_defhelp = None;
        option_get = (fun _ -> Some ());
        option_set_value = (fun () -> ());
        option_set =
         (fun option args ->
            let arg = List.hd args in
            let datum = ref None in
              begin 
              try 
                datum := Some (coerce arg)
              with
                  exn -> raise (Option_error (option, errfmt exn arg))
              end;

              Option.may f !datum)
      }
  end

module StdOpt =
  struct

    open Opt

    let store_const ?default const =
      let data = ref default in
      {
        option_metavars = []; 
        option_defhelp = None;
        option_get = (fun _ -> !data);
        option_set_value = (fun x -> data := Some x);
        option_set = fun _ _ -> data := Some const
      }

    let store_true () = store_const ~default:false true

    let store_false () = store_const ~default:true false

    let int_option ?default ?(metavar = "INT") () =
      value_option metavar default int_of_string
        (fun _ s -> sprintf "invalid integer value '%s'" s)

    let int_callback ?(metavar = "INT") =
      callback_option metavar int_of_string
        (fun _ s -> sprintf "invalid integer value '%s'" s)

    let float_option ?default ?(metavar = "FLOAT") () =
      value_option metavar default float_of_string
        (fun _ s -> sprintf "invalid floating point value '%s'" s)

    let float_callback ?(metavar = "FLOAT") =
      callback_option metavar float_of_string
        (fun _ s -> sprintf "invalid floating point value '%s'" s)

    let str_option ?default ?(metavar = "STR") () =
      value_option metavar default (fun s -> s) (fun _ _ -> "cannot happen")

    let str_callback ?(metavar = "STR") =
      callback_option metavar (fun s -> s) (fun _ _ -> "cannot happen")

    let count_option ?(dest = ref 0) ?(increment = 1) () =
      {
        option_metavars = []; 
        option_defhelp = None;
        option_get = (fun _ -> Some !dest);
        option_set_value = (fun x -> dest := x);
        option_set = fun _ _ -> dest := !dest + increment
      }

    let incr_option ?(dest = ref 0) = 
      count_option ~dest ~increment:1

    let decr_option ?(dest = ref 0) = 
      count_option ~dest ~increment:(-1)

    let help_option () =
      {
        option_metavars = [];
        option_defhelp = Some "show this help message and exit";
        option_get = (fun _ -> raise No_value);
        option_set_value = (fun _ -> ());
        option_set = fun _ _ -> raise Option_help
      }

    let version_option vfunc =
      {
        option_metavars = [];
        option_defhelp = Some "show program's version and exit";
        option_get = (fun _ -> raise No_value);
        option_set_value = (fun _ -> ());
        option_set = fun _ _ -> print_endline (vfunc ()); exit 0
      }
  end




module Formatter =
  struct

    (* Note that the whitespace regexps must NOT treat the non-breaking
       space character as whitespace. *)
    let whitespace = "\t\n\013\014\r "

    let split_into_chunks s =
      let buf = Buffer.create (String.length s) in
      let flush () =
        let s = Buffer.contents buf
        in
          Buffer.clear buf;
          s
      in
      let rec loop state accum i =
        if (i<String.length s) then
          if ((state && not (String.contains whitespace s.[i])) || 
              ((not state) && String.contains whitespace s.[i])) then
            if Buffer.length buf > 0 then
               loop (not state) (flush () :: accum) i 
             else 
               loop (not state) accum i
          else
            begin
              Buffer.add_char buf s.[i];
              loop state accum (i+1)
            end
        else
          if Buffer.length buf > 0 then
            flush () :: accum
          else 
            accum
      in
        List.rev (loop false [] 0)

    let is_whitespace s =
      let rec loop i =
        if i<String.length s then
          if String.contains whitespace s.[i] then
            loop (i+1)
          else 
            false
        else 
          true
      in
        loop 0

    let expand_tabs ?(tab_size = 8) s =
      let len = String.length s in
      let spaces n = String.make n ' '
      and b = Buffer.create len in
      let rec expand i col =
        if i < len then
          match s.[i] with
            '\t' ->
              let n = tab_size - col mod tab_size in
              Buffer.add_string b (spaces n);
              expand (i + 1) (col + n)
          | '\n' -> 
              Buffer.add_string b "\n";
              expand (i + 1) 0
          | c -> 
              Buffer.add_char b c;
              expand  (i + 1) (col + 1)
      in
      expand 0 0; 
      Buffer.contents b

    let wrap ?(initial_indent = 0) ?(subsequent_indent = 0) text _width =
      let wrap_chunks_line width acc =
        let rec wrap (chunks, cur_line, cur_len) =
          match chunks with
            [] -> [], cur_line, cur_len
          | hd :: tl ->
              let l = String.length hd in
              if cur_len + l <= width then
                wrap (tl, hd :: cur_line, cur_len + l)
              else chunks, cur_line, cur_len
        in
        wrap acc
      in
      let wrap_long_last_word width (chunks, cur_line, cur_len) =
        match chunks with
          [] -> [], cur_line, cur_len
        | hd :: tl ->
            let l = String.length hd in
            if l > width then
              match cur_line with
                [] -> tl, [hd], cur_len + l
              | _ -> chunks, cur_line, cur_len
            else chunks, cur_line, cur_len
      in
      let wrap_remove_last_ws (chunks, cur_line, cur_len) =
        match cur_line with
          [] -> chunks, cur_line, cur_len
        | hd :: tl ->
            if is_whitespace hd then chunks, tl, cur_len - String.length hd
            else chunks, cur_line, cur_len
      in
      let rec wrap_chunks_lines chunks lines =
        let indent =
          match lines with
            [] -> initial_indent
          | _ -> subsequent_indent
        in
        let width = _width - indent in
        match chunks with
          hd :: tl ->
            if is_whitespace hd && lines <> [] then wrap_chunks_lines tl lines
            else (* skip *)
              let (chunks', cur_line, _) =
                wrap_remove_last_ws
                  (wrap_long_last_word width
                     (wrap_chunks_line width (chunks, [], 0)))
              in
              wrap_chunks_lines chunks'
                ((String.make indent ' ' ^
                    String.concat "" (List.rev cur_line)) ::
                   lines)
        | [] -> List.rev lines
      in
      let chunks = split_into_chunks (expand_tabs text) in
      wrap_chunks_lines chunks []


    let fill ?(initial_indent = 0) ?(subsequent_indent = 0) text width =
      String.concat "\n" (wrap ~initial_indent ~subsequent_indent text width)



    type t = { 
      indent : unit -> unit;
      dedent : unit -> unit;
      format_usage : string -> string;
      format_heading : string -> string;
      format_description : string -> string;
      format_option : char list * string list -> string list -> 
                                             string option -> string
    }

    let format_option_strings short_first (snames, lnames) metavars =
      let metavar = String.concat " " metavars in
      let lopts =
        List.map
          (match metavar with
             "" -> (fun z -> sprintf "--%s" z)
           | _ -> fun z -> sprintf "--%s=%s" z metavar)
          lnames
      and sopts = List.map (fun x -> sprintf "-%c%s" x metavar) snames in
      match short_first with
        true -> String.concat ", " (sopts @ lopts)
      | false -> String.concat ", " (lopts @ sopts)


    let indented_formatter ?level:(extlevel = ref 0)
      ?indent:(extindent = ref 0) ?(indent_increment = 2) 
      ?(max_help_position = 24) ?(width = terminal_width - 1) 
      ?(short_first = true) () =
      let indent = ref 0
      and level = ref 0 in
      let help_position = ref max_help_position
      and help_width = ref (width - max_help_position) in
      {
        indent =
         (fun () ->
            indent := !indent + indent_increment;
            level := !level + 1;
            extindent := !indent;
            extlevel := !level);

        dedent =
         (fun () ->
            indent := !indent - indent_increment;
            level := !level - 1;
            assert (!level >= 0);
            extindent := !indent;
            extlevel := !level);
        
        format_usage = (fun usage -> sprintf "usage: %s\n" usage);
        
        format_heading =
         (fun heading -> sprintf "%*s%s:\n\n" !indent "" heading);
        
        format_description =
         (fun description ->
            let x =
              fill ~initial_indent:(!indent) ~subsequent_indent:(!indent)
                description (width - !indent)
            in
              if not (String.ends_with x "\n") then x ^ "\n\n" else x ^ "\n");
        
        format_option =
         fun names metavars help ->
           let opt_width = !help_position - !indent - 2 in
           let opt_strings =
             format_option_strings short_first names metavars
           in
           let buf = Buffer.create 256 in
           let indent_first =
             if String.length opt_strings > opt_width then
               begin
                 bprintf buf "%*s%s\n" !indent "" opt_strings; !help_position
               end
             else
               begin
                 bprintf buf "%*s%-*s  " !indent "" opt_width opt_strings; 0
               end
           in
           Option.may
             (fun option_help ->
                let lines = wrap option_help !help_width in
                match lines with
                  h :: t ->
                    bprintf buf "%*s%s\n" indent_first "" h;
                    List.iter
                      (fun x -> bprintf buf "%*s%s\n" !help_position "" x) t
                | [] -> ())
             help;

           let contents =
             Buffer.contents buf
           in
             if String.length contents > 0 && not (String.ends_with contents "\n") then
               contents ^ "\n"
             else
               contents
      }

    let titled_formatter ?(level = ref 0) ?(indent = ref 0) 
      ?(indent_increment = 0) ?(max_help_position = 24) 
      ?(width = terminal_width - 1) ?(short_first = true) 
      () =
      let formatter =
        indented_formatter ~level ~indent ~indent_increment ~max_help_position
          ~width ~short_first ()
      in
      let format_heading h =
        let c =
          match !level with
            0 -> '='
          | 1 -> '-'
          | _ -> failwith "titled_formatter: Too much indentation"
        in
        sprintf "%*s%s\n%*s%s\n\n" !indent "" (String.capitalize h) !indent ""
          (String.make (String.length h) c)
      in
      let format_usage usage =
        sprintf "%s  %s\n" (format_heading "Usage") usage
      in
      { formatter with 
          format_usage = format_usage;
          format_heading = format_heading
      }
  end



open Opt
open Formatter

module OptParser =
  struct

    exception Option_conflict of string

    type group = { 
      og_heading : string;
      og_description : string option;
      og_options :
        ((char list * string list) * string list * string option) RefList.t;
      og_children : group RefList.t 
    }

    type t = { 
      op_usage : string;
      op_suppress_usage : bool;
      op_prog : string;

      op_formatter : Formatter.t;
      
      op_long_options : GetOpt.long_opt RefList.t;
      op_short_options : GetOpt.short_opt RefList.t;
      
      op_groups : group 
    }

    let unprogify optparser s =
      (snd (String.replace ~str:s ~sub:"%prog" ~by:optparser.op_prog))

    let add optparser ?(group = optparser.op_groups) ?help ?(hide = false)
      ?short_name ?(short_names = []) ?long_name ?(long_names = []) opt =
      let lnames =
        match long_name with
            None -> long_names
          | Some x -> x :: long_names
      and snames =
        match short_name with
            None -> short_names
          | Some x -> x :: short_names
      in
      if lnames = [] && snames = [] then
        failwith "Options must have at least one name"
      else
        (* Checking for duplicates: *)
        let snames' =
          List.fold_left (fun r (x, _, _) -> x :: r) []
            (RefList.to_list optparser.op_short_options)
        and lnames' =
          List.fold_left (fun r (x, _, _) -> x :: r) []
            (RefList.to_list optparser.op_long_options)
        in
        let sconf =
          List.filter (fun e -> List.exists (( = ) e) snames') snames
        and lconf =
          List.filter (fun e -> List.exists (( = ) e) lnames') lnames
        in
        if List.length sconf > 0 then
          raise (Option_conflict (sprintf "-%c" (List.hd sconf)))
        else if List.length lconf > 0 then
          raise (Option_conflict (sprintf "--%s" (List.hd lconf)));
          
        (* Add to display list. *)
        if not hide then
          RefList.add group.og_options
            ((snames, lnames), opt.option_metavars,
             (match help with
                  None -> opt.option_defhelp
                | Some _ -> help));
          
        (* Getopt: *)
        let nargs = List.length opt.option_metavars in
          List.iter
            (fun short ->
               RefList.add optparser.op_short_options
               (short, nargs, opt.option_set))
            snames;
          List.iter
            (fun long ->
               RefList.add optparser.op_long_options
               (long, nargs, opt.option_set))
            lnames
            
    let add_group optparser ?(parent = optparser.op_groups) ?description heading =
      let g =
        {
          og_heading = heading; 
          og_description = description;
          og_options = RefList.empty (); 
          og_children = RefList.empty ()
        }
      in
      RefList.add parent.og_children g; g

    let make ?(usage = "%prog [options]") ?description ?version
      ?(suppress_usage = false) ?(suppress_help = false) ?prog 
      ?(formatter = Formatter.indented_formatter ()) () =
      let optparser =
        {
          op_usage = usage; 
          op_suppress_usage = suppress_usage;
          op_prog = Option.default (Filename.basename Sys.argv.(0)) prog;
          op_formatter = formatter; 
          op_short_options = RefList.empty ();
          op_long_options = RefList.empty ();
          op_groups = {
            og_heading = "options"; 
            og_options = RefList.empty ();
            og_children = RefList.empty ();
            og_description = description
          }
        }
      in
      Option.may                         (* Add version option? *)
        (fun version ->
           add optparser ~long_name:"version"
             (StdOpt.version_option
                (fun () -> unprogify optparser version)))
        version;
      if not suppress_help then              (* Add help option? *)
        add optparser ~short_name:'h' ~long_name:"help"
          (StdOpt.help_option ());

      optparser

    let format_usage optparser eol =
      match optparser.op_suppress_usage with
        true -> ""
      | false ->
          unprogify optparser
            (optparser.op_formatter.format_usage optparser.op_usage) ^ eol

    let error optparser ?(chn = stderr) ?(status = 1) message =
      fprintf chn "%s%s: %s\n" (format_usage optparser "\n") optparser.op_prog
        message;
      flush chn;
      exit status

    let usage optparser ?(chn = stdout) () =
      let rec loop g =
        (* Heading: *)
        output_string chn
          (optparser.op_formatter.format_heading g.og_heading);

        optparser.op_formatter.indent ();
        (* Description: *)
        Option.may
          (fun x ->
             output_string chn (optparser.op_formatter.format_description x))
          g.og_description;
        (* Options: *)
        RefList.iter
          (fun (names, metavars, help) ->
             output_string chn
               (optparser.op_formatter.format_option names metavars help))
          g.og_options;
        (* Child groups: *)
        output_string chn "\n";
        RefList.iter loop g.og_children;

        optparser.op_formatter.dedent ()
      in
      output_string chn (format_usage optparser "\n");
      loop optparser.op_groups;
      flush chn

    let parse optparser ?(first = 0) ?last argv =
      let args = RefList.empty ()
      and n =
        match last with
          None -> Array.length argv - first
        | Some m -> m - first + 1
      in
      begin 
        try
          GetOpt.parse (RefList.push args)
            (GetOpt.find_short_opt
               (RefList.to_list optparser.op_short_options))
            (GetOpt.find_long_opt (RefList.to_list optparser.op_long_options))
            (Array.to_list (Array.sub argv first n))
        with
            GetOpt.Error (opt, errmsg) ->
              error optparser (sprintf "option '%s': %s" opt errmsg)
          | Option_error (opt, errmsg) ->
              error optparser (sprintf "option '%s': %s" opt errmsg)
          | Option_help -> usage optparser (); exit 0
      end;
      List.rev (RefList.to_list args)

    let parse_argv optparser = 
      parse optparser ~first:1 Sys.argv
  end
