(*
 * Copyright (c) 2015 Nicolas Ojeda Bar <n.oje.bar@gmail.com>
 *
 * Permission to use, copy, modify, and distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 *)

open Printf
open Ppxlib
open Ast_helper

module Ast = struct
  include Ast_builder.Default

  let econstr ~loc tag xs =
    pexp_construct ~loc (Loc.make ~loc (lident tag)) (
      match xs with
      | [] -> None
      | _ -> Some (pexp_tuple ~loc xs)
    )

  let pconstr ~loc tag xs =
    ppat_construct ~loc (Loc.make ~loc (lident tag)) (
      match xs with
      | [] -> None
      | _ -> Some (ppat_tuple ~loc xs)
    )

  let tconstr ~loc tag xs =
    ptyp_constr ~loc (Loc.make ~loc (lident tag)) xs
end

type mode = Big_endian | Little_endian | Host_endian | Bi_endian

type prim =
  | Char
  | UInt8
  | UInt16
  | UInt32
  | UInt64

type ty =
  | Prim of prim
  | Buffer of prim * int

type raw_field = {
  name: string;
  ty: ty;
  definition_loc: Location.t;
}

type named_field = {
  name: string;
  ty: ty;
  definition_loc: Location.t;
  off: int;
}

type field =
  | Named_field of named_field
  | Ignored_field

let field_is_ignored name =
  String.get name 0 = '_'

type t = {
  name: string;
  fields: field list;
  len: int;
  endian: mode;
}

let ty_of_string =
  function
  |"char_t"  |"char" -> Some Char
  |"uint8_t" |"uint8" |"int8" |"int8_t"  -> Some UInt8
  |"uint16_t"|"uint16"|"int16"|"int16_t" -> Some UInt16
  |"uint32_t"|"uint32"|"int32"|"int32_t" -> Some UInt32
  |"uint64_t"|"uint64"|"int64"|"int64_t" -> Some UInt64
  |_ -> None

let width_of_prim = function
  | Char -> 1
  | UInt8 -> 1
  | UInt16 -> 2
  | UInt32 -> 4
  | UInt64 -> 8

let width_of_ty = function
  | Prim p -> width_of_prim p
  | Buffer (p, len) -> width_of_prim p * len

let width_of_field f =
  width_of_ty f.ty

let field_to_string f =
  let rec string = function
    |Prim Char -> "char_t"
    |Prim UInt8 -> "uint8_t"
    |Prim UInt16 -> "uint16_t"
    |Prim UInt32 -> "uint32_t"
    |Prim UInt64 -> "uint64_t"
    |Buffer (prim, len) -> sprintf "%s[%d]" (string (Prim prim)) len
  in
  sprintf "%s %s" (string f.ty) f.name

let loc_err loc fmt = Location.raise_errorf ~loc ("ppx_cstruct: " ^^ fmt)

let parse_field loc name field_type sz =
  match ty_of_string field_type with
  |None -> loc_err loc "Unknown type %s" field_type
  |Some ty -> begin
    let ty = match ty,sz with
      |_,None -> Prim ty
      |prim,Some sz -> Buffer (prim, sz)
    in
    { name; ty; definition_loc = loc }
  end

let check_for_duplicates fields =
  let module StringSet = Set.Make(String) in
  let _ : StringSet.t =
    List.fold_left (fun seen f ->
        match f with
        | Ignored_field -> seen
        | Named_field {name; definition_loc; _} ->
          if StringSet.mem name seen then
            loc_err definition_loc "field %s is present several times in this type" name
          else
            StringSet.add name seen
      )
      StringSet.empty
      fields
  in
  ()

let create_struct loc endian name fields =
  let endian = match endian with
    |"little_endian" -> Little_endian
    |"big_endian" -> Big_endian
    |"host_endian" -> Host_endian
    |"bi_endian" -> Bi_endian
    |_ -> loc_err loc "unknown endian %s, should be little_endian, big_endian, host_endian or bi_endian" endian
  in
  let len, fields =
    List.fold_left (fun (off,acc) ({name; ty; definition_loc}:raw_field) ->
        let field =
          if field_is_ignored name then
            Ignored_field
          else
            Named_field { name; ty; off; definition_loc }
        in
        let off = width_of_ty ty + off in
        let acc = acc @ [field] in
        (off, acc)
      ) (0,[]) fields
  in
  check_for_duplicates fields;
  { fields; name = name.txt; len; endian }

