(* $Id: csview.ml,v 1.9 2002/06/19 15:08:40 garrigue Exp $ *)

(* Compile with
   ocamlc -pp camlp4o -I +lablgtk lablgtk.cma csview.ml -o csview
*)

open StdLabels

(* A simple CSV data viewer *)

type data =
    { fields : string list;
      titles : string list;
      data : string list list }

let mem_string ~char s =
  try
    for i = 0 to String.length s - 1 do
      if s.[i] = char then raise Exit
    done;
    false
  with Exit -> true

let rec until ~chars ?(escapes="") ?(buf = Buffer.create 80) s =
  match Stream.peek s with
    Some c ->
      if mem_string ~char:c escapes then begin
        Stream.junk s;
        Buffer.add_char buf (Stream.next s);
        until ~chars ~escapes ~buf s
      end else if mem_string ~char:c chars then
        Buffer.contents buf
      else begin
        Buffer.add_char buf c;
        Stream.junk s;
        until ~chars ~escapes ~buf s
      end
  | None ->
      if Buffer.length buf > 0 then raise (Stream.Error "until")
      else raise Stream.Failure

let rec ignores ?(chars = " \t") s =
  match Stream.peek s with
    Some c when mem_string ~char:c chars ->
      Stream.junk s; ignores ~chars s
  | _ -> ()

let parse_field = parser
    [< ''"'; f = until ~chars:"\"" ~escapes:"\\"; ''"'; _ = ignores >] ->
      for i = 0 to String.length f - 1 do
        if f.[i] = '\031' then f.[i] <- '\n'
      done;
      f
  | [< f = until ~chars:",\n\r" >] -> f
  | [< >] -> ""

let comma = parser [< '','; _ = ignores >] -> ()

let rec parse_list ~item ~sep = parser
    [< i = item; s >] ->
      begin match s with parser
        [< _ = sep; l = parse_list ~item ~sep >] -> i :: l
      | [< >] -> [i]
      end
  | [< >] -> []

let parse_one = parse_list ~item:parse_field ~sep:comma

let lf = parser [< ''\n'|'\r'; _ = ignores ~chars:"\n\r"; _ = ignores >] -> ()

let parse_all = parse_list ~item:parse_one ~sep:lf

let read_file file =
  let ic = open_in file in
  let s = Stream.of_channel ic in
  let data = parse_all s in
  close_in ic;
  match data with
    ("i"::fields) :: ("T"::titles) :: data ->
      {fields=fields; titles=titles; data=List.map ~f:List.tl data}
  | titles :: data ->
      {fields=titles; titles=titles; data=data}
  | _ -> failwith "Insufficient data"

let print_string s =
  Format.print_char '"';
  for i = 0 to String.length s - 1 do
    match s.[i] with
      '\'' -> Format.print_char '\''
    | '"' -> Format.print_string "\\\""
    | '\160'..'\255' as c -> Format.print_char c
    | c -> Format.print_string (Char.escaped c)
  done;
  Format.print_char '"'  

(*
#install_printer print_string;;
*)

open GMain

let field_widths =
  [ "i", 0;
    "ATTR", 0;
    "NAME", 17;
    "NAPR", 8;
    "TEL1", 14;
    "ZIPC", 12;
    "ADR1", 40;
    "BRTH", 10;
    "RMRK", 20;
    "CHK1", 0;
    "CHK2", 0;
    "CHK3", 0;
    "CHK4", 0;
    "TIM1", 16;
    "TIM2", 16;
    "ALRM", 0;
    "ATTM", 0;
  ]

let main argv =
  if Array.length argv <> 2 then begin
    prerr_endline "Usage: csview <csv file>";
    exit 2
  end;
  let data = read_file argv.(1) in
  let locale = Main.init ~setlocale:true () in
  let w = GWindow.window () in
  w#misc#realize ();
  let style = w#misc#style in
  let font = Gdk.Font.load_fontset "-schumacher-clean-medium-r-normal--13-*-*-*-c-60-*,-mnkaname-fixed-*--12-*" in
  let w0 = Gdk.Font.char_width font '0' in
  style#set_font font;
  w#connect#destroy ~callback:Main.quit;
  let sw = GBin.scrolled_window ~width:600 ~height:300 ~packing:w#add () in
  let cl = GList.clist ~titles:data.titles ~packing:sw#add () in
  List.fold_left data.fields ~init:0 ~f:
    begin fun acc f ->
      let width = try List.assoc f field_widths with Not_found -> -1 in
      if width = 0 then
        cl#set_column ~visibility:false acc
      else begin
        if width > 0 then cl#set_column ~width:(width * w0) acc
        else cl#set_column ~auto_resize:true acc;
        if f = "NAPR" || f = "TIM1" || f = "CLAS" then
          cl#set_sort ~auto:true ~column:acc ();
        try
          let ali = GBin.alignment_cast (cl#column_widget acc) in
          let lbl = GMisc.label_cast (List.hd ali#children) in
          lbl#set_alignment ~x:0. ()
        with _ ->
          prerr_endline ("No column widget for field " ^ f)
      end;
      succ acc
    end;
  List.iter data.data
    ~f:(fun l -> if List.length l > 1 then ignore (cl#append l));
  w#show ();
  Main.main ()

let _ = main Sys.argv
