(** 
  PHP serialization 
  http://php.net/manual/en/function.serialize.php
*)

open ExtLib

(* open Prelude *)
let (>>) x f = f x
let ($) f g = function x -> f (g x)

(*


Anatomy of a serialize()'ed value:

 String
 s:size:value;

 Integer
 i:value;

 Boolean
 b:value; (does not store "true" or "false", does store '1' or '0')

 Null
 N;

 Array
 a:size:{key definition;value definition;(repeated per element)}

 Object
 O:strlen(object name):object name:object size:{s:strlen(property name):property name:property definition;(repeated per property)}

 String values are always in double quotes
 Array keys are always integers or strings
    "null => 'value'" equates to 's:0:"";s:5:"value";',
    "true => 'value'" equates to 'i:1;s:5:"value";',
    "false => 'value'" equates to 'i:0;s:5:"value";',
    "array(whatever the contents) => 'value'" equates to an "illegal offset type" warning because you can't use an
    array as a key; however, if you use a variable containing an array as a key, it will equate to 's:5:"Array";s:5:"value";',
     and
    attempting to use an object as a key will result in the same behavior as using an array will.
*)

type php = AI of (int * php) list | AS of (string * php) list | S of string | I of int | B of bool | F of float | N

let check x y = if x <> y then failwith (Printf.sprintf "Php_serialize failed : %u <> %u" x y)

let rec parse_one = parser
  | [< ''a'; '':'; n=number; '':'; ''{'; a=parse_array; ''}' >] -> ignore n;(*check n (List.length a);*) a
  | [< ''b'; '':'; n=number; '';' >] -> B (0 <> n)
  | [< ''d'; '':'; f=parse_float_semi; >] -> F f
  | [< n=parse_int >] -> I n
  | [< s=parse_str >] -> S s
  | [< ''N'; '';' >] -> N
and number t = parse_nat 0 t
and parse_nat n = parser (* overflow test?* *)
  | [< ''0'..'9' as c; t >] -> let digit = Char.code c - Char.code '0' in parse_nat (n * 10 + digit) t
  | [< >] -> n
and integer = parser
  | [< ''-'; t >] -> - (number t)
  | [< t >] -> number t
and parse_int = parser
  | [< ''i'; '':'; n=integer; '';' >] -> n
and parse_float_semi t = (* ugly, because of one look ahead token FIXME *)
  let buf = Scanf.Scanning.from_function (fun () -> Stream.next t) in
  Scanf.bscanf buf "%f;" (fun f -> f)
and parse_str = parser
  | [< ''s'; '':'; n=number; '':'; ''"'; s=take_string n; ''"'; '';' >] -> s
and take_string n t = String.init n (fun _ -> Stream.next t)
and parse_array = parser
  | [< k=parse_int; v=parse_one; a=parse_int_array [k,v] >] -> AI a
  | [< k=parse_str; v=parse_one; a=parse_str_array [k,v] >] -> AS a
  | [< >] -> AI [] (* empty array *)
and parse_int_array acc = parser
  | [< k=parse_int; v=parse_one; t >] -> parse_int_array ((k,v)::acc) t
  | [< >] -> List.rev acc
and parse_str_array acc = parser
  | [< k=parse_str; v=parse_one; t >] -> parse_str_array ((k,v)::acc) t
  | [< >] -> List.rev acc

let parse stream =
  let show () =
    let tail = Stream.npeek 10 stream >> List.map (String.make 1) >> String.concat "" in
    Printf.sprintf "Position %u : %s" (Stream.count stream) tail
  in
  try
    let r = parse_one stream in
    Stream.empty stream; r
  with
  | Stream.Error _ | Stream.Failure -> failwith (show ())

let parse_string = parse $ Stream.of_string

(** Combinators for easy deconstruction *)

exception Error of string

let fail v str = raise (Error (Printf.sprintf "%s : %s" str (Std.dump v)))

let int = function I n -> n | x -> fail x "int"
let str = function S s -> s | x -> fail x "str"

let opt k x = try Some (k x) with Error _ -> None

let values f = function
  | AS a -> List.map (f $ snd) a
  | AI a -> List.map (f $ snd) a
  | x -> fail x "values"

let array f = function
  | AS a -> List.map (fun (k,v) -> k, f v) a
  | x -> fail x "array"

let assoc php name =
  match php with
  | AS a -> List.assoc name a
  | _ -> fail php "assoc"

module Out = struct

(** Combinators to build values of [php] type *)

let str s = S s
let int n = I n

let  array f e = AI (e >> Enum.mapi (fun i x  -> i, f x) >> List.of_enum)
let iarray f e = AI (e >> Enum.map (fun (k,v) -> k, f v) >> List.of_enum)
let sarray f e = AS (e >> Enum.map (fun (k,v) -> k, f v) >> List.of_enum)

(** Serialize [php] value *)
let output out v =
  let put_arr f a = IO.printf out "a:%u:{" (List.length a); List.iter f a; IO.write out '}' in
  let rec put = function
    | AS a -> put_arr (fun (k,v) -> put (S k); put v) a
    | AI a -> put_arr (fun (k,v) -> put (I k); put v) a
    | I n -> IO.printf out "i:%i;" n
    | B b -> IO.printf out "b:%u;" (if b then 1 else 0)
    | F f -> IO.printf out "d:%f;" f
    | N -> IO.nwrite out "N;"
    | S s -> IO.printf out "s:%u:\"%s\";" (String.length s) s
  in
  put v

end

let to_string v =
  let out = IO.output_string () in
  Out.output out v;
  IO.close_out out