let ($.) l x = Longident.Ldot (l, x)
let cstruct_id = Longident.Lident "Cstruct"
let mode_mod s = function
  |Big_endian -> cstruct_id$."BE"$.s
  |Little_endian -> cstruct_id$."LE"$.s
  |Host_endian -> cstruct_id$."HE"$.s
  |Bi_endian -> cstruct_id$."BL"$.s

let mode_mod loc x s =
  Exp.ident ~loc {loc ; txt = mode_mod s x}

type op =
  | Op_get of named_field
  | Op_set of named_field
  | Op_copy of named_field
  | Op_blit of named_field
  | Op_sizeof
  | Op_hexdump
  | Op_hexdump_to_buffer

let op_name s op =
  let parts =
    match op with
    | Op_get f -> ["get"; s.name; f.name]
    | Op_set f -> ["set"; s.name; f.name]
    | Op_copy f -> ["copy"; s.name; f.name]
    | Op_blit f -> ["blit"; s.name; f.name]
    | Op_sizeof -> ["sizeof"; s.name]
    | Op_hexdump -> ["hexdump"; s.name]
    | Op_hexdump_to_buffer -> ["hexdump"; s.name; "to_buffer"]
  in
  String.concat "_" parts

let op_pvar ~loc s op = Ast.pvar ~loc (op_name s op)
let op_evar ~loc s op = Ast.evar ~loc (op_name s op)

let get_expr loc s f =
  let m = mode_mod loc s.endian in
  let num x = Ast.eint ~loc x in
  match f.ty with
  |Buffer (_, _) ->
    let len = width_of_field f in
    [%expr
      fun src -> Cstruct.sub src [%e num f.off] [%e num len]
    ]
  |Prim prim ->
    [%expr
      fun v ->
        [%e match prim with
            |Char -> [%expr Cstruct.get_char v [%e num f.off]]
            |UInt8 -> [%expr Cstruct.get_uint8 v [%e num f.off]]
            |UInt16 -> [%expr [%e m "get_uint16"] v [%e num f.off]]
            |UInt32 -> [%expr [%e m "get_uint32"] v [%e num f.off]]
            |UInt64 -> [%expr [%e m "get_uint64"] v [%e num f.off]]]]

let type_of_int_field ~loc = function
  |Char -> [%type: char]
  |UInt8 -> [%type: Cstruct.uint8]
  |UInt16 -> [%type: Cstruct.uint16]
  |UInt32 -> [%type: Cstruct.uint32]
  |UInt64 -> [%type: Cstruct.uint64]

let set_expr loc s f =
  let m = mode_mod loc s.endian in
  let num x = Ast.eint ~loc x in
  match f.ty with
  |Buffer (_,_) ->
    let len = width_of_field f in
    [%expr
      fun src srcoff dst ->
        Cstruct.blit_from_string src srcoff dst [%e num f.off] [%e num len]]
  |Prim prim ->
    [%expr fun v x ->
        [%e match prim with
            |Char -> [%expr Cstruct.set_char v [%e num f.off] x]
            |UInt8 -> [%expr Cstruct.set_uint8 v [%e num f.off] x]
            |UInt16 -> [%expr [%e m "set_uint16"] v [%e num f.off] x]
            |UInt32 -> [%expr [%e m "set_uint32"] v [%e num f.off] x]
            |UInt64 -> [%expr [%e m "set_uint64"] v [%e num f.off] x]]]

let type_of_set ~loc f =
  match f.ty with
  |Buffer (_,_) ->
    [%type: string -> int -> Cstruct.t -> unit]
  |Prim prim ->
    let retf = type_of_int_field ~loc prim in
    [%type: Cstruct.t -> [%t retf] -> unit]

