open Import

(* Mini clone of Dune_lang.Decoder. Main advantage is that it forbids all the
   crazy stuff and is automatically bi-directional *)

(* TODO error handling is complete crap for now.

   This should be unified with [Dune_lang.Decoder] eventually. *)

type error =
  | Parse_error of
      { message : string
      ; payload : (string * Sexp.t) list
      }
  | Version_error of
      { since : int * int
      ; until : (int * int) option
      ; message : string
      ; payload : (string * Sexp.t) list
      }

let dyn_of_error =
  let open Dyn in
  function
  | Version_error { message; payload; until; since } ->
    record
      [ "message", string message
      ; "payload", list (pair string Sexp.to_dyn) payload
      ; "until", option (pair int int) until
      ; "since", (pair int int) since
      ]
  | Parse_error { message; payload } ->
    record
      [ "message", string message; "payload", list (pair string Sexp.to_dyn) payload ]
;;

exception Of_sexp of error

let raise_of_sexp ?(payload = []) message =
  raise (Of_sexp (Parse_error { message; payload }))
;;

let raise_version_error ?until ?(payload = []) ~since message =
  raise (Of_sexp (Version_error { since; until; message; payload }))
;;

let () =
  Printexc.register_printer (function
    | Of_sexp (Parse_error { message; payload }) ->
      Some (message ^ " " ^ Sexp.to_string (Sexp.record payload))
    | _ -> None)
;;

module Fields = struct
  type t = Unparsed of Sexp.t String.Map.t

  let check_empty (Unparsed s) =
    if not (String.Map.is_empty s)
    then (
      let payload =
        [ ( "unparsed"
          , Sexp.List
              (String.Map.to_list s
               |> List.map ~f:(fun (k, v) -> Sexp.List [ Sexp.Atom k; v ])) )
        ]
      in
      raise_of_sexp ~payload "unexpected fields")
  ;;

  let empty = Unparsed String.Map.empty

  let merge (Unparsed a) (Unparsed b) =
    Unparsed
      (String.Map.union a b ~f:(fun _ _ _ ->
         (* field names are guaranteed to be different at construction time in
            [Both] *)
         assert false))
  ;;

  let of_field name sexp = Unparsed (String.Map.singleton name sexp)

  let of_sexp (x : Sexp.t) =
    match x with
    | Atom _ -> raise_of_sexp "Unexpected atom"
    | List x ->
      (match
         String.Map.of_list_map x ~f:(function
           | List [ Atom s; v ] -> s, v
           | _ -> raise_of_sexp "unable to read field")
       with
       | Error (s, _, _) -> raise_of_sexp "duplicate fields" ~payload:[ "field", Atom s ]
       | Ok s -> Unparsed s)
  ;;

  let optional (Unparsed t) name =
    match String.Map.find t name with
    | None -> None, Unparsed t
    | Some v -> Some v, Unparsed (String.Map.remove t name)
  ;;

  let required t name =
    let r, t = optional t name in
    match r with
    | Some s -> s, t
    | None -> raise_of_sexp "missing required field" ~payload:[ "name", Atom name ]
  ;;

  let to_sexp (Unparsed t) : Sexp.t =
    List (String.Map.to_list t |> List.map ~f:(fun (k, v) -> Sexp.List [ Atom k; v ]))
  ;;
end

type values = Sexp.t
type fields = Fields.t

type version =
  { since : int * int
  ; until : (int * int) option
  }