let hexdump_expr ~loc s =
  [%expr fun v ->
    let buf = Buffer.create 128 in
    Buffer.add_string buf [%e Ast.estring ~loc (s.name ^ " = {\n")];
    [%e op_evar ~loc s Op_hexdump_to_buffer] buf v;
    print_endline (Buffer.contents buf);
    print_endline "}"
  ]

let hexdump_to_buffer_expr ~loc s =
  let prim_format_string = function
    | Char -> [%expr "%c\n"]
    | UInt8 | UInt16 -> [%expr "0x%x\n"]
    | UInt32 -> [%expr "0x%lx\n"]
    | UInt64 -> [%expr "0x%Lx\n"]
  in
  let hexdump_field = function
    | Ignored_field ->
      [%expr ()]
    | Named_field f ->
      let get_f = op_evar ~loc s (Op_get f) in
      let expr =
        match f.ty with
        |Prim p ->
          [%expr Printf.bprintf buf [%e prim_format_string p] ([%e get_f] v)]
        |Buffer (_,_) ->
          [%expr Printf.bprintf buf "<buffer %s>" [%e Ast.estring ~loc (field_to_string f)];
            Cstruct.hexdump_to_buffer buf ([%e get_f] v)]
    in
    [%expr
      Printf.bprintf buf "  %s = " [%e Ast.estring ~loc f.name];
      [%e expr]]
  in
  [%expr fun buf v -> [%e Ast.esequence ~loc (List.map hexdump_field s.fields)]]

let op_expr loc s = function
  | Op_sizeof -> Ast.eint ~loc s.len
  | Op_hexdump -> hexdump_expr ~loc s
  | Op_hexdump_to_buffer -> hexdump_to_buffer_expr ~loc s
  | Op_get f -> get_expr loc s f
  | Op_set f -> set_expr loc s f
  | Op_copy f ->
    let len = width_of_field f in
    [%expr fun src -> Cstruct.to_string src ~off:[%e Ast.eint ~loc f.off] ~len:[%e Ast.eint ~loc len] ]
  | Op_blit f ->
    let len = width_of_field f in
    [%expr fun src srcoff dst ->
      Cstruct.blit src srcoff dst [%e Ast.eint ~loc f.off] [%e Ast.eint ~loc len]]

let field_ops_for =
  function
  | Ignored_field ->
    []
  | Named_field f ->
    let if_buffer x =
      match f.ty with
      |Buffer (_,_) -> [x]
      |Prim _ -> []
    in
    List.concat
      [ [Op_get f]
      ; if_buffer (Op_copy f)
      ; [Op_set f]
      ; if_buffer (Op_blit f)
      ]

let ops_for s =
  ( [Op_sizeof]
  @ List.concat (List.map field_ops_for s.fields)
  @ [Op_hexdump_to_buffer;
     Op_hexdump;
    ])

(** Generate functions of the form {get/set}_<struct>_<field> *)
let output_struct_one_endian loc s =
  List.map
    (fun op ->
       [%stri let[@ocaml.warning "-32"] [%p op_pvar ~loc s op] =
                [%e op_expr loc s op]])
    (ops_for s)

let output_struct _loc s =
  match s.endian with
  | Bi_endian ->
    (* In case of Bi-endian, create two modules - one for BE and one for LE *)
    let expr_be = Mod.structure (output_struct_one_endian _loc {s with endian = Big_endian})
    and expr_le = Mod.structure (output_struct_one_endian _loc {s with endian = Little_endian})

    in [{pstr_desc = Pstr_module
        {pmb_name = {txt = Some "BE"; loc = _loc}; pmb_expr = expr_be;
        pmb_attributes = []; pmb_loc = _loc;}; pstr_loc = _loc;};
        {pstr_desc = Pstr_module
        {pmb_name = {txt = Some "LE"; loc = _loc}; pmb_expr = expr_le;
        pmb_attributes = []; pmb_loc = _loc;}; pstr_loc = _loc;}
        ]
  | _ -> output_struct_one_endian _loc s

let type_of_get ~loc f =
  match f.ty with
  |Buffer (_,_) ->
    [%type: Cstruct.t -> Cstruct.t]
  |Prim prim ->
    let retf = type_of_int_field ~loc prim in
    [%type: Cstruct.t -> [%t retf]]

let op_typ ~loc = function
  | Op_sizeof -> [%type: int]
  | Op_hexdump_to_buffer -> [%type: Buffer.t -> Cstruct.t -> unit]
  | Op_hexdump -> [%type: Cstruct.t -> unit]
  | Op_get f -> type_of_get ~loc f
  | Op_set f -> type_of_set ~loc f
  | Op_copy _ -> [%type: Cstruct.t -> string]
  | Op_blit _ -> [%type: Cstruct.t -> int -> Cstruct.t -> unit]

(** Generate signatures of the form {get/set}_<struct>_<field> *)
let output_struct_sig loc s =
  List.map
    (fun op ->
       Sig.value
         (Val.mk
            (Loc.make (op_name s op) ~loc)
            (op_typ ~loc op)))
    (ops_for s)

type enum_op =
  | Enum_to_sexp
  | Enum_of_sexp
  | Enum_get
  | Enum_set
  | Enum_print
  | Enum_parse
  | Enum_compare

type cenum =
  { name : string Loc.t;
    fields : (string Loc.t * int64) list;
    prim : prim;
    sexp : bool;
  }

let enum_op_name cenum =
  let s = cenum.name.txt in
  function
  | Enum_to_sexp -> sprintf "sexp_of_%s" s
  | Enum_of_sexp -> sprintf "%s_of_sexp" s
  | Enum_get -> sprintf "int_to_%s" s
  | Enum_set -> sprintf "%s_to_int" s
  | Enum_print -> sprintf "%s_to_string" s
  | Enum_parse -> sprintf "string_to_%s" s
  | Enum_compare -> sprintf "compare_%s" s

let enum_pattern ~loc {prim; _} =
  let pat_integer f suffix i =
    Pat.constant (Pconst_integer(f i, suffix))
  in
  match prim with
  | Char ->
    (fun i -> Ast.pchar ~loc (Char.chr (Int64.to_int i)))
  | (UInt8 | UInt16) -> pat_integer Int64.to_string None
  | UInt32 -> pat_integer (fun i -> Int32.to_string (Int64.to_int32 i)) (Some 'l')
  | UInt64 -> pat_integer Int64.to_string (Some 'L')

let enum_integer ~loc {prim; _} =
  let expr_integer f suffix i =
    Exp.constant (Pconst_integer(f i, suffix))
  in
  match prim with
    | Char -> (fun i -> Ast.echar ~loc (Char.chr (Int64.to_int i)))
    | (UInt8 | UInt16) -> expr_integer Int64.to_string None
    | UInt32 -> expr_integer (fun i -> Int32.to_string (Int64.to_int32 i)) (Some 'l')
    | UInt64 -> expr_integer Int64.to_string (Some 'L')

let declare_enum_expr ~loc ({fields; _} as cenum) = function
  | Enum_to_sexp ->
    [%expr fun x -> Sexplib.Sexp.Atom ([%e Ast.evar ~loc (enum_op_name cenum Enum_print)] x) ]
  | Enum_of_sexp ->
    [%expr
      fun x ->
        match x with
        | Sexplib.Sexp.List _ ->
          raise (Sexplib.Pre_sexp.Of_sexp_error (Failure "expected Atom, got List", x))
        | Sexplib.Sexp.Atom v ->
          match [%e Ast.evar ~loc (enum_op_name cenum Enum_parse)] v with
          | None ->
            raise (Sexplib.Pre_sexp.Of_sexp_error (Failure "unable to parse enum string", x))
          | Some r -> r
    ]
  | Enum_get ->
    let getters = (List.map (fun ({txt = f; _},i) ->
        Exp.case (enum_pattern ~loc cenum i) [%expr Some [%e Ast.econstr ~loc f []]]
      ) fields) @ [Exp.case [%pat? _] [%expr None]]
    in
    Exp.function_ getters
  | Enum_set ->
    let setters = List.map (fun ({txt = f; _},i) ->
        Exp.case (Ast.pconstr ~loc f []) (enum_integer ~loc cenum i)
      ) fields in
    Exp.function_ setters
  | Enum_print ->
    let printers = List.map (fun ({txt = f; _},_) ->
        Exp.case (Ast.pconstr ~loc f []) (Ast.estring ~loc f)
      ) fields in
    Exp.function_ printers
  | Enum_parse ->
    let parsers = List.map (fun ({txt = f; _},_) ->
        Exp.case (Ast.pstring ~loc f) [%expr Some [%e Ast.econstr ~loc f []]]
      ) fields in
    Exp.function_ (parsers @ [Exp.case [%pat? _] [%expr None]])
  | Enum_compare -> [%expr fun x y ->
    let to_int = [%e Ast.evar ~loc (enum_op_name cenum Enum_set)] in
    Stdlib.compare (to_int x) (to_int y)
  ]