type ('a, 'kind) t =
  | String : (string, values) t
  | Int : (int, values) t
  | Float : (float, values) t
  | Unit : (unit, values) t
  | Char : (char, values) t
  | Iso : ('a, 'kind) t * ('a -> 'b) * ('b -> 'a) -> ('b, 'kind) t
  | Iso_result : ('a, 'kind) t * ('a -> ('b, exn) result) * ('b -> 'a) -> ('b, 'kind) t
  | Version : ('a, 'kind) t * version -> ('a, 'kind) t
  | Both :
      (* Invariant: field names must be different *)
      ('a, fields) t
      * ('b, fields) t
      -> ('a * 'b, fields) t
  | Sexp : (Sexp.t, values) t
  | List : ('a, values) t -> ('a list, values) t
  | Field : string * 'a field -> ('a, fields) t
  | Enum : (string * 'a) list -> ('a, values) t
  | Sum : 'a econstr list * ('a -> case) -> ('a, values) t
  | Pair : ('a, values) t * ('b, values) t -> ('a * 'b, values) t
  | Triple : ('a, values) t * ('b, values) t * ('c, values) t -> ('a * 'b * 'c, values) t
  | Fdecl : int * ('a, 'k) t Fdecl.t -> ('a, 'k) t
  | Either :
      (* Invariant: field names must be different *)
      ('a, fields) t
      * ('b, fields) t
      -> (('a, 'b) Either.t, fields) t
  | Record : ('a, fields) t -> ('a, values) t

and ('a, 'arg) constr =
  { (* TODO allow constructors without an argument *)
    name : string
  ; arg : ('arg, values) t
  ; inj : 'arg -> 'a
  }

and 'a econstr = Constr : ('a, 'arg) constr -> 'a econstr
and case = Case : 'arg * ('a, 'arg) constr -> case

and 'a field =
  | Required : ('a, values) t -> 'a field
  | Optional : ('a, values) t -> 'a option field

and 'k ret =
  | Values : values ret
  | Fields : Fields.t -> fields ret

type 'a value = ('a, values) t

let case a c = Case (a, c)
let constr name arg inj = { name; arg; inj }
let econstr c = Constr c
let both x y = Both (x, y)
let list x = List x
let sum x y = Sum (x, y)
let pair x y = Pair (x, y)
let triple x y z = Triple (x, y, z)

let discard_values ((a, x) : _ * values ret) =
  match (x : values ret) with
  | Values -> a
;;

let string = String
let int = Int
let float = Float
let unit = Unit

let option x =
  let none = constr "None" unit (fun () -> None) in
  let some = constr "Some" x (fun x -> Some x) in
  sum
    [ econstr none; econstr some ]
    (function
      | None -> case () none
      | Some s -> case s some)
;;

let char = Char

let sexp_for_digest t =
  let rec iter : type a b. int list -> (a, b) t -> Sexp.t =
    fun ids -> function
      | String -> Atom "String"
      | Int -> Atom "Int"
      | Float -> Atom "Float"
      | Unit -> Atom "Unit"
      | Char -> Atom "Char"
      | Iso (t, _, _) -> List [ Atom "Iso"; iter ids t ]
      | Iso_result (t, _, _) -> List [ Atom "Iso_result"; iter ids t ]
      | Version (t, { since = a, b; until }) ->
        let items : Sexp.t list =
          [ Atom "Version"
          ; iter ids t
          ; List [ Atom "since"; Atom (Int.to_string a); Atom (Int.to_string b) ]
          ]
        in
        let items =
          match until with
          | None -> items
          | Some (a, b) ->
            items
            @ [ List [ Atom "until"; Atom (Int.to_string a); Atom (Int.to_string b) ] ]
        in
        List items
      | Both (a, b) -> List [ Atom "Both"; iter ids a; iter ids b ]
      | Sexp -> Atom "Sexp"
      | List t -> List [ Atom "List"; iter ids t ]
      | Field (name, field) ->
        let field : Sexp.t =
          match field with
          | Required t -> List [ Atom "Required"; iter ids t ]
          | Optional t -> List [ Atom "Optional"; iter ids t ]
        in
        List [ Atom "Field"; Atom name; field ]
      | Enum cases ->
        List (Atom "Enum" :: List.map cases ~f:(fun (name, _) : Sexp.t -> Atom name))
      | Sum (constrs, _) ->
        List
          (Atom "Sum"
           :: List.map constrs ~f:(fun (Constr { name; arg; inj = _ }) : Sexp.t ->
             List [ Atom name; iter ids arg ]))
      | Pair (a, b) -> List [ Atom "Pair"; iter ids a; iter ids b ]
      | Triple (a, b, c) -> List [ Atom "Triple"; iter ids a; iter ids b; iter ids c ]
      | Fdecl (id, fdecl) ->
        (* Although the id is represented as an auto-incrementing integer, we
         find De Bruijn indices to put in the digest so that equivalent
         structures produce the same digest. *)
        (match List.findi ids ~f:(Int.equal id) with
         | Some (_, index) -> List [ Atom "Recurse"; Atom (Int.to_string index) ]
         | None -> List [ Atom "Fixpoint"; iter (id :: ids) (Fdecl.get fdecl) ])
      | Either (a, b) -> List [ Atom "Either"; iter ids a; iter ids b ]
      | Record t -> List [ Atom "Record"; iter ids t ]
  in
  iter [] t
;;

let to_sexp : 'a. ('a, values) t -> 'a -> Sexp.t =
  fun t a ->
  let rec loop : type a k. (a, k) t -> a -> k =
    fun t a ->
    match t with
    | String -> Atom a
    | Int -> Atom (Int.to_string a)
    | Float -> Atom (Float.to_string a)
    | Unit -> List []
    | Char -> Atom (String.make 1 a)
    | Sexp -> a
    | Version (t, _) -> loop t a
    | Fdecl (_, t) -> loop (Fdecl.get t) a
    | List t -> List (List.map a ~f:(loop t))
    | Pair (x, y) ->
      let a, b = a in
      List [ loop x a; loop y b ]
    | Triple (x, y, z) ->
      let a, b, c = a in
      List [ loop x a; loop y b; loop z c ]
    | Record r ->
      let fields = loop r a in
      Fields.to_sexp fields
    | Field (name, spec) ->
      (match spec with
       | Required t -> Fields.of_field name (loop t a)
       | Optional t ->
         (match a with
          | None -> Fields.empty
          | Some a -> Fields.of_field name (loop t a)))
    | Iso_result (t, _, from) -> loop t (from a)
    | Iso (t, _, from) -> loop t (from a)
    | Both (x, y) ->
      let x = loop x (fst a) in
      let y = loop y (snd a) in
      Fields.merge x y
    | Either (x, y) ->
      (match a with
       | Left a -> loop x a
       | Right a -> loop y a)
    | Sum (_, constr) ->
      let (Case (a, constr)) = constr a in
      let arg = loop constr.arg a in
      Sexp.List [ Atom constr.name; arg ]
    | Enum choices ->
      (match
         List.find_map choices ~f:(fun (s, a') ->
           if Poly.equal a a' then Some s else None)
       with
       | Some v -> Atom v
       | None ->
         let open Dyn in
         Code_error.raise
           "enum does not include this value"
           [ "valid values", list (fun (x, _) -> string x) choices ])
  in
  loop t a
;;

let check_version ~version ~since ~until _ctx =
  if
    version < since
    ||
    match until with
    | None -> false
    | Some until -> version > until
  then raise_version_error ?until ~since "invalid version"
;;

let of_sexp : 'a. ('a, values) t -> version:int * int -> Sexp.t -> 'a =
  fun t ~version sexp ->
  let rec loop : type a k. (a, k) t -> k -> a * k ret =
    fun (type a k) (t : (a, k) t) (ctx : k) : (a * k ret) ->
    match t with
    | String ->
      (match ctx with
       | Atom s -> s, Values
       | List _ as list ->
         raise_of_sexp ~payload:[ "list", list ] "string: expected atom. received list")
    | Int ->
      (match ctx with
       | List _ as list ->
         raise_of_sexp ~payload:[ "list", list ] "int: expected atom. received list"
       | Atom s ->
         (match Int.of_string s with
          | None -> raise_of_sexp "unable to read int"
          | Some i -> i, Values))
    | Float ->
      (match ctx with
       | List _ as list ->
         raise_of_sexp ~payload:[ "list", list ] "float: expected atom. received list"
       | Atom s ->
         (match Float.of_string_opt s with
          | None -> raise_of_sexp "unable to read float"
          | Some i -> i, Values))
    | Unit ->
      (match ctx with
       | List [] -> (), Values
       | _ -> raise_of_sexp "expected empty list")
    | Char ->
      (match ctx with
       | Atom s ->
         if String.length s = 1
         then s.[0], Values
         else raise_of_sexp "expected only a single character"
       | List _ -> raise_of_sexp "expected a string of length 1")
    | Sexp -> ctx, Values
    | Version (t, { since; until }) ->
      check_version ~version ~since ~until ctx;
      loop t ctx
    | Fdecl (_, t) -> loop (Fdecl.get t) ctx
    | List t ->
      (match ctx with
       | List xs -> List.map xs ~f:(fun x -> discard_values (loop t x)), Values
       | Atom _ -> raise_of_sexp "expected list")
    | Pair (x, y) ->
      (match ctx with
       | List [ a; b ] ->
         let a, Values = loop x a in
         let b, Values = loop y b in
         (a, b), Values
       | _ -> raise_of_sexp "expected field entry")
    | Triple (x, y, z) ->
      (match ctx with
       | List [ a; b; c ] ->
         let a, Values = loop x a in
         let b, Values = loop y b in
         let c, Values = loop z c in
         (a, b, c), Values
       | _ -> raise_of_sexp "expected field entry")
    | Record (r : (a, fields) t) ->
      let (fields : Fields.t) = Fields.of_sexp ctx in
      let a, Fields f = loop r fields in
      Fields.check_empty f;
      a, Values
    | Field (name, spec) ->
      (match spec with
       | Required v ->
         let field, rest = Fields.required ctx name in
         let t, Values = loop v field in
         t, Fields rest
       | Optional v ->
         let field, rest = Fields.optional ctx name in
         let t =
           match field with
           | None -> None
           | Some f ->
             let a, Values = loop v f in
             Some a
         in
         t, Fields rest)
    | Either (x, y) ->
      (try
         (* TODO share computation somehow *)
         let a, x = loop x ctx in
         Left a, x
       with
       | Of_sexp _ ->
         let a, y = loop y ctx in
         Right a, y)
    | Iso (t, f, _) ->
      let a, k = loop t ctx in
      f a, k
    | Iso_result (t, f, _) ->
      let a, k = loop t ctx in
      (match f a with
       | Error exn -> raise exn
       | Ok a -> a, k)
    | Both (x, y) ->
      let a, Fields k = loop x ctx in
      let b, k = loop y k in
      (a, b), k
    | Sum (constrs, _) ->
      (match ctx with
       | List [ Atom head; args ] ->
         (match
            List.find_map constrs ~f:(fun (Constr c) ->
              if head = c.name
              then
                Some
                  (let a, k = loop c.arg args in
                   c.inj a, k)
              else None)
          with
          | None -> raise_of_sexp "invalid constructor name"
          | Some p -> p)
       | _ -> raise_of_sexp "expected constructor")
    | Enum choices ->
      (match ctx with
       | List _ -> raise_of_sexp "expected list"
       | Atom a ->
         (match List.assoc choices a with
          | None -> raise_of_sexp "unable to read enum"
          | Some s -> s, Values))
  in
  discard_values (loop t sexp)
;;

let of_sexp conv ~version sexp =
  match of_sexp conv ~version sexp with
  | s -> Ok s
  | exception Of_sexp e -> Error e
;;

let record r = Record r
let either x y = Either (x, y)
let iso a t f = Iso (a, t, f)
let iso_result a t f = Iso_result (a, t, f)
let version ?until t ~since = Version (t, { until; since })
let field name spec = Field (name, spec)
let enum choices = Enum choices

let three a b c =
  iso (Both (a, Both (b, c))) (fun (x, (y, z)) -> x, y, z) (fun (x, y, z) -> x, (y, z))
;;

let four a b c d =
  iso
    (both (both a b) (both c d))
    (fun ((w, x), (y, z)) -> w, x, y, z)
    (fun (w, x, y, z) -> (w, x), (y, z))
;;

let five a b c d e =
  iso
    (both (both a b) (three c d e))
    (fun ((a, b), (c, d, e)) -> a, b, c, d, e)
    (fun (a, b, c, d, e) -> (a, b), (c, d, e))
;;

let six a b c d e f =
  iso
    (both (three a b c) (three d e f))
    (fun ((a, b, c), (d, e, f)) -> a, b, c, d, e, f)
    (fun (a, b, c, d, e, f) -> (a, b, c), (d, e, f))
;;

let seven a b c d e f g =
  iso
    (both (three a b c) (four d e f g))
    (fun ((a, b, c), (d, e, f, g)) -> a, b, c, d, e, f, g)
    (fun (a, b, c, d, e, f, g) -> (a, b, c), (d, e, f, g))
;;

let eight a b c d e f g h =
  iso
    (both (four a b c d) (four e f g h))
    (fun ((a, b, c, d), (e, f, g, h)) -> a, b, c, d, e, f, g, h)
    (fun (a, b, c, d, e, f, g, h) -> (a, b, c, d), (e, f, g, h))
;;

let sexp = Sexp
let required x = Required x
let optional x = Optional x
let fdecl_id = ref 0

let fixpoint f =
  let fdecl = Fdecl.create Dyn.opaque in
  let id = !fdecl_id in
  incr fdecl_id;
  let result = Fdecl (id, fdecl) in
  Fdecl.set fdecl (f result);
  result
;;

let error e = raise (Of_sexp e)