let enum_ops_for {sexp; _} =
  Enum_get ::
  Enum_set ::
  Enum_compare ::
  Enum_print ::
  Enum_parse ::
  if sexp then
    [ Enum_to_sexp
    ; Enum_of_sexp
    ]
  else
    []

let enum_type_decl {name; fields; _} =
  let decls = List.map (fun (f,_) -> Type.constructor f) fields in
  Type.mk ~kind:(Ptype_variant decls) name

let output_enum ~loc cenum =
  Str.type_ Recursive [enum_type_decl cenum] ::
  List.map
    (fun op ->
       [%stri
         let[@ocaml.warning "-32"] [%p Ast.pvar ~loc (enum_op_name cenum op)] =
           [%e declare_enum_expr ~loc cenum op]
       ])
    (enum_ops_for cenum)

let enum_op_type ~loc {name; prim; _} =
  let cty = Ast.tconstr ~loc name.txt [] in
  let oty = match prim with
    | Char -> [%type: char]
    | (UInt8|UInt16) -> [%type: int]
    | UInt32 -> [%type: int32]
    | UInt64 -> [%type: int64]
  in
  function
  | Enum_get -> [%type: [%t oty] -> [%t cty] option]
  | Enum_set -> [%type: [%t cty] -> [%t oty]]
  | Enum_print -> [%type: [%t cty] -> string]
  | Enum_parse -> [%type: string -> [%t cty] option]
  | Enum_to_sexp -> [%type: [%t cty] -> Sexplib.Sexp.t]
  | Enum_of_sexp -> [%type: Sexplib.Sexp.t -> [%t cty]]
  | Enum_compare -> [%type: [%t cty] -> [%t cty] -> int]

let output_enum_sig loc (cenum:cenum) =
  Sig.type_ Recursive [enum_type_decl cenum] ::
  List.map
    (fun op ->
       let name = enum_op_name cenum op in
       let typ = enum_op_type ~loc cenum op in
       Sig.value (Val.mk (Loc.make name ~loc) typ))
    (enum_ops_for cenum)

let constr_enum = function
  | {pcd_name = f; pcd_args = Pcstr_tuple []; pcd_attributes = attrs; _} ->
    let id = match attrs with
      | [{attr_name = {txt = "id";_}; 
          attr_payload = 
            PStr [{ pstr_desc = 
              Pstr_eval ({pexp_desc = Pexp_constant cst; pexp_loc = loc; _}, _); _}]
          ;_ }] ->
        let cst = match cst with
          | Pconst_integer(i, _) -> Int64.of_string i
          | _ ->
            loc_err loc "invalid id"
        in
        Some cst
      | _ ->
        None
    in
    (f, id)
  | {pcd_loc = loc; _} ->
    loc_err loc "invalid cenum variant"

let get_len = function
  | [{attr_name = {txt = "len"; loc};
      attr_payload = PStr
        [{pstr_desc =
          Pstr_eval ({pexp_desc = Pexp_constant (Pconst_integer (sz, None)); _}, _);
          _}]
      ; _}]
    ->
    let n = int_of_string sz in
    if n > 0 then
      Some n
    else
      loc_err loc "[@len] argument should be > 0"
  | [{attr_name = {txt = "len"; loc}; _} ] ->
    loc_err loc "[@len] argument should be an integer"
  | _ ->
    None

let constr_field {pld_name = fname; pld_type = fty; pld_loc = loc; pld_attributes = att; _} =
  let sz = match get_len fty.ptyp_attributes, get_len att with
  | Some sz, None
  | None, Some sz -> Some sz
  | Some _, Some _ -> loc_err loc "multiple field length attribute"
  | None, None -> None
  in
  let fty = match fty.ptyp_desc with
    | Ptyp_constr ({txt = Lident fty; _}, []) -> fty
    | _ ->
      loc_err fty.ptyp_loc "type identifier expected"
  in
  parse_field loc fname.txt fty sz

let cstruct decl =
  let {ptype_name = name; ptype_kind = kind;
       ptype_attributes = attrs; ptype_loc = loc; _} = decl in
  let fields = match kind with
    | Ptype_record fields -> List.map constr_field fields
    | _ -> loc_err loc "record type declaration expected"
  in
  let endian = match attrs with
    | [{attr_name = {txt = endian; _}; attr_payload = PStr []; _}] -> endian
    | [_] -> loc_err loc "no attribute payload expected"
    | _ -> loc_err loc "too many attributes"
  in
  create_struct loc endian name fields

let cenum decl =
  let {ptype_name = name; ptype_kind = kind;
       ptype_attributes = attrs; ptype_loc = loc; _} = decl in
  let fields = match kind with
    | Ptype_variant fields -> fields
    | _ ->
      loc_err loc "expected variant type"
  in
  let width, sexp =
    match attrs with
    | ({attr_name = {txt = width; _};attr_payload= PStr [];_}) 
      :: ({attr_name = {txt = "sexp"; _};attr_payload = PStr []; _}) :: [] ->
      width, true
    | ({attr_name = {txt = width; _};attr_payload= PStr [];_}) :: [] ->
      width, false
    | _ ->
      loc_err loc "invalid cenum attributes"
  in
  let n = ref Int64.minus_one in
  let incr_n () = n := Int64.succ !n in
  let fields = List.map constr_enum fields in
  let fields =
    List.map (function
        | (f, None)   -> incr_n (); (f, !n)
        | (f, Some i) -> n := i; (f, i)
      ) fields in
  let prim = match ty_of_string width with
    | None -> loc_err loc "enum: unknown width specifier %s" width
    | Some p -> p
  in
  { name;
    fields;
    prim;
    sexp;
  }

let signature_item' mapper = function
  | {psig_desc =
       Psig_extension (({txt = "cstruct"; _}, PStr [{pstr_desc = Pstr_type(_, [decl]); _}]), _);
     psig_loc = loc} ->
    output_struct_sig loc (cstruct decl)
  | {psig_desc =
       Psig_extension (({txt = "cenum"; _}, PStr [{pstr_desc = Pstr_type(_, [decl]); _}]), _);
     psig_loc = loc} ->
    output_enum_sig loc (cenum decl)
  | other -> [mapper other]

let structure_item' mapper = function
  | {pstr_desc =
       Pstr_extension (({txt = "cstruct"; _}, PStr [{pstr_desc = Pstr_type(_, [decl]); _}]), _);
     pstr_loc = loc} ->
    output_struct loc (cstruct decl)
  | {pstr_desc =
       Pstr_extension (({txt = "cenum"; _}, PStr [{pstr_desc = Pstr_type(_, [decl]); _}]), _);
     pstr_loc = loc ;
     _ } ->
    output_enum ~loc (cenum decl)
  | other -> [mapper other]

class mapper = object
  inherit Ast_traverse.map as super

  method! signature s =
    List.concat (List.map (signature_item' super#signature_item) s)

  method! structure s =
    List.concat (List.map (structure_item' super#structure_item) s)
end

let () =
  let mapper = new mapper in
  Driver.register_transformation "ppx_cstruct" ~impl:mapper#structure ~intf:mapper#signature